Monaco Editor 配置自定义语言高亮

10,857 阅读5分钟

在上一篇译文中,我们已经跑通了 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: microsoft.github.io/monaco-edit…