前一篇中的基础使用和详解介绍了基本的SQL IDE的构建。但是有的时候,我们期望按照需求去高亮我们自定义的语句。这个时候我们需要通过CodeMirror中的defineMode去构建自己的高亮逻辑。
CodeMirror.defineMode详解
defineMode的类型定义如下:
/**
* id will be the id for the defined mode. Typically, you should use this second argument to defineMode as your module scope function
* (modes should not leak anything into the global scope!), i.e. write your whole mode inside this function.
*/
function defineMode(id: string, modefactory: ModeFactory<any>): void;
我们可以看到函数中具有两个参数:id、modefactory
- id:Mode 的名称,应为小写的字符串,最好是 Mode 的文件名
- modefactory: 第二个参数是一个函数,该函数的第一个参数为 CodeMirror 配置对象(
CodeMirror
构造函数的配置对象), 该函数的第二个可选参数为 Mode 的配置对象(mode
),返回值为 Mode 对象
/**
* A function that, given a CodeMirror configuration object and an optional mode configuration object, returns a mode object.
*/
interface ModeFactory<T> {
(config: EditorConfiguration, modeOptions?: any): Mode<T>;
}
然后我们可以看到Mode对象的类型
interface Mode<T> {
name?: string | undefined;
/**
* This function should read one token from the stream it is given as an argument, optionally update its state,
* and return a style string, or null for tokens that do not have to be styled. Multiple styles can be returned, separated by spaces.
*/
token: (stream: StringStream, state: T) => string | null;
startState?: (() => T) | undefined;
blankLine?: ((state: T) => void) | undefined;
copyState?: ((state: T) => T) | undefined;
indent?: ((state: T, textAfter: string, line: string) => number) | undefined;
lineComment?: string | undefined;
blockCommentStart?: string | undefined;
blockCommentEnd?: string | undefined;
blockCommentLead?: string | undefined;
electricChars?: string | undefined;
electricinput?: RegExp | undefined;
}
我们这里比较关注的其实是token
这个函数,所有 Mode 都必须定义此方法。 该函数从 stream
参数中提取 token
,可选的更新状态,并返回样式字符串,或 null
表示 token
无需样式。大致来说,就是我们通过token解析出来字符,然后根据业务逻辑返回这个字符属于某个样式。在codeMirror中,一般会有如下样式。如果是变量,你可以返回'variable'
字符串。那么对于这个字符来说,就会命中cm-operator
这个样式。当然,如果你需要修改样式,你可以直接使用css文件覆盖或者在外出wrapper中添加className。原理类似于theme的配置。
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
基本实现
cm.defineMode("customer", () => {
const wordReg = (words: string[]) => new RegExp(`^(${words.join("|")})$`);
const variables = []
const builtins = []
const booleanStrings = []
const isOperatorChar = /[+\-*&%=<>!?|^\/]/;
const isVariable = wordReg(variables);
const isBuiltins = wordReg(builtins);
const isBoolean = wordReg(booleanStrings);
const stringEater = (type: any) => {
let prev = "";
return (c: any) => {
prev = c;
if (c === type) {
return prev === "\\";
}
return true;
};
};
return {
startState() {
return {
prev: false,
};
},
token(stream, state) {
let ch;
if (stream.eatSpace()) {
state.prev = true;
return null;
}
state.prev = false;
ch = stream.next();
// 字符串
if (ch === "'" || ch === '"') {
stream.eatWhile(stringEater(ch));
stream.next();
return "string";
}
// 操作符
if (ch && isOperatorChar.test(ch)) {
return "meta";
}
// 如果是()
if (ch === "(" || ch === ")") {
return "bracket";
}
return null;
},
};
});
cm.defineMIME("text/customer", "customer");
token相关API详解
-
eol() → boolean
。 当 stream 在行尾时返回 true。 -
sol() → boolean
。 当 stream 在行首时返回 true。 -
peek() → string
。返回流中的下一个字符但不前进下标。在行尾时返回null
。 -
next() → string
。返回流中的下一个字符且前进下标。在该行没有剩余字符时返回null
。 -
eat(match: string|regexp|function(char: string) → boolean) → string
。match
可以是字符串、正则表达式或一个参数为字符串并返回 boolean 值的函数。 如果 stream 中的下一个字符与参数项匹配,将会被消耗掉并返回。否则返回undefined
。 -
eatWhile(match: string|regexp|function(char: string) → boolean) → boolean
。 重复调用eat
直到截止。当所有的单词都被消耗掉后返回 true。 -
eatSpace() → boolean
。用eatWhile
匹配空格的快捷函数。 -
skipToEnd()
。移动当前位置到行尾。 -
skipTo(str: string) → boolean
。如果再当前行上找到指定的字符,则跳转到下个字符的开始位置(如果没有找到则不跳转)。找到时返回 true。 -
match(pattern: string, consume?: boolean, caseFold?: boolean) → boolean
-
match(pattern: regexp, consume?: boolean) → array<string>
。consume
为 true 时,类似多字符的eat
函数, 为 false 或未指定时,不会更新 stream 的位置。pattern
可以是字符串或由^
开头的正则表达式。 当为字符串时,caseFold
为 true 时则不区分大小写。 当为正则表达式时,match
的返回值是一个数组,可以用来提取匹配结果。 -
backUp(n: integer)
。后退n
个字符。 后退超过当前 token 的开始位置时会导致终端,请当心。 -
column() → integer
。返回当前 token 开始位置的列。 -
indentation() → integer
。 返回当前行缩进的数量(以空格为单位),不是 tab 符号。 -
current() → string
。返回当前 token 开始位置到当前 stream 位置之间的字符串。 -
lookAhead(n: number) → string | null
。返回当前行后的n
(>0) 行,用来跨行扫描。 注意:你需要小心地使用该函数,跨太多的行会降低 Mode 状态的缓存效率。 -
baseToken() → {type?: string, size: number} | null
。 通过addOverlay
函数添加的 Mode 可以使用该函数来检查当前Mode 下的 Token 。
总结
CodeMirror具备良好的可扩展性,因此如果需要在已有Mode进行扩展或者继承,我们可以使用CodeMirror.extendMode
函数为 Mode 对象添加属性来生成具体的 Mode。 它的第一个参数是 Mode 的名称,第二个参数需要添加的属性的对象。
木更 2022.08.01