博客 揭秘如何用Monaco Editor打造功能强大的日志查看器(上)

揭秘如何用Monaco Editor打造功能强大的日志查看器(上)

   数栈君   发表于 2024-12-31 16:25  318  0

Web IDE 中,控制台中展示日志是至关重要的功能。Monaco Editor 作为一个强大的代码编辑器,提供了丰富的功能和灵活的 API ,支持为内容进行装饰,非常适合用来构建日志展示器。如下图:

http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/7831654b24e1c54e313df55c1b8054a2..png

除了实时日志外,还有一些需要查看历史日志的场景。如下图:

http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/c8faaf77eef09ce57e37dc79a19e383d..png

Monarch

Monarch 是 Monaco Editor 自带的一个语法高亮库,通过它,我们可以用类似 JSON 的语法来实现自定义语言语法高亮功能。这里不做过多的介绍,只介绍在本文中使用到的那部分内容。

一个语言定义基本上就是描述语言的各种属性的JSON值,部分通用属性如下:

· tokenizer(必填项,带状态的对象)这个定义了tokenization的规则。 Monaco Editor 中用于定义语言语法高亮和解析的一个核心组件。它的主要功能是将输入的代码文本分解成一个个的 token,以便于编辑器能够根据这些 token 进行语法高亮、错误检查和其他编辑功能。

· ignoreCase(可选项=false,布尔值)语言是否大小写不敏感?tokenizer(分词器)中的正则表达式使用这个属性去进行大小写(不)敏感匹配,以及case场景中的测试。

· brackets(可选项,括号定义的数组)tokenizer使用这个来轻松的定义大括号匹配,更多信息详见 @brackets  bracket 部分。每个方括号定义都是一个由3个元素或对象组成的数组,描述了open左大括号close右大括号token令牌类。默认定义如下:

[ ['{','}','delimiter.curly'],
['[',']','delimiter.square'],
['(',')','delimiter.parenthesis'],
['<','>','delimiter.angle'] ]

tokenizer

tokenizer 属性描述了如何进行词法分析,以及如何将输入转换成 token ,每个 token 都会被赋予一个 css 类名,用于在编辑器中渲染,内置的 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]

当然也可以自定义 css token,通过以下方式将自定义的 css token 注入。

editor.defineTheme("vs", {
  base: "vs",
  inherit: true,
  rules: [
    {
      token: "token-name",
      foreground: "#117700",
    }
  ],
  colors: {},
});

一个 tokenizer 由一个描述状态的对象组成。tokenizer 的初始状态由 tokenizer 定义的第一个状态决定。这句话什么意思呢?查看下方例子,root就是 tokenizer 定义的第一个状态,就是初始状态。同理,如果把 afterIf root 两个状态调换位置,那么 afterIf 就是初始状态。

monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
        root: [
            // 初始状态的规则
            [/\d+/, 'number'], // 识别数字
            [/\w+/, 'keyword'], // 识别关键字
            // 转移到下一个状态
            [/^if$/, { token: 'keyword', next: 'afterIf' }],
        ],
        afterIf: [
            // 处理 if 语句后的内容
            [/\s+/, ''], // 忽略空白
            [/[\w]+/, 'identifier'], // 识别标识符
            // 返回初始状态
            [/;$/, { token: '', next: 'root' }],
        ]
    }
});

如何获取 tokenizer 定义的第一个状态呢?

class MonarchTokenizer {
  ...
  public getInitialState(): languages.IState {
    const rootState = MonarchStackElementFactory.create(null, this._lexer.start!);
    return MonarchLineStateFactory.create(rootState, null);
  }
  ...
}

通过 getInitialState 获取初始的一个状态,通过代码可以看到 确认哪个是初始状态是通过 this._lexer.start 这个属性。这个属性又是怎么被赋值的呢?

function compile() {
  ...
  for (const key in json.tokenizer) {
    if (json.tokenizer.hasOwnProperty(key)) {
      if (!lexer.start) {
        lexer.start = key;
      }
  
      const rules = json.tokenizer[key];
      lexer.tokenizer[key] = new Array();
      addRules('tokenizer.' + key, lexer.tokenizer[key], rules);
    }
}
  ...
}

compile 解析 setMonarchTokensProvider 传入的语言定义对象时,会将读取出来的第一个 key 作为初始状态。可能会有疑问,就一定能保证在定义对象时,写入的第一个属性,在读取时一定第一个被读出吗?

JavaScript 中,对象属性的顺序有一些特定的规则:

1. 整数键:如果属性名是一个整数(如 "1""2" 等),这些属性会按照数值的升序排列。

2. 字符串键:对于非整数的字符串键,属性的顺序是按照它们被添加到对象中的顺序。

