从头打造一个 Markdown 编辑器(一):数据结构和展现

刚开始写这个编辑器的时候,我是毫无思路的,就是完全不知道如何下手,后来去翻了一下 CodeMirror, ACEditor, VSCode 这些优秀编辑器的代码,但是我没有全看,因为我要我的编辑器大部分都是我自己想出来的,只有我想不到的时候才去看。 首先我们需要一个数据结构来储存我们的文本,为什么要用数据结构而不直接用 string,是因为编辑器需要大量的增删查改操作,而当一个文本很大的时候,string 是不够快的,因为 string 的增删查改操作的时间复杂度基本上都是 O(n),还不够快。

数据结构 Data structure

一开始我打算用的是 Rope,是因为看上这个数据结构足够快,后来弃用了,是因为它和我的编辑器的 View 部分的构想不太一样,很难融合在一起,另外就是它本身自己也比较难实现,所以我就用了很简单的 chains of lines 来实现了,就是用一个数组,里面存的是每一行的内容。废话不多说先上代码

/src/model/textModel.ts

class TextModel {

    protected _lines : LineModel[];

    constructor(_string: string) {
        // ctor
    }

    // other methods
}

/src/model/lineModel.ts

class LineModel {

    protected _number : number;
    protected _text : string;

    constructor(_num : number, _t : string) {
        // ctor
    }

    // other methods
}

数据结构就是这么简单,就是用数组把每一行的内容都存起来存起来。在这里我用了一个 LineModel 的类来储存,是因为我还要实现一些别的方法,比如最典型的增删操作。当然我们实际上要实现的数据结构的操作不止那么多,至少要把insert, delete, replace这几个操作都实现了才行。这样我们文中所有的字符都可以用一个对象 Position 来表示,就是行+位移

export interface Position {
    line : number;
    offset : number;
}

而一段文字,也就是我们说的 Range,或者说选区(Selection)则可以用两个Position 来表示:

export interface Range {
    begin: Position;
    end: Position;
}

想想我们要做编辑器要做的操作

1.插入(Insert): 在文本内的一个位置(Position)插入一段文字(string)

2.删除(Delete): 删除掉一段范围(Range)内的文字

3.替换(Replace): 把一段范围(Range)内的文字替换为一段新的文字(string)

其实替换操作很容易理解,我们先删除,再插入即可,所以我们主要实现 Insert 和 Delete。具体的实现方法可以参考我的源码

展现 Presentation

下面讲讲如何展现(presentation),就是如何通过 HTML DOM 操作把数据结构里面的数据展现出来,我想大家都已经想到了,一个 LineModel 对应一行,一个父 DOM 包含着每一行的 DOM

screen present

看图可知,MDE 里面每一行其实就是一个 <p>,所以其实没什么神秘的东西,不过就是加上了行号,还有 Syntax Highlighting 而已(这个我们后面会说)。怎么把上面提到的 TextModel 编程 HTML DOM 元素了,自己写个遍历器遍历一遍就好了,在这里不推荐自己拼接 HTML 字符然后用 innerHTML 更新,这样一来效率低下,二来需要手动过滤字符,三来不方便我们后续的更新。

遍历一遍 TextModel,然后用 document.createElement 好了,这里你可能需要一个方便的工具类

/src/util/dom.ts

function elem(elemName : string, className?: string, props?: any) {
    let _elm = document.createElement(elemName);

    if (className)
        _elm.setAttribute("class", className);

    if (props && typeof props === "object") {
        for (let key in props) {
            _elm.setAttribute(key, props[key]);
        }
    }

    return _elm;
}

具体的实现大家可以参考下列几个文件,不过因为已经实现了 Syntax Highlighting,可能现在的版本已经很复杂了,初学的话看可能有点压力

/src/view/viewLine.ts

/src/view/viewDocument.ts

到现在为止,你可能已经知道怎么把 TextModel 里面的内容展现出来了,但是做一个编辑器,仅仅这样还是不够的,下一张讲讲如何更新视图。