zh CN API 参考 - chiba233/yumeDSL GitHub Wiki
API 参考
四个核心函数,做四件事:
DSL 源文本
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
parseRichText stripRichText parseStructural
→ TextToken[] → 纯文本 string → StructuralNode[]
(渲染用) (搜索/预览用) (高亮/lint/编辑器用)
│
▼
printStructural
→ DSL 源文本(无损往返)
推荐用法: 用 createParser 绑定一次配置,然后到处调 .parse() / .strip() / .structural() / .print()。
createParser(defaults) -- 推荐入口
createParser 将你的 ParseOptions 绑定到一个可复用的实例中。这是推荐的使用方式——只需定义一次标签处理器,然后在任何地方调用
dsl.parse() / dsl.strip() / dsl.structural() / dsl.print(),无需重复传递配置。
import {
createParser,
createSimpleInlineHandlers,
parsePipeArgs,
} from "yume-dsl-rich-text";
const dsl = createParser({
handlers: {
...createSimpleInlineHandlers(["bold", "italic", "underline"]),
link: {
inline: (tokens, ctx) => {
const args = parsePipeArgs(tokens, ctx);
return {
type: "link",
url: args.text(0),
value: args.materializedTailTokens(1),
};
},
},
},
});
// 到处使用——处理器已经绑定好了
dsl.parse("Hello $$bold(world)$$!");
dsl.strip("Hello $$bold(world)$$!");
Parser 接口
interface Parser {
parse: (text: string, overrides?: ParseOptions) => TextToken[];
strip: (text: string, overrides?: ParseOptions) => string;
structural: (text: string, overrides?: StructuralParseOptions) => StructuralNode[];
print: (nodes: StructuralNode[], overrides?: PrintOptions) => string;
}
createParser 绑定的内容
大多数情况下你只需要绑定 handlers。其余选项只是为了方便一并设置。
| 选项 | 预绑定后的作用 |
|---|---|
handlers |
你的标签定义——使用 createParser 的主要原因 |
syntax |
自定义语法 token(如果你覆盖了 $$ 前缀等) |
tagName |
自定义标签名字符规则 |
allowForms |
限制可接受的标签形式(默认:所有形式启用) |
implicitInlineShorthand |
启用 tag(...) inline 简写(1.3 起) |
depthLimit |
嵌套限制——很少需要每次调用都改变 |
createId |
自定义 token id 生成器(可按调用覆盖) |
blockTags |
接受 block 级换行规范化的标签 |
onError |
默认错误处理器(仍可按调用覆盖) |
trackPositions |
为所有输出节点附加源码位置(可按调用覆盖) |
方法
| 方法 | 输入 | 输出 | 从默认值继承的内容 |
|---|---|---|---|
parse |
DSL 文本 + overrides? | TextToken[] |
所有 ParseOptions——overrides 对 syntax/tagName 进行一层深合并 |
strip |
DSL 文本 + overrides? | string |
与 parse 相同 |
structural |
DSL 文本 + overrides? | StructuralNode[] |
handlers、allowForms、implicitInlineShorthand、syntax、tagName、depthLimit、trackPositions |
print |
StructuralNode[] + overrides? |
string |
仅 syntax——overrides 与 defaults 深合并。无损序列化器,不进行门控 |
按调用覆盖合并(自 1.0.11 起)
按调用的 overrides 会浅合并到默认值上,但 syntax 和 tagName 还会额外进行一层深合并,因此部分覆盖可以保留其余默认值:
const dsl = createParser({
handlers,
syntax: {tagPrefix: "@@"},
});
// 此覆盖会合并到现有 syntax 中——tagPrefix 保持为 "@@"
dsl.parse(text, {syntax: {tagDivider: ";"}});
// 生效的 syntax: { tagPrefix: "@@", tagDivider: ";", ...来自 DEFAULT_SYNTAX 的其余值 }
在内部,createParser 的合并逻辑如下:
const merge = <T extends ParserBaseOptions>(overrides: T): ParseOptions & T => {
const merged = {...defaults, ...overrides};
if (defaults.syntax && overrides.syntax) {
merged.syntax = {...defaults.syntax, ...overrides.syntax};
}
if (defaults.tagName && overrides.tagName) {
merged.tagName = {...defaults.tagName, ...overrides.tagName};
}
return merged;
};
注意:一层深合并仅在 defaults 和 overrides 都包含该字段时才会发生。如果只有 override 包含 syntax,则直接使用(不与默认值合并)。
使用与不使用 createParser 的对比
// 不使用 createParser——重复传参,每次都必须传 handlers
parseRichText(text1, {handlers});
parseRichText(text2, {handlers});
stripRichText(text3, {handlers});
parseStructural(text4, {handlers});
// 使用 createParser——绑定一次,到处使用
const dsl = createParser({handlers});
dsl.parse(text1);
dsl.parse(text2);
dsl.strip(text3);
dsl.structural(text4);
dsl.print(tree);
parseRichText(text, options?)
function parseRichText(text: string, options?: ParseOptions): TextToken[];
核心解析函数。接受 DSL 源文本并返回 TextToken[] 树。未注册或格式错误的标签会降级为纯文本——永远不会抛异常。
参数
| 参数 | 类型 | 描述 |
|---|---|---|
text |
string |
DSL 源文本。如果为空,立即返回 []。 |
options |
ParseOptions |
可选。标签处理器、语法配置、错误回调及其他设置。 |
ParseOptions 字段
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
handlers |
Record<string, TagHandler> |
{} |
标签处理器映射。键为标签名,值定义每个标签的解析方式。 |
allowForms |
readonly TagForm[] |
["inline","raw","block"] |
限制可接受的标签形式。未列出的形式会优雅降级。 |
depthLimit |
number |
50 |
最大嵌套深度。超过此限制的标签降级为纯文本。 |
syntax |
Partial<SyntaxInput> |
DEFAULT_SYNTAX |
覆盖 DSL 语法 token。 |
tagName |
Partial<TagNameConfig> |
DEFAULT_TAG_NAME |
覆盖标签名字符规则。 |
createId |
CreateId |
() => "rt-${seed++}" |
Token id 生成器。默认为解析本地计数器:rt-0、rt-1、... |
blockTags |
readonly BlockTagInput[] |
(从 handlers 派生) | 接受 block 级换行规范化的标签。默认为每个具有 raw/block handler 的标签。每个条目可以是普通标签名(同时用于 raw 和 block 形式)或 { tag, forms } 以限制到特定多行形式。 |
onError |
(error: ParseError) => void |
(静默) | 每遇到一个解析错误就会被调用。如果省略,错误将被静默丢弃。 |
trackPositions |
boolean |
false |
为每个 TextToken 附加 position: SourceSpan。 |
baseOffset |
number |
0 |
添加到所有源码位置的基础偏移量。在解析更大文档的子字符串时使用。 |
tracker |
PositionTracker |
(无) | 从原始完整文档预构建的位置追踪器。使用 buildPositionTracker(text) 构建。 |
mode |
"render" |
"render" |
解析模式。目前仅支持 "render"。 |
implicitInlineShorthand |
boolean | readonly string[] |
false |
启用 tag(...) inline 简写。true = 所有 handler,string[] = 白名单。1.3 起。详见 ParseOptions。 |
详见 ParseOptions 选项。
返回值
TextToken[]——一个 token 对象数组:
interface TextToken {
type: string; // 纯文本为 "text",否则为 handler 定义的类型
value: string | TextToken[]; // 纯字符串或嵌套子节点
id: string; // 顺序 id(默认)或通过 createId 自定义
position?: SourceSpan; // 当 trackPositions 为 true 时存在
[key: string]: unknown; // handler 定义的额外字段(url、lang 等)
}
何时使用 parseRichText vs createParser
在复用同一组 handler 的应用代码中使用 createParser。在以下场景直接使用 parseRichText:
- 一次性工具脚本
- 需要对每次调用完全控制所有选项时
- 测试和原型开发
边界情况
- 空字符串:立即返回
[](无分配、无副作用)。 - 无 handlers:所有类标签语法降级为纯文本。
- 未闭合标签:降级为纯文本,并通过
onError回调报告(如果提供了的话)(错误码:INLINE_NOT_CLOSED、SHORTHAND_NOT_CLOSED、BLOCK_NOT_CLOSED、RAW_NOT_CLOSED)。周围内容不会被破坏。 - 格式错误的闭合标记:通过
BLOCK_CLOSE_MALFORMED或RAW_CLOSE_MALFORMED报告,并优雅降级。 - 意外的闭合标记:没有匹配打开标记的闭合标记通过
UNEXPECTED_CLOSE报告,并被视为纯文本。 - 超出深度限制:问题标签降级为纯文本,错误码为
DEPTH_LIMIT。
示例
import {parseRichText, createSimpleInlineHandlers} from "yume-dsl-rich-text";
const tokens = parseRichText("Hello $$bold(world)$$!", {
handlers: createSimpleInlineHandlers(["bold"]),
});
// [
// { type: "text", value: "Hello ", id: "rt-0" },
// { type: "bold", value: [{ type: "text", value: "world", id: "rt-1" }], id: "rt-2" },
// { type: "text", value: "!", id: "rt-3" },
// ]
stripRichText(text, options?)
function stripRichText(text: string, options?: ParseOptions): string;
解析 DSL 文本并仅提取纯文本内容,丢弃所有标签结构。
参数
与 parseRichText 相同。接受相同的 ParseOptions。
返回值
一个去除了所有标签标记的纯 string。仅保留 token 的文本内容。
实现细节
内部调用 parseRichText(text, options) 然后 extractText(tokens)。开销与 parseRichText 相同——没有更廉价的"仅剥离"
代码路径。如果你同时需要 token 和纯文本,请调用一次 parseRichText 然后对结果调用 extractText,以避免解析两次。
示例
import {stripRichText, createSimpleInlineHandlers} from "yume-dsl-rich-text";
stripRichText("Hello $$bold(world)$$!", {
handlers: createSimpleInlineHandlers(["bold"]),
});
// "Hello world!"
stripRichText("");
// ""
边界情况
- 空字符串:立即返回
""。 - 降级行为继承自
parseRichText,因此会影响 strip 输出:- 未注册 inline 标签:strip 输出内部内容(定界符去掉)
- 未注册 raw/block 标签:strip 输出整段原始标记(包括定界符)
- 不支持的形式或 allowForms 限制:strip 输出整段原始标记
使用场景
- 提取可搜索的纯文本用于索引
- 生成文本预览或摘要
- 构建无障碍标签(如
aria-label) - 忽略标记的字符/词计数
parseStructural(text, options?) -- 结构化解析
function parseStructural(text: string, options?: StructuralParseOptions): StructuralNode[];
面向结构化消费者——语法高亮、代码检查、编辑器、源码检视,或任何需要知道使用了哪种标签形式 而非仅语义结果的场景。在输出树中显式保留标签形式(inline / raw / block)。
它与 parseRichText 共享相同的语言配置(handlers、allowForms、syntax、tagName、depthLimit、trackPositions
),因此你无需维护两套独立的 DSL 规则。
参数
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
text |
string |
-- | DSL 源文本。空字符串返回 []。 |
options.handlers |
Record<string, TagHandler> |
(无) | 标签识别和形式门控。省略则接受所有语法上有效的标签/形式,不进行语义门控。 |
options.allowForms |
readonly TagForm[] |
(所有形式) | 限制可接受的形式(需要 handlers)。 |
options.depthLimit |
number |
50 |
最大嵌套深度。 |
options.syntax |
Partial<SyntaxInput> |
DEFAULT_SYNTAX |
覆盖语法 token。 |
options.tagName |
Partial<TagNameConfig> |
DEFAULT_TAG_NAME |
覆盖标签名字符规则。 |
options.trackPositions |
boolean |
false |
为每个 StructuralNode 附加 position: SourceSpan。 |
options.baseOffset |
number |
0 |
位置追踪的基础偏移量。 |
options.tracker |
PositionTracker |
(无) | 从原始文档预构建的位置追踪器。 |
options.implicitInlineShorthand |
boolean | readonly string[] |
false |
启用 tag(...) inline 简写。true = 所有 handler,string[] = 白名单。1.3 起。 |
StructuralParseOptions
interface StructuralParseOptions extends ParserBaseOptions {
trackPositions?: boolean;
}
interface ParserBaseOptions {
handlers?: Record<string, TagHandler>;
allowForms?: readonly TagForm[];
implicitInlineShorthand?: InlineShorthandOption;
depthLimit?: number;
syntax?: Partial<SyntaxInput>;
tagName?: Partial<TagNameConfig>;
baseOffset?: number;
tracker?: PositionTracker;
}
注意:StructuralParseOptions 不包含 createId、blockTags、mode 或 onError——这些是仅属于渲染管线的
ParseOptions 字段。
StructuralNode 变体
| 类型 | 字段 | 描述 |
|---|---|---|
text |
value: string |
纯文本 |
escape |
raw: string |
转义序列(如 \)) |
separator |
-- | 管道 ` |
inline |
tag: string, children: StructuralNode[], implicitInlineShorthand?: boolean |
$$tag(...)$$ 或 tag(...) 简写 |
raw |
tag: string, args: StructuralNode[], content: string |
$$tag(...)% ... %end$$ |
block |
tag: string, args: StructuralNode[], children: StructuralNode[] |
$$tag(...)* ... *end$$ |
当启用 trackPositions 时,所有变体都携带可选的 position?: SourceSpan。
示例
import {parseStructural} from "yume-dsl-rich-text";
const tree = parseStructural("$$bold(hello)$$ and $$code(ts)%\nconst x = 1;\n%end$$");
// [
// { type: "inline", tag: "bold", children: [{ type: "text", value: "hello" }] },
// { type: "text", value: " and " },
// { type: "raw", tag: "code",
// args: [{ type: "text", value: "ts" }],
// content: "\nconst x = 1;\n" },
// ]
Handler 门控行为
当提供了 handlers 时: 门控行为与 parseRichText 相同。使用相同的 supportsInlineForm 决策表和
filterHandlersByForms 逻辑(共享代码在 resolveOptions.ts 中,非镜像)。Handler 函数本身永远不会被调用——只有 handler
对象上 inline / raw / block 方法的存在与否才会影响支持哪些形式。
当省略 handlers 时: 接受所有语法上有效的、所有形式的标签。这是语法高亮和代码检查工具的典型模式,它们需要看到完整的源码结构而无需语义过滤。
与 parseRichText 的区别
这些是特性,不是 bug——两个解析器服务于不同的受众。
| 方面 | parseRichText |
parseStructural |
|---|---|---|
| 标签识别 | 相同(共享 ParserBaseOptions) |
相同(共享 ParserBaseOptions) |
| 形式门控 | 相同 | 相同 |
| 换行规范化 | 始终剥离(渲染模式) | 始终保留 |
管道 | |
作为文本的一部分(由 handler 通过 parsePipeArgs 消费) |
参数中为 separator 节点;其他位置为文本 |
| 错误报告 | onError 回调 |
静默降级 |
| 转义处理 | 在根级别取消转义(产生字面文本) | 结构化 escape 节点(保留原始序列) |
| 位置追踪 | TextToken.position 上的 trackPositions(规范化范围) |
StructuralNode.position 上的 trackPositions(原始语法范围) |
| 输出类型 | TextToken[] |
StructuralNode[] |
我应该使用哪个?
- 如果你的目标是渲染内容(Vue 组件、HTML、终端输出),使用
parseRichText。 - 如果你的目标是分析源码结构(语法高亮、代码检查、编辑器集成、源码检视),使用
parseStructural。
已弃用的环境状态警告
已弃用: 当在
withSyntax()/withTagNameConfig()包装器内调用且没有显式options.syntax/options.tagName时,parseStructural仍会读取环境状态作为回退,但此路径已弃用并会发出console.warn。请改为显式传递options.syntax/options.tagName。此环境回退将在未来的主版本中移除。
printStructural(nodes, options?) -- 结构化打印
function printStructural(nodes: StructuralNode[], options?: PrintOptions): string;
parseStructural 的逆操作——将 StructuralNode[] 树序列化回 DSL 源文本。这是一个无损序列化器,不进行门控或验证。标记了
implicitInlineShorthand: true 的 inline 节点在位于 inline 参数上下文中时会序列化为简写形式(tag(...));其他节点始终使用完整语法。
参数
| 参数 | 类型 | 描述 |
|---|---|---|
nodes |
StructuralNode[] |
要序列化的结构化树。 |
options.syntax |
Partial<SyntaxInput> |
覆盖语法 token。必须与 parseStructural 使用的语法一致。 |
PrintOptions
interface PrintOptions {
syntax?: Partial<SyntaxInput>;
}
syntax 字段是唯一的选项。内部,printStructural 调用 createSyntax(options?.syntax) 来解析完整的语法配置。
往返示例
当结构化树保留了原始语法相关信息且使用了相同的语法配置时,往返序列化是无损的:
import {parseStructural, printStructural} from "yume-dsl-rich-text";
const input = "Hello $$bold(world)$$!";
const tree = parseStructural(input);
printStructural(tree); // "Hello $$bold(world)$$!"
程序化构建树
你可以程序化地构建 StructuralNode[] 树并将其序列化:
import type {StructuralNode} from "yume-dsl-rich-text";
import {printStructural} from "yume-dsl-rich-text";
const tree: StructuralNode[] = [
{type: "text", value: "Hello "},
{
type: "inline",
tag: "bold",
children: [{type: "text", value: "world"}],
},
];
printStructural(tree); // "Hello $$bold(world)$$"
构建 raw 节点:
const rawTree: StructuralNode[] = [
{
type: "raw",
tag: "code",
args: [{type: "text", value: "ts"}],
content: "\nconst x = 1;\n",
},
];
printStructural(rawTree); // "$$code(ts)%\nconst x = 1;\n%end$$"
构建带管道分隔参数的 block 节点:
const blockTree: StructuralNode[] = [
{
type: "block",
tag: "info",
args: [
{type: "text", value: "title"},
{type: "separator"},
{type: "text", value: "subtitle"},
],
children: [{type: "text", value: "Block content here"}],
},
];
printStructural(blockTree); // "$$info(title|subtitle)*Block content here*end$$"
简写感知序列化(1.3 起)
当 parseStructural 启用了 implicitInlineShorthand 时,简写语法产生的 inline 节点会携带
implicitInlineShorthand: true。printStructural 在该节点位于其他 inline 标签的参数内时会尊重此标记,
输出 tag(...) 而不是 $$tag(...)$$:
import {parseStructural, printStructural, createSimpleInlineHandlers} from "yume-dsl-rich-text";
const source = "$$bold(bold(x))$$";
const tree = parseStructural(source, {
handlers: createSimpleInlineHandlers(["bold"]),
implicitInlineShorthand: true,
});
printStructural(tree); // "$$bold(bold(x))$$" — 内层 `bold(x)` 保持简写形式
在顶层(不在任何 inline 参数内),即使节点标记了 shorthand,仍会序列化为完整语法,以确保输出在任何消费者配置下都是合法 DSL。
createParser 集成
parser.print(nodes) 从解析器的默认闭包中继承 syntax。
按调用的 overrides 会与 defaults 深合并,与 parse、structural 行为一致:
import {createParser, createSimpleInlineHandlers} from "yume-dsl-rich-text";
const dsl = createParser({
syntax: {tagPrefix: "@@", tagOpen: "[", tagClose: "]", endTag: "]@@"},
handlers: createSimpleInlineHandlers(["bold"]),
});
const tree = dsl.structural("@@bold[hello]@@");
dsl.print(tree); // "@@bold[hello]@@" -- syntax 从 createParser 默认值继承
// 按调用覆盖:与 defaults 深合并
dsl.print(tree, {syntax: {tagPrefix: "%%", endTag: "]%%"}});
// "%%bold[hello]%%" -- tagPrefix/endTag 被覆盖,tagOpen/tagClose 保留 defaults
这意味着通过 createParser 实例进行往返时,你不需要重新指定 syntax。
需要为单次 print 使用不同 syntax 时,作为 override 传入即可——defaults 不会被修改。
内部行为
打印器根据每个节点的 type 进行分支:
text:原样追加node.value。escape:原样追加node.raw(保留转义序列)。separator:追加syntax.tagDivider。inline:追加tagPrefix + tag + tagOpen + (递归 children) + endTag。raw:追加tagPrefix + tag + tagOpen + (递归 args) + rawOpen + content + rawClose。content字段原样输出,不进行递归处理。block:追加tagPrefix + tag + tagOpen + (递归 args) + blockOpen + (递归 children) + blockClose。
打印时忽略位置信息(position 字段)。
如果树中包含运行时解析器不支持其形式的节点,它们仍会以完整语法打印,重新解析时会自然降级为纯文本。这是有意为之的——打印器是序列化器,不是验证器。
buildZones(nodes) -- zone 分组
function buildZones(nodes: readonly StructuralNode[]): Zone[];
将顶层 StructuralNode[] 分组为连续的 Zone[],用于 zone 级缓存或批处理。
需要 trackPositions: true 解析的节点。若首个节点缺少 position(通常是忘记开启 trackPositions),会抛出错误。空输入返回
[],不会报错。
规则
- 相邻的 text / escape / separator / inline 节点合并为一个 zone
- 每个 raw 或 block 节点独占一个 zone
- 每个 raw / block 节点之后开始新 zone
Zone
interface Zone {
startOffset: number; // 源码偏移(包含)
endOffset: number; // 源码偏移(不包含)
nodes: StructuralNode[];
}
示例
import {createParser, createSimpleInlineHandlers, createSimpleRawHandlers, buildZones} from "yume-dsl-rich-text";
const parser = createParser({
handlers: {
...createSimpleInlineHandlers(["bold"]),
...createSimpleRawHandlers(["code"]),
},
});
const tree = parser.structural(
"hello $$bold(world)$$\n$$code(ts)%\nlet x = 1;\n%end$$\nend",
{trackPositions: true},
);
const zones = buildZones(tree);
// zones[0]: { startOffset: 0, endOffset: 22, nodes: [text, inline:bold] }
// zones[1]: { startOffset: 22, endOffset: 49, nodes: [raw:code] }
// zones[2]: { startOffset: 49, endOffset: 53, nodes: [text] }
使用场景:zone 级渲染缓存
在编辑器中,可以按 zone 缓存渲染结果,只重新渲染与编辑范围重叠的 zone:
const tree = parser.structural(source, {trackPositions: true});
const zones = buildZones(tree);
for (const zone of zones) {
const key = source.slice(zone.startOffset, zone.endOffset);
if (cache.has(key)) continue; // 复用缓存
const tokens = parseSlice(source, zone.nodes[0].position, parser, tracker);
cache.set(key, renderTokens(tokens));
}
增量解析:parseIncremental / createIncrementalSession
面向编辑器工作流的 structural 增量缓存 API。它会在多次编辑之间维护更新后的 StructuralNode[] + Zone[] 快照,
避免每次都对整篇文档做全量 structural 扫描。
完整说明与示例见:增量解析。
parseIncremental(source, options?)
function parseIncremental(
source: string,
options?: IncrementalParseOptions,
): IncrementalDocument;
构建快照:内部强制开启 trackPositions: true,并执行 buildZones(...)。
createIncrementalSession(source, options?, sessionOptions?)
function createIncrementalSession(
source: string,
options?: IncrementalParseOptions,
sessionOptions?: IncrementalSessionOptions,
): IncrementalSession;
创建会话级增量控制器(getDocument / applyEdit / rebuild),编辑时会自动处理全量回退。
性能:printStructural、buildZones、walkTokens、mapTokens
在 ~200 KB 文档上测量(126,270 字符,12,161 结构节点,9,042 渲染 token)。
环境:鲲鹏 920 aarch64 / Node v24.14.0(1.1.6)——5 轮 x 5 次迭代取中位数。
| 操作 | 耗时 | 备注 |
|---|---|---|
| printStructural(12,161 节点) | 2.0 ms | 无损 round-trip 已验证 |
| buildZones(→ 1,873 zones) | 0.87 ms | 从 6,235 个顶层结构节点分组 |
| walkTokens(访问 9,042 个) | 0.39 ms | 对 9,042 个渲染 token 先序 DFS |
| mapTokens 恒等 | 0.86 ms | 返回相同的树,除包装外无额外分配 |
| mapTokens 变换 | 1.2 ms | 将整棵树中的 bold 改名为 strong |
五个操作在 200 KB 文档上均低于 4ms。在编辑器管线中 parseStructural(1.1.6 ~24.6 ms)是瓶颈,printStructural /
buildZones /
walkTokens 相当于免费。
parser 本体的性能基准集中放在性能:
正常 ~200 KB 文档基准、parseSlice 增量解析,以及 5000 层深链 stress case 都在那里统一维护。
parseRichText 和 parseStructural 基准见性能。
createEasyStableId 基准见稳定 Token ID——性能。
unescapeInline / splitTokensByPipe 基准见处理器工具函数——性能。