3. Symbol 键:如果属性的键是 Symbol 类型,这些属性会按照它们被添加到对象中的顺序。

因此,当使用 for...in 循环遍历对象的属性时,属性的顺序如下:

· 首先是所有整数键,按升序排列。

· 然后是所有字符串键,按添加顺序排列。

· 最后是所有 Symbol 键,按添加顺序排列。

看个例子:

http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/ca47664afd7dc8dc7c6cf84ee6c617be..png

上述例子可以看出,12虽然被写在了后面,但仍然会被排序优先输出,其后才是字符串键根据添加顺序输出。所以,尽可能不要使用整数键去定义状态名。

tokenizer 处于某种状态时,只有那个状态的规则才能匹配。所有规则是按顺序进行匹配的,当匹配到第一个规则时,它的 action 将被用来确定 token 的类型。不会再使用后面的规则进行尝试,因此,以一种最有效的方式排列规则是很重要的。比如空格和标识符优先。

如何定义一个状态?

每个状态定义为一个用于匹配输入的规则数组,规则可以有如下形式:

· [regex, action]{regex: regex, action: action}形式的简写。

· [regex, action, next] { regex: regex, action: action{ next: next} }形式的简写。

monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
        root: [
            // [regex, action]
            [/\d+/, 'number'],
            /**
             * [regex, action, next]
             * [/\w+/, { token: 'keyword', next: '@pop' }] 的简写
             */
            [/\w+/, 'keyword', '@pop'],
        ]
    }
});

regex 是正则表达式,action 分为以下几种:

· string{ token: string } 的简写

· [action, ..., actionN]多个 action 组成的数组。这仅在正则表达式恰好由 N 个组(即括号部分)组成时才允许。举个例子:

[/(\d)(\d)(\d)/, ['string', 'string', 'string']

· { token: tokenClass }这个 tokenClass 可以是内置的 css token,也可以是自定义的 token。同时,还规定了一些特殊的 token 类:

o "@rematch"备份输入并重新调用 tokenizer 。这只在状态发生变化时才有效(或者我们进入了无限的递归),所以这个通常和 next 属性一起使用。例如,当你处于特定的 tokenizer 状态,并想要在看到某些结束标记时退出,但是不想在处于该状态时使用它们,就可以使用这个。例如:

monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
        root: [
            [/\d+/, 'number', 'word'],
        ],
        word: [
            [/\d/, '@rematch', '@pop'],
            [/[^\d]+/, 'string']
        ]
    }
});

这个 language 的状态流转图是怎么样的呢?

http://dtstack-static.oss-cn-hangzhou.aliyuncs.com/2021bbs/files_user1/article/554e29f722a2ea1fcb32ad07ce9253fa..png

 

可以看出,在定义一个状态时,应保证状态存在出口即没有定义转移到其他状态的规则),否则可能会导致死循环,不断的使用状态内的规则去匹配。

o "@pop"弹出 tokenizer 栈以返回到之前的状态。

o "@push"推入当前状态,并在当前状态中继续。

monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
    root: [
      // 当匹配到开始标记时,推送新的状态
      [/^\s*function\b/, { token: 'keyword', next: '@function' }],
    ],
    function: [
      // 在 function 状态下的匹配规则
      [/^\s*{/, { token: 'delimiter.bracket', next: '@push' }],
      [/[^}]+/, 'statement'],
      [/^\s*}/, { token: 'delimiter.bracket', next: '@pop' }],
    ],
  }
});

o $n匹配输入的第n组,或者是$0代表这个匹配的输入。

o $Sn状态的第 n 个部分,比如,状态 @tag.foo,用 $S0 代表整个状态名(即 tag.foo ),$S1 返回 tag,$S2 返回 foo 。

《数据资产管理白皮书》下载地址:https://www.dtstack.com/resources/1073/?src=bbs

《行业指标体系白皮书》下载地址:https://www.dtstack.com/resources/1057/?src=bbs

《数据治理行业实践白皮书》下载地址:https://www.dtstack.com/resources/1001/?src=bbs

《数栈V6.0产品白皮书》下载地址:https://www.dtstack.com/resources/1004/?src=bbs

想了解或咨询更多有关袋鼠云大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=bbs

同时,欢迎对大数据开源项目有兴趣的同学加入「袋鼠云开源框架钉钉技术群」,交流最新开源技术信息,群号码:30537511,项目地址:https://github.com/DTStack

0条评论
社区公告
  • 大数据领域最专业的产品&技术交流社区,专注于探讨与分享大数据领域有趣又火热的信息,专业又专注的数据人园地

最新活动更多
微信扫码获取数字化转型资料
钉钉扫码加入技术交流群