Lezer 语法高亮系统
Lezer 的语法高亮系统分为两部分:
- 将 syntax tree 上的节点根据其类型与相应的 tag 标签进行映射
- 将标签 tag 与相应的 CSS class 类名进行映射
参考
官方给出了一个示例 Highlighting Example 演示完整的高亮配置流程
Tag 高亮信息标记
@lezer/highlight 模块导出了 class Tag,该类的实例作为高亮信息的 marker 标记
说明
在 CodeMirror 里可以将不同的 tag 映射到相应 CSS class 类名以设置高亮样式,这样就实现 Lezer 与 CodeMirror 的兼容(通过 tag 将 Lezer 语法树的信息传递给 CodeMirror)
通过该类的静态方法进行实例化:
- static
Tag.define(name?, parent?)静态方法(也可以省略第一个参数采用Tag.define(parent)) - static
Tag.defineModifier(name?)静态方法
@lezer/highlight 模块导出了一个 tags 变量(它是一个对象),是基于编程语言常见的语义所预先构建的一系列的 class Tag 实例,一般使用这些内置的高亮标签即可覆盖大部分场景
提示
不必将不同的节点类型映射为不同的 tag 标签,这可能会导致 tag 类型过细琐
对于语义相近的节点,可以映射为同一种 tag 高亮标签
@lezer/highlight 模块导出了一个方法 styleTags(spec) 可以很方便地将 node 与 tag 的映射关系添加到解析器上
styleTags(spec: Object<Tag | readonly Tag[]>) → NodePropSource
参数 spec 是一个对象,以键值对的形式描述了节点 node 与高亮标签 tag 之间的映射关系,其映射关系可以有多种:
- 一对一:键名是一个 node 节点名称,值是一个 class
Tag实例 - 多对一:键名是一个 node 节点名称拼接而成的字符串,之间用空格分隔
- 一对多:值是一个数组,每个元素都是一个 class
Tag实例 - 多对多
映射格式
在参数 spec 对象的属性名中,支持使用一些特殊的符号来表示复杂的映射关系
/分隔符:表示所需映射的节点需要满足的路径,例如"Block/Declaration/VariableName",表示所需映射的节点是VariableName,而且它的父节点需要是Declaration节点,它的祖父节点需要是Block节点*通配符:如果在节点的路径中出现*通配符,表示祖先节点可以是任何类型的(但是只能表示匹配一个节点)- 节点路径以
/...结尾,表示除了将 tag 分配给当前节点(该路径最后具名的节点)的同时,也会将该 tag 分配给它的所有子节点 - 节点路径以
!结尾,例如Attribute!表示该节点的子节点不会再进行匹配,当前节点会作为一个整个
将该方法的返回值是一个函数(符合 type NodePropSource 类型),用于 NodeSet.extend 或 LRParser.configure 方法中,为对应的节点添加 node prop;或者通过指令 @external propSource 将外部定义的 node prop source 直接引入 .grammar 文件中,这样 Lezer 会自动将其整合进解析器中,为相应的节点添加 node prop
parser.configure({props: [
styleTags({
// Style Number and BigNumber nodes
"Number BigNumber": tags.number,
// Style Escape nodes whose parent is String
"String/Escape": tags.escape,
// Style anything inside Attributes nodes
"Attributes!": tags.meta,
// Add a style to all content inside Italic nodes
"Italic/...": tags.emphasis,
// Style InvalidString nodes as both `string` and `invalid`
"InvalidString": [tags.string, tags.invalid],
// Style the node named "/" as punctuation
'"/"': tags.punctuation
})
]})
该模块还导出一个方法 getStyleTags(node) 用于查询语法树上特定节点 node(该参数符合 TypeScript interface SyntaxNodeRef 类型)是否打上了 tag 高亮信息标记,返回值有两种可能:
- 如果该节点有高亮标记,则返回一个对象
{tags: readonly Tag[], opaque: boolean, inherit: boolean}其中属性opaque表示该高亮映射规则中,节点路径是否以!结尾(将该节点视作整体);属性inherit表示该高亮映射规则中,节点路径是否以/...结尾(将这些 tag 高亮标记分配给该节点的所有子节点) - 如果该节点没有对应的高亮标记,则返回
null
Highlighter 高亮器
高亮器(一个对象,符合 interface Highlighter 类型)用于将标签 tag 映射到相应的 CSS class 类名
Highlighter
TypeScript interface Highlighter 具有以下属性:
style(tags)方法:将给定的tags数组(其元素是一个个 classTag实例,作为高亮信息的 marker 标记)映射到为 CSS class 类名
返回值可能是一个字符串,表示对应的 CSS class 类名;也可能是null,表示这些高亮标记没有对应的 CSS 类scope属性:用于设置一个函数fn(node: NodeType) → boolean以判断该高亮器是否仅应用于给定的节点(及其后代节点),即该高亮器的作用域是针对语法树的局部的
一般通过方法 tagHighlighter 构建一个 Highlighter 高亮器
@lezer/highlight 模块导出了一个变量 classHighlighter,它是一个预先构建的 Highlight 高亮器,将一系列标签 tag 映射至对应带有 tok- 前缀的 CSS class 类名(例如 comment tag 会映射为 tok-comment 类名),此外还对于一些特别的标签 tag 映射了多个 CSS class 类名
另外该模块还导出两个比较底层的方法,它们会遍历语法树的节点,获取到它们的高亮信息,一般用于手动拼接出高亮代码文本(例如在服务端渲染的场景)
highlightCode方法jshighlightCode( // 需要高亮的代码文本 code: string, // 解析得到的语法树 tree: Tree, // 高亮器 highlighter: Highlighter | readonly Highlighter[], // 遍历每一个文本片段(即叶子节点所对应的内容),并执行以下回调函数 // 该函数接收两个参数: // * 当前所遍历的文本内容 code // * 该文本片段(所对应节点具有的高亮标记)对应的 classes 类名,如果没有高亮标记则对应的是空字符串 putText: fn(code: string, classes: string), // 遍历每一个换行符,并执行以下回调函数 putBreak: fn(), // 可选参数 from 和 to 用于设置所需遍历的文本范围(基于字符偏移量) from?: number = 0, to?: number = code.length )highlightTree方法jshighlightTree( // 所需遍历的节点树 tree: Tree, // 高亮器 highlighter: Highlighter | readonly Highlighter[], // 遍历每个具有(具有的高亮标记所对应)classes 类名的节点,并执行以下回调函数 // 该函数接收三个参数: // * from 和 to 是当前遍历节点的范围 // * classes 是该节点所具有的 classes 类名 putStyle: fn(from: number, to: number, classes: string), // 可选参数 from 和 to 用于设置所需遍历的范围(基于字符偏移量) from?: number = 0, to?: number = tree.length )
区别
方法 highlightCode 封装程度更高、更易用的,在遍历时直接获取文本流,一般场景使用它即可
/**
* 用 highlightCode 逐段输出高亮文本
*/
import {highlightCode} from "@lezer/highlight";
const code = "42 + 'hi'\n123";
// 假设 parser 是一个已存在的解析器
const tree = parser.parse(code);
// 用于收集拼接的带有高亮样式的 HTML 字符串
let html = "";
highlightCode(
code,
tree,
// 假设 highlighter 是一个已存在的高亮器
highlighter,
// 每遇到一段具有高亮信息的文本,就用 span 包一层
(text, classes) => {
if (classes) {
html += `<span class="${classes}">${text}</span>`;
} else {
html += text;
}
},
// 每遇到换行符,就执行以下回调函数
() => {
html += "\n";
}
);
console.log(html);
/* 输出:
<span class="num">42</span> + <span class="str">'hi'</span>
<span class="num">123</span>
*/
如果需要自由程度更高,则可以使用更底层的方法 highlightTree,在遍历时获取到所遍历节点的位置区间信息,可以基于此继续访问多更多节点信息
/**
* 用 highlightTree 收集应用高亮样式的区间范围
*/
import {highlightTree} from "@lezer/highlight";
const code = "42 + 'hi'";
// 假设 parser 是一个已存在的解析器
const tree = parser.parse(code);
// 用来存放结果
const fragments = [];
// 调用 highlightTree,遍历整棵树,收集每个有样式的区间
highlightTree(
tree,
// 假设 highlighter 是一个已存在的高亮器
highlighter,
(from, to, classes) => {
fragments.push({ from, to, classes });
}
);
// 看看我们得到了什么
console.log(fragments);
/* 可能输出:
[
{ from: 0, to: 2, classes: "num" }, // "42"
{ from: 5, to: 8, classes: "str" } // "'hi'"
]
*/