Lezer 语法高亮系统


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 的映射关系添加到解析器上

ts
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.extendLRParser.configure 方法中,为对应的节点添加 node prop;或者通过指令 @external propSource 将外部定义的 node prop source 直接引入 .grammar 文件中,这样 Lezer 会自动将其整合进解析器中,为相应的节点添加 node prop

js
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 数组(其元素是一个个 class Tag 实例,作为高亮信息的 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 方法
    js
    highlightCode(
      // 需要高亮的代码文本
      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 方法
    js
    highlightTree(
      // 所需遍历的节点树
      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 封装程度更高、更易用的,在遍历时直接获取文本流,一般场景使用它即可

js
/**
 * 用 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,在遍历时获取到所遍历节点的位置区间信息,可以基于此继续访问多更多节点信息

js
/**
 * 用 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'"
]
*/

Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes