Monarch 是 Monaco Editor 自带的一个语法高亮库,经过它,咱们能够用相似 Json 的语法来实现自定义语言的语法高亮功能。本文将经过编写一个简单的自定义日志语言(下文简称 log )来介绍 Monarch 的使用。javascript
首先,咱们须要在 monaco 里注册一下咱们的 log 语言。css
monaco.languages.register({ id: 'log' });
复制代码
很简单,咱们只须要传入语言的 id 便可,可是,如今这个语言除了有个名字,还空空如也,因此,接下来,咱们就要开始给 log 语言加上咱们的语法高亮功能。html
monaco.languages.setMonarchTokensProvider('log', monarchObj);
复制代码
monaco 提供了setMonarchTokensProvider
函数来让我定义语言的高亮功能,而monarchObj
就是咱们所须要填写的 Monarch 所规定的 Json 内容。java
Monarch 由一系列 Json 键值对组成,他有许多属性,其中最重要的就是 tokenizer
属性,咱们描述语法的代码就写在这里面。先来看一个简单的例子:git
monaco.languages.setMonarchTokensProvider('log', {
tokenizer: {
root:[
[/\d+/,{token:"keyword"}],
[/[a-z]+/,{token:"string"}]
],
}
});
复制代码
咱们在 tokenizer 中定义了一个 root 属性,root 是 tokenizer 中的一个 state , 这就是咱们用来编写解析规则(rule)的地方,在 rule 中,咱们能够编写匹配文本的正则表达式,而后再给匹配到的文本设置一个执行动做的 action ,在 action 中,咱们能够给匹配到的文本设置 token class 。github
在咱们的例子中,咱们在 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]
复制代码
不过上面的高亮代码还存在一点问题json
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"}]
],
}
});
复制代码
最终效果以下: curl
咱们要实现的 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'
});
复制代码
[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 稍微复杂点,能够是这几个内容
有了这些工具,咱们能够接着写咱们的高亮代码
{
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 提供的功能,例如括号匹配,语言嵌入等,也还有许多细节点未列出,同窗们若是有兴趣想深刻研究,能够阅读官方文档与示例。