闲谈Monaco Editor-自定义语言之Monarch

什么是Monarch

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

Monarch 由一系列 Json 键值对组成,他有许多属性,其中最重要的就是 tokenizer 属性,咱们描述语法的代码就写在这里面。先来看一个简单的例子:git

monaco.languages.setMonarchTokensProvider('log', {
	tokenizer: {
		root:[
			[/\d+/,{token:"keyword"}],
			[/[a-z]+/,{token:"string"}]
		],
	}
});
复制代码

咱们在 tokenizer 中定义了一个 root 属性,roottokenizer 中的一个 state , 这就是咱们用来编写解析规则(rule)的地方,在 rule 中,咱们能够编写匹配文本的正则表达式,而后再给匹配到的文本设置一个执行动做的 action ,在 action 中,咱们能够给匹配到的文本设置 token classgithub

在咱们的例子中,咱们在 root 中设置了两个 rule ,分别用来匹配数字字母,匹配成功后就接着执行对应的 action ,最后在 action 中,咱们设置了匹配文本的 token class :keywordstring。最终效果如图: 正则表达式

这里有些人就会有疑问,token class 是 css class 吗?本质上,token class 其实就是设置 css 中的 class ,不过,keyword != .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

咱们发现大写没有识别出来,这时,咱们能够再给完善如下匹配字符串的 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"}]
	    ],
    }
});
复制代码

最终效果以下: curl

了解了 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

不过,还有一个小问题,那就是除了errorinfowarning这三个日志类型,其他的 [debug][test] 也会被匹配进去。这时候,咱们须要引入一个新的工具:cases

{ cases: { guard1: action1, ..., guardN: actionN } }
复制代码

cases 和普通的 if ,else if 语法同样,能够写多个判断条件(guard),而后根据不一样 guard 去执行对应的 action

guard 和正则表达式相似,功能是用来匹配文本,当他不以 @$ 开头时,他就是一个普通的正则表达式,不过,当他以 @$ 开头时,他才是一个真正意义上的 guard 。

guard 有固定的结构 [pat][op]matchpat 表明匹配的文本op 表明一个比较符match 则是要比较的内容

pat$ 开头,和咱们上文正则表达式使用的 $1 含义是同样的,不过这边 $# 表明所有匹配文本,而正则表达式是使用 $0 表明所有匹配文本。另外,咱们还能够用 $Sn 来获取当前 state的名字,例如在 root state 下 $S0 就表明 root

opmatch 稍微复杂点,能够是这几个内容

  • ~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 提供的功能,例如括号匹配,语言嵌入等,也还有许多细节点未列出,同窗们若是有兴趣想深刻研究,能够阅读官方文档与示例

相关文章
相关标签/搜索