什么是Monarch
Monarch 是 Monaco Editor 自带的一个语法高亮库,通过它,我们可以用类似 Json 的语法来实现自定义语言的语法高亮功能。本文将通过编写一个简单的自定义日志语言(下文简称 log )来介绍 Monarch 的使用。
开始
初始化
首先,我们需要在 monaco 里注册一下我们的 log 语言。
monaco.languages.register({ id: 'log' });
很简单,我们只需要传入语言的 id 即可,但是,现在这个语言除了有个名字,还空空如也,所以,接下来,我们就要开始给 log 语言加上我们的语法高亮功能。
monaco.languages.setMonarchTokensProvider('log', monarchObj);
monaco 提供了setMonarchTokensProvider
函数来让我定义语言的高亮功能,而monarchObj
就是我们所需要填写的 Monarch 所规定的 Json 内容。
Monarch
Monarch 由一系列 Json 键值对组成,他有许多属性,其中最重要的就是 tokenizer
属性,我们描述语法的代码就写在这里面。先来看一个简单的例子:
monaco.languages.setMonarchTokensProvider('log', {
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
。最终效果如图:
.keyword
,Monarch 中会有一层对应关系,keyword 对应着 css 中的 .mtk8
,而 string 对应着 css 中的 .mtk5
。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]
不过上面的高亮代码还存在一点问题
我们发现大写没有识别出来,这时,我们可以再给完善以下匹配字符串的 rule 正则表达式。tokenizer: {
root:[
[/\d+/,{token:"keyword"}],
[/[a-zA-Z]+/,{token:"string"}]
],
}
假如我们的语言是忽略大小写的,那么,我们可以直接添加一条 ignoreCase
属性。
monaco.languages.setMonarchTokensProvider('log', {
ignoreCase: true,
tokenizer: {
root:[
[/\d+/, {token: "keyword"}],
[/[a-z]+/, {token: "string"}]
],
}
});
最终效果如下:
了解了 Monarch 的基本结构,下面,我们就开始正式编写 log 语言。log 语言
我们要实现的 log 语言主要是用来区分显示不同类型的日志,大体效果如下:
我们以[error]
, [info]
, [warning]
作为一行的开头,从而代表日志的级别。如图所示, error
后的日志将全部为红色,直到遇到下一个日志级别。
标记日志级别
首先,我们来标记一下[error]
,[info]
这些日志级别的显示。
tokenizer: {
root: [
[/^\[error\]/, { token: "custom-error" }],
[/^\[info\]/, { token: "custom-info" }],
[/^\[warning\]/, { token: "custom-warning" }]
]
}
//设置含有custom-error等token class的主题
monaco.editor.defineTheme('logTheme', {
base: 'vs',
inherit: true,
rules: [
{ token: 'custom-info', foreground: '808080' },
{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
{ token: 'custom-warning', foreground: 'FFA500' }
]
});
monaco.editor.create(document.getElementById("container"), {
theme: 'logTheme',
value: getCode(),
language: 'log'
});
我们写了三条 rule ,分别将 [error]
标记为 custom-error
,[info]
标记为 custom-info
,[warning]
标记为 custom-warning
。我们发现,这些 rule 都是类似的,所以,我们可以想办法把他们合在一起。
tokenizer: {
root: [
[/^\[(\w+)\]/, { token: "custom-$1" }]
]
}
这里我们用到了一个美元符号 $
,它代表取正则表达式第几个匹配项,$0
代表取所有的匹配项(例:[error]),$1
代表取第一个匹配项(例:error)。上述代码将日志类型作为参数传入了 token class ,与 custom-
做拼接,从而组成了最终的 token class,例如 custom-error
。
不过,还有一个小问题,那就是除了error
,info
,warning
这三个日志类型,其余的 [debug]
,[test]
也会被匹配进去。这时候,我们需要引入一个新的工具:cases
。
{ cases: { guard1: action1, ..., guardN: actionN } }
cases 和普通的 if ,else if 语法一样,可以写多个判断条件(guard),然后根据不同 guard 去执行对应的 action 。
guard 和正则表达式类似,功能是用来匹配文本,当他不以 @
或 $
开头时,他就是一个普通的正则表达式,不过,当他以 @
或 $
开头时,他才是一个真正意义上的 guard 。
guard 有固定的结构 [pat][op]match
,pat 代表匹配的文本,op 代表一个比较符,match 则是要比较的内容。
pat 以 $
开头,和我们上文正则表达式使用的 $1
含义是一样的,不过这边 $#
代表全部匹配文本,而正则表达式是使用 $0
代表全部匹配文本。另外,我们还可以用 $Sn
来获取当前 state的名字,例如在 root state 下 $S0
就代表 root
。
op 和 match 稍微复杂点,可以是这几个内容
- ~regex or !~regex :匹配/不匹配一个正则
- @attribute or !@attribute :匹配/不匹配一个属性,属性定义在 Monarch 的根层级下,可以是数组、字符串、正则。
- ==str or !=str :匹配/不匹配一个字符串
- @default :匹配默认情况
- @eos : 一行结束,则匹配成功
有了这些工具,我们可以接着写我们的高亮代码
{
keywords: ['error', 'info', 'warning'],
tokenizer: {
root: [
[/^\[(\w+)\]/, {
cases: {
"$1@keywords": { token: 'custom-$1' },
"@default": { token: "string" }
}
}]
]
}
}
这里,我们用到了 $1@keywords
来判断日志类型($1) 是否存在于 keywords
数组中,还用到了 @default
来匹配未定义的日志类型。最终效果如下:
标记日志
tokenizer: {
root: [
[/^\[(\w+)\]/, {
cases: {
"$1@keywords": {token:'custom-$1', next:"@text.$1"},
"@default":'string'
}
}],
],
text:[
[/^\[(\w+)\]/,{token:"@rematch",next:"@pop"}],
[/.*/,{token:"custom-$S2"}]
]
}
这里第一次出现了 next: "@text.$1"
,意思是由当前 root state 跳入 text state,并且把 root state 放入 tokenizer 栈中,在 text state 中,我们又可以通过 next:@pop
回到栈的第一个 state 中,也就是我们的 root state。
这里还有一个 @rematch
,意思是,匹配到了当前文本,但是,不做任何操作,让后续的 rule 再匹配一次。
总结起来,上述代码的逻辑就是匹配到日志类型之后,我们携带着日志类型($1) 进入到了 text state ,在 text state 中,我们将后续文本(.*) 都标记成和 日志类型相同的 token class ,然后在遇到日志类型标记之后,利用 @rematch
和 @pop
重新回到 root state 再次执行匹配。效果如下:
{
keywords: ['error', 'warning', 'info'],
header: /\[(\w+)\]/,
tokenizer: {
root: [
[/^@header/, {
cases: {
"$1@keywords": { token: 'custom-$1', next: "@text.$1" },
"@default": 'string'
}
}],
],
text: [
[/^@header/, { token: "@rematch", next: "@pop" }],
[/.*/, { token: "custom-$S2" }]
]
}
}
我们将匹配日志类型的正则表达式提取为一个单独的 header ,然后通过 @
来嵌入。但是这里的 @
和 guard
的 @
不同,他只支持正则表达式,而不支持数组类型。
结尾
本文介绍了 Monarch 的基本概念和使用方法,不过篇幅有限,本文无法介绍其他 Monarch 提供的功能,例如括号匹配,语言嵌入等,也还有许多细节点未列出,同学们如果有兴趣想深入研究,可以阅读官方文档与示例。