在上一篇译文中,我们已经跑通了 Monaco Editor 的项目,接下来我们来具体看下,如何配置自定义语言高亮。
Monaco Editor 通过自带的语法高亮库 Monarch 来支持配置自定义语言。通过它,即可使用 JSON 创建声明式语法支持高亮。我们可以通过 monarch 提供 playground 来在线调试我们的自定义语言。
语言定义基本上只是描述语言的各种属性的 JSON 值。

开始

Monaco 通过 setMonarchTokensProvider(languageId, monarchConfig) 函数来定义语言的高亮功能,它的参数有两个,一个是自定义语言设定的 ID,一个就是上述的 JSON 配置项了。
我们先来看个例子:

  1. {
  2. tokenizer: {
  3. root:[
  4. [/\d+/,{token:"keyword"}],
  5. [/[a-z]+/,{token:"string"}]
  6. ],
  7. }
  8. }

我们在 tokenizer 中定义了一个 root 属性,root 是 tokenizer 中的一个 state , 这就是我们用来编写解析规则(rule)的地方,在 rule 中,我们可以编写匹配文本的正则表达式,然后再给匹配到的文本设置一个执行动作的 action ,在 action 中,我们可以给匹配到的文本设置 token class 。

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

Monaco Editor 配置自定义语言高亮 - 图1
Monarch 中内置了以下几种 token class:

  1. identifier entity constructor
  2. operators tag namespace
  3. keyword info-token type
  4. string warn-token predefined
  5. string.escape error-token invalid
  6. comment debug-token
  7. comment.doc regexp
  8. constant attribute
  9. delimiter .[curly,square,parenthesis,angle,array,bracket]
  10. number .[hex,octal,binary,float]
  11. variable .[name,value]
  12. meta .[content]

可以看到上面的高亮还有问题,大写的 TEST 没有被识别出来,这时,我们可以再给完善以下匹配字符串的 rule 正则表达式。

  1. tokenizer: {
  2. root:[
  3. [/\d+/,{token:"keyword"}],
  4. [/[a-zA-Z]+/,{token:"string"}]
  5. ],
  6. }

假如我们的语言是忽略大小写的,那么,我们可以直接添加一条 ignoreCase 属性。

  1. {
  2. ignoreCase: true,
  3. tokenizer: {
  4. root:[
  5. [/\d+/, {token: "keyword"}],
  6. [/[a-z]+/, {token: "string"}]
  7. ],
  8. }
  9. }

我们再来看下高亮效果:
Monaco Editor 配置自定义语言高亮 - 图2

Monarch 使用创建声明式 JSON

通过上述示例,我们可以看到大致的配置了,接下来具体看下 monarch 的相关用法。
该库允许你使用 JSON 值来指定高效的语法突出显示工具。该规范具有足够的表现力,可以指定具有复杂状态转换,动态花括号匹配,自动完成,其他语言嵌入等功能的高亮。

创建语言定义

语言定义基本上只是描述语言的各种属性的 JSON 值,一些默认的属性有:
ignoreCase :语言是否区分大小写?默认为 true
defaultToken :如果 token 生成器中没有匹配项,则返回默认 token。
brackets :token 生成器使用定义匹配的括号。每个括号定义是一个由3个元素或对象组成的数组,用于描述 open括号,close 括号和 token 类。默认定义是:

  1. [
  2. [''','}','delimiter.curly'],
  3. ['['']''delimiter.square'],
  4. ['('')''delimiter.parenthesis'],
  5. ['<''>''delimiter.angle']
  6. ]

tokenizer : 必选项,这定义了标记化规则。

创建一个 tokenizer

tokenizer 属性描述了词法分析是如何进行的,以及如何将输入划分为 token 。每个标记都有一个 CSS 类名称,该名称用于在编辑器中呈现每个 token。标准 CSS token 类包括:

  1. identifier entity constructor
  2. operators tag namespace
  3. keyword info-token type
  4. string warn-token predefined
  5. string.escape error-token invalid
  6. comment debug-token
  7. comment.doc regexp
  8. constant attribute
  9. delimiter .[curly,square,parenthesis,angle,array,bracket]
  10. number .[hex,octal,binary,float]
  11. variable .[name,value]
  12. 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 。例如,这可以用于标记开始块注释:

  1. ['/ \\ *''comment''@ comment']

请注意这是以下的简写:

  1. { regex: '/\\*', action: { token: 'comment', next: '@comment' } }

有一些特殊状态可用于该 next 属性:
@pop:使令牌生成器堆栈返回到先前的状态。例如,这在用于看到结束 token 后从块注释 token 返回:

  1. ['\\*/', 'comment', '@pop']

@push: 推入当前状态并继续当前状态。在看到注释开始 token 时执行嵌套的块注释,即在 @comment 状态下,我们可以执行以下操作:

  1. ['/\\*', 'comment', '@push']

@popall: 从令牌生成器堆栈中推出所有内容,并返回到栈顶状态。可以在恢复期间使用它,以从深度嵌套级别“跳回”到初始状态。

log : 用于调试。登录 message 到浏览器中的控制台窗口(按F12进行查看)。

  1. [/\d+/, { token: 'number', log: 'found number $0 in state $S0' } ]

cases

{ cases: { guard1: action1, ..., guardN: actionN } }
最后一种操作对象是 case 语句。case 对象包含一个对象,其中每个字段均用作条件选择。将每个 guard 应用于匹配的输入,并且一旦其中一个匹配,就会应用相应的 action 操作。注意,由于这些本身就是 action,因此 case 可以嵌套。使用 case 来提高效率:例如,我们匹配标识符,然后测试标识符是否可能是关键字或内置函数:

  1. [/[a-z_\$][a-zA-Z0-9_\$]*/,
  2. { cases: { '@typeKeywords': 'keyword.type'
  3. , '@keywords': 'keyword'
  4. , '@default': 'identifier' }
  5. }
  6. ]

guard 可以包括:

  • @keywords 该属性 keywords 必须提前在语言对象中定义,并且由字符串数组组成。如果输入匹配到任何字符串,则条件判断成功。
  • @default 始终成功的匹配 “@” 或 “”
  • @eos 如果匹配的输入已到达行尾
  • regex 如果不是以@(或$)字符开头,则将其解释为对匹配输入进行测试的正则表达式。例如,这可以用于测试特定的输入,这是 Koka 语言的示例,
  1. [/[a-z](\w|\-[a-zA-Z])*/,
  2. { cases:{ '@keywords': {
  3. cases: { 'alias' : { token: 'keyword', next: '@alias-type' }
  4. , 'struct' : { token: 'keyword', next: '@struct-type' }
  5. , 'type|cotype|rectype': { token: 'keyword', next: '@type' }
  6. , 'module|as|import' : { token: 'keyword', next: '@module' }
  7. , '@default' : 'keyword' }
  8. }
  9. , '@builtins': 'predefined'
  10. , '@default' : 'identifier' }
  11. }
  12. ]

请注意可以使用嵌套 case 来提高效率。此外,该库可识别上述简单正则表达式,并有效地对其进行编译。

理解上述定义之后,已基本掌握配置自定义语言高亮的写法了,接下来我们来解读下官方示例

Monaco 官方示例

  1. {
  2. // 为了插件尚未被 token 解析的内容,设置 defaultToken invalid
  3. defaultToken: 'invalid',
  4. // 关键字定义
  5. keywords: [
  6. 'abstract', 'continue', 'for', 'new', 'switch', 'assert', 'goto', 'do',
  7. 'if', 'private', 'this', 'break', 'protected', 'throw', 'else', 'public',
  8. 'enum', 'return', 'catch', 'try', 'interface', 'static', 'class',
  9. 'finally', 'const', 'super', 'while', 'true', 'false'
  10. ],
  11. // 类型定义
  12. typeKeywords: [
  13. 'boolean', 'double', 'byte', 'int', 'short', 'char', 'void', 'long', 'float'
  14. ],
  15. // 操作符定义
  16. operators: [
  17. '=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
  18. '&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%',
  19. '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=',
  20. '%=', '<<=', '>>=', '>>>='
  21. ],
  22. // 定义常见的正则表达式
  23. symbols: /[=><!~?:&|+\-*\/\^%]+/,
  24. // C# 样式字符串
  25. escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
  26. // 语言的主要 token 生成器
  27. tokenizer: {
  28. root: [
  29. // 标识符与关键字
  30. [/[a-z_$][\w$]*/, { cases: { '@typeKeywords': 'keyword',
  31. '@keywords': 'keyword',
  32. '@default': 'identifier' } }],
  33. [/[A-Z][\w\$]*/, 'type.identifier' ], // to show class names nicely
  34. // 空格
  35. { include: '@whitespace' },
  36. // 括号与运算符
  37. [/[{}()\[\]]/, '@brackets'],
  38. [/[<>](?!@symbols)/, '@brackets'],
  39. [/@symbols/, { cases: { '@operators': 'operator',
  40. '@default' : '' } } ],
  41. // @ 注释.
  42. // 作为示例,我们在这些 token 上发出调试日志消息
  43. [/@\s*[a-zA-Z_\$][\w\$]*/, { token: 'annotation', log: 'annotation token: $0' }],
  44. // 各类数字定义
  45. [/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
  46. [/0[xX][0-9a-fA-F]+/, 'number.hex'],
  47. [/\d+/, 'number'],
  48. // 分隔符
  49. [/[;,.]/, 'delimiter'],
  50. // 字符串定义
  51. [/"([^"\\]|\\.)*$/, 'string.invalid' ], // non-teminated string
  52. [/"/, { token: 'string.quote', bracket: '@open', next: '@string' } ],
  53. [/'[^\\']'/, 'string'],
  54. [/(')(@escapes)(')/, ['string','string.escape','string']],
  55. [/'/, 'string.invalid']
  56. ],
  57. // 自定义规则 - 备注
  58. comment: [
  59. [/[^\/*]+/, 'comment' ],
  60. [/\/\*/, 'comment', '@push' ], // nested comment
  61. ["\\*/", 'comment', '@pop' ],
  62. [/[\/*]/, 'comment' ]
  63. ],
  64. // 自定义规则 - 字符串
  65. string: [
  66. [/[^\\"]+/, 'string'],
  67. [/@escapes/, 'string.escape'],
  68. [/\\./, 'string.escape.invalid'],
  69. [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' } ]
  70. ],
  71. // 自定义规则 - 空格
  72. whitespace: [
  73. [/[ \t\r\n]+/, 'white'],
  74. [/\/\*/, 'comment', '@comment' ],
  75. [/\/\/.*$/, 'comment'],
  76. ],
  77. },
  78. };

通过以上的学习与理解,一个自定义语言的配置我们已能配置出来了。

相关参考

monarch playgroud: https://microsoft.github.io/monaco-editor/monarch.html