在上一篇译文中,我们已经跑通了 Monaco Editor 的项目,接下来我们来具体看下,如何配置自定义语言高亮。
Monaco Editor 通过自带的语法高亮库 Monarch 来支持配置自定义语言。通过它,即可使用 JSON 创建声明式语法支持高亮。我们可以通过 monarch 提供 playground 来在线调试我们的自定义语言。
语言定义基本上只是描述语言的各种属性的 JSON 值。
开始
Monaco 通过 setMonarchTokensProvider(languageId, monarchConfig)
函数来定义语言的高亮功能,它的参数有两个,一个是自定义语言设定的 ID,一个就是上述的 JSON 配置项了。
我们先来看个例子:
{
tokenizer: {
root:[
[/\d+/,{token:"keyword"}],
[/[a-z]+/,{token:"string"}]
],
}
}
我们在 tokenizer 中定义了一个 root 属性,root 是 tokenizer 中的一个 state , 这就是我们用来编写解析规则(rule)的地方,在 rule 中,我们可以编写匹配文本的正则表达式,然后再给匹配到的文本设置一个执行动作的 action ,在 action 中,我们可以给匹配到的文本设置 token class 。
在我们的例子中,我们在 root 中设置了两个 rule ,分别用来匹配数字和字母,匹配成功后就接着执行对应的 action ,最后在 action 中,我们设置了匹配文本的 token class :keyword 和 string。最终效果如图:
Monarch 中内置了以下几种 token class:
identifier entity constructor
operators tag namespace
keyword info-token type
string warn-token predefined
string.escape error-token invalid
comment debug-token
comment.doc regexp
constant attribute
delimiter .[curly,square,parenthesis,angle,array,bracket]
number .[hex,octal,binary,float]
variable .[name,value]
meta .[content]
可以看到上面的高亮还有问题,大写的 TEST 没有被识别出来,这时,我们可以再给完善以下匹配字符串的 rule 正则表达式。
tokenizer: {
root:[
[/\d+/,{token:"keyword"}],
[/[a-zA-Z]+/,{token:"string"}]
],
}
假如我们的语言是忽略大小写的,那么,我们可以直接添加一条 ignoreCase 属性。
{
ignoreCase: true,
tokenizer: {
root:[
[/\d+/, {token: "keyword"}],
[/[a-z]+/, {token: "string"}]
],
}
}
我们再来看下高亮效果:
Monarch 使用创建声明式 JSON
通过上述示例,我们可以看到大致的配置了,接下来具体看下 monarch 的相关用法。
该库允许你使用 JSON 值来指定高效的语法突出显示工具。该规范具有足够的表现力,可以指定具有复杂状态转换,动态花括号匹配,自动完成,其他语言嵌入等功能的高亮。
创建语言定义
语言定义基本上只是描述语言的各种属性的 JSON 值,一些默认的属性有:
ignoreCase
:语言是否区分大小写?默认为 true
。
defaultToken
:如果 token 生成器中没有匹配项,则返回默认 token。
brackets
:token 生成器使用定义匹配的括号。每个括号定义是一个由3个元素或对象组成的数组,用于描述 open括号,close 括号和 token 类。默认定义是:
[
[''','}','delimiter.curly'],
['[',']','delimiter.square'],
['(',')','delimiter.parenthesis'],
['<','>','delimiter.angle']
]
tokenizer
: 必选项,这定义了标记化规则。
创建一个 tokenizer
tokenizer
属性描述了词法分析是如何进行的,以及如何将输入划分为 token 。每个标记都有一个 CSS 类名称,该名称用于在编辑器中呈现每个 token。标准 CSS token 类包括:
identifier entity constructor
operators tag namespace
keyword info-token type
string warn-token predefined
string.escape error-token invalid
comment debug-token
comment.doc regexp
constant attribute
delimiter .[curly,square,parenthesis,angle,array,bracket]
number .[hex,octal,binary,float]
variable .[name,value]
meta .[content]
状态
分词器由定义状态的对象组成。令牌生成器的初始状态是令牌生成器中定义的第一个状态。当令牌生成器处于特定状态时,将仅应用该状态中的规则。所有规则都按顺序匹配,并且当第一个匹配时,将使用其操作来确定令牌类。不会尝试进一步的规则。因此,以最高效的方式对规则进行排序很重要,即首先使用空格和标识符。
规则
每个状态都定义为一组规则,用于匹配输入。规则可以采用以下形式:
[regex, action]
[regex, action, next]
可以简写为{ regex: regex, action: action{next: next} }
{regex: regex, action: action }
{ include: state }
当 regex 与当前输入匹配时,则 action 设置的令牌类作用于该输入。正则表达式 regex 可以是正则表达式(使用),也可以是表示正则表达式的字符串。如果以字符开头,则表达式仅在源代码行的开头匹配。请注意,当行尾已经到达时,不会调用令牌生成器,因此,空模式 /$/
将永远不会匹配。
include
是为了更好地组织规则,并引入定义的所有规则 state。
Actions
actions 确定结果标记类。可以具有以下形式:
string
[action1,...,actionN]
{ token: tokenclass }
@brackets
或者@brackets.tokenclass
一个 action 对象可以包含更多影响词法分析器状态的字段。可以识别以下属性:
next
: state,(字符串)如果已定义,则将当前状态压入令牌生成器堆栈并生成当前状态 state 。例如,这可以用于标记开始块注释:
['/ \\ *','comment','@ comment']
请注意这是以下的简写:
{ regex: '/\\*', action: { token: 'comment', next: '@comment' } }
有一些特殊状态可用于该 next
属性:
@pop:使令牌生成器堆栈返回到先前的状态。例如,这在用于看到结束 token 后从块注释 token 返回:
['\\*/', 'comment', '@pop']
@push: 推入当前状态并继续当前状态。在看到注释开始 token 时执行嵌套的块注释,即在 @comment
状态下,我们可以执行以下操作:
['/\\*', 'comment', '@push']
@popall: 从令牌生成器堆栈中推出所有内容,并返回到栈顶状态。可以在恢复期间使用它,以从深度嵌套级别“跳回”到初始状态。
log
: 用于调试。登录 message 到浏览器中的控制台窗口(按F12进行查看)。
[/\d+/, { token: 'number', log: 'found number $0 in state $S0' } ]
cases
{ cases: { guard1: action1, ..., guardN: actionN } }
最后一种操作对象是 case 语句。case 对象包含一个对象,其中每个字段均用作条件选择。将每个 guard 应用于匹配的输入,并且一旦其中一个匹配,就会应用相应的 action 操作。注意,由于这些本身就是 action,因此 case 可以嵌套。使用 case 来提高效率:例如,我们匹配标识符,然后测试标识符是否可能是关键字或内置函数:
[/[a-z_\$][a-zA-Z0-9_\$]*/,
{ cases: { '@typeKeywords': 'keyword.type'
, '@keywords': 'keyword'
, '@default': 'identifier' }
}
]
guard 可以包括:
- @keywords 该属性 keywords 必须提前在语言对象中定义,并且由字符串数组组成。如果输入匹配到任何字符串,则条件判断成功。
- @default 始终成功的匹配 “@” 或 “”
- @eos 如果匹配的输入已到达行尾
- regex 如果不是以@(或$)字符开头,则将其解释为对匹配输入进行测试的正则表达式。例如,这可以用于测试特定的输入,这是 Koka 语言的示例,
[/[a-z](\w|\-[a-zA-Z])*/,
{ cases:{ '@keywords': {
cases: { 'alias' : { token: 'keyword', next: '@alias-type' }
, 'struct' : { token: 'keyword', next: '@struct-type' }
, 'type|cotype|rectype': { token: 'keyword', next: '@type' }
, 'module|as|import' : { token: 'keyword', next: '@module' }
, '@default' : 'keyword' }
}
, '@builtins': 'predefined'
, '@default' : 'identifier' }
}
]
请注意可以使用嵌套 case 来提高效率。此外,该库可识别上述简单正则表达式,并有效地对其进行编译。
理解上述定义之后,已基本掌握配置自定义语言高亮的写法了,接下来我们来解读下官方示例
Monaco 官方示例
{
// 为了插件尚未被 token 解析的内容,设置 defaultToken 为 invalid
defaultToken: 'invalid',
// 关键字定义
keywords: [
'abstract', 'continue', 'for', 'new', 'switch', 'assert', 'goto', 'do',
'if', 'private', 'this', 'break', 'protected', 'throw', 'else', 'public',
'enum', 'return', 'catch', 'try', 'interface', 'static', 'class',
'finally', 'const', 'super', 'while', 'true', 'false'
],
// 类型定义
typeKeywords: [
'boolean', 'double', 'byte', 'int', 'short', 'char', 'void', 'long', 'float'
],
// 操作符定义
operators: [
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%',
'<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=',
'%=', '<<=', '>>=', '>>>='
],
// 定义常见的正则表达式
symbols: /[=><!~?:&|+\-*\/\^%]+/,
// C# 样式字符串
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
// 语言的主要 token 生成器
tokenizer: {
root: [
// 标识符与关键字
[/[a-z_$][\w$]*/, { cases: { '@typeKeywords': 'keyword',
'@keywords': 'keyword',
'@default': 'identifier' } }],
[/[A-Z][\w\$]*/, 'type.identifier' ], // to show class names nicely
// 空格
{ include: '@whitespace' },
// 括号与运算符
[/[{}()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[/@symbols/, { cases: { '@operators': 'operator',
'@default' : '' } } ],
// @ 注释.
// 作为示例,我们在这些 token 上发出调试日志消息
[/@\s*[a-zA-Z_\$][\w\$]*/, { token: 'annotation', log: 'annotation token: $0' }],
// 各类数字定义
[/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
[/0[xX][0-9a-fA-F]+/, 'number.hex'],
[/\d+/, 'number'],
// 分隔符
[/[;,.]/, 'delimiter'],
// 字符串定义
[/"([^"\\]|\\.)*$/, 'string.invalid' ], // non-teminated string
[/"/, { token: 'string.quote', bracket: '@open', next: '@string' } ],
[/'[^\\']'/, 'string'],
[/(')(@escapes)(')/, ['string','string.escape','string']],
[/'/, 'string.invalid']
],
// 自定义规则 - 备注
comment: [
[/[^\/*]+/, 'comment' ],
[/\/\*/, 'comment', '@push' ], // nested comment
["\\*/", 'comment', '@pop' ],
[/[\/*]/, 'comment' ]
],
// 自定义规则 - 字符串
string: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' } ]
],
// 自定义规则 - 空格
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\*/, 'comment', '@comment' ],
[/\/\/.*$/, 'comment'],
],
},
};
通过以上的学习与理解,一个自定义语言的配置我们已能配置出来了。
相关参考
monarch playgroud: https://microsoft.github.io/monaco-editor/monarch.html