zh CN 处理器工具函数 - chiba233/yumeDSL GitHub Wiki

处理器工具函数

编写标签处理器 | Token 结构

写 handler 时用到的底层工具函数。

createPipeHandlers 的关系: createPipeHandlers(见 处理器辅助函数)内部就是帮你调这里的 parsePipeArgs / parsePipeTextArgs。如果你用了 createPipeHandlers,通常不需要自己调这些函数。 当 createPipeHandlers 的包装不够用(比如你需要在调 parsePipeArgs 之前做预处理,或者你根本不走管道分割),才需要直接用本页的工具。

分两层:日常的够用了,高级的是日常函数的底层积木。


全景图:谁调谁

createPipeHandlers (辅助函数页)
    │ 内部自动调用 ↓
    │
你的 handler 代码(或 createPipeHandlers 内部)
    │
    ├─ parsePipeArgs(tokens, ctx)      ← 把 inline token 按管道拆分
    │      └─ splitTokensByPipe        ← 底层:逐字符扫描、转义处理
    │
    ├─ parsePipeTextArgs(text, ctx)    ← 同上,但输入是字符串(raw/block 的 arg)
    │      └─ createTextToken → parsePipeArgs
    │
    ├─ parsePipeTextList(text, ctx)    ← 管道分割 → 纯字符串数组
    │      └─ parsePipeTextArgs
    │
    ├─ extractText(tokens)             ← 从 token 树提取纯文本
    │
    ├─ createTextToken(value, ctx)     ← 手动造一个文本 token
    │      └─ createToken
    │
    └─ materializeTextTokens(tokens)   ← 递归反转义文本叶子
           └─ unescapeInline
                  └─ readEscaped
                        └─ readEscapedSequence  ← 最底层:字符级转义扫描

快速选择

你想干嘛 用什么 备注
inline handler 里拆管道参数 parsePipeArgs(tokens, ctx) createPipeHandlers 自动帮你调了这个
raw/block handler 里拆 arg 字符串的管道参数 parsePipeTextArgs(arg ?? "", ctx) createPipeHandlers 自动帮你调了这个
只要纯字符串数组,不要 token parsePipeTextList(text, ctx)
从 token 树提纯文本(搜索/展示用) extractText(tokens)
手动造一个文本 token createTextToken(value, ctx)

日常工具函数


parsePipeArgs(tokens, ctx?)

function parsePipeArgs(tokens: TextToken[], ctx?: DslContext): PipeArgs

按管道分隔符拆分 inline token,返回 PipeArgs 视图。你可以用 .text(0) 拿纯文本,用 .materializedTokens(0) 拿反转义后的 token。

如果你用了 createPipeHandlers,这一步已经自动完成——你的回调直接收到 PipeArgs。 手动调 parsePipeArgs 的场景是:你没用 createPipeHandlers,或者你在自定义 handler 里需要更精细的控制。

const linkHandler: TagHandler = {
    inline: (tokens, ctx) => {
        const args = parsePipeArgs(tokens, ctx);
        return {
            type: "link",
            url: args.text(0),
            value: args.materializedTailTokens(1),
        };
    },
};

边界情况: 空 tokens → 一个空段;没有管道 → 整体是一段;转义的 \| 不拆。


parsePipeTextArgs(text, ctx?)

function parsePipeTextArgs(text: string, ctx?: DslContext): PipeArgs

parsePipeArgs 一样,但接受字符串而不是 token 数组。在 raw/block handler 里用——因为 raw/block 的参数是字符串,不是 token。

createPipeHandlersraw / block 回调已经自动调了 parsePipeTextArgs。 原始字符串参数通过 rawArg 第四参数传给你,需要时才用。

const codeHandler: TagHandler = {
    raw: (arg, content, ctx) => {
        const args = parsePipeTextArgs(arg ?? "", ctx);
        return {type: "code", lang: args.text(0, "text"), value: content};
    },
};

parsePipeTextList(text, ctx?)

function parsePipeTextList(text: string, ctx?: DslContext): string[]

管道分割 → 纯字符串数组。不涉及 token,最简单的一种。

parsePipeTextList("ts | Demo | Label");
// → ["ts", "Demo", "Label"]

parsePipeTextList("a \\| b | c");
// → ["a | b", "c"]  — 转义管道不拆

extractText(tokens?)

function extractText(tokens?: TextToken[]): string

递归拼出 token 树里的所有纯文本。不反转义\| 原样输出)、不保留结构(bold 和纯文本贡献同样的字符串)。

const tokens = parseRichText("Hello $$bold(world)$$", {handlers});
extractText(tokens);
// → "Hello world"

要反转义的纯文本?用 unescapeInline(extractText(tokens), ctx)PipeArgs.text()


createTextToken(value, ctx?)

function createTextToken(value: string, ctx?: DslContext): TextToken

手动造一个 { type: "text", value, id } 的文本 token。在 handler 里需要插入分隔符 token 时用。

const separator = createTextToken(" | ", ctx);

高级工具函数

日常函数的底层积木。需要更低级别控制时才用。


splitTokensByPipe(tokens, ctx?)

function splitTokensByPipe(tokens: TextToken[], ctx?: DslContext): TextToken[][]

不带 PipeArgs 包装的原始管道分割。逐字符扫描文本 token,遇到管道就拆。

行为细节:

  • 转义管道 \| 不拆,保留转义形式
  • 管道后面的空白被吃掉("a | b"["a "] + ["b"]
  • 能产生空段("a || b" → 三段)
  • 非文本 token 不扫描,原样归入当前段

大多数情况用 parsePipeArgs 就够了。


materializeTextTokens(tokens, ctx?)

function materializeTextTokens(tokens: TextToken[], ctx?: DslContext): TextToken[]

递归反转义文本叶子节点:\||\)$$)$$,等等。

为什么叫"物化"? 解析过程中,文本 token 故意保留转义序列(这样管道分割才能区分真管道和转义管道)。物化是最终把转义解析成字面值的步骤。

为什么跳过非文本 token? raw-code 里的 \n 是真 JavaScript,不是 DSL 转义。只处理 type === "text" 的 token 才安全。


unescapeInline(str, ctx?)

function unescapeInline(str: string, ctx?: DslContext | SyntaxConfig): string

反转义字符串中所有 DSL 转义序列。从左到右扫描,每次消耗一个"输出单元"。

unescapeInline("hello \\) world");  // → "hello ) world"
unescapeInline("a \\| b \\| c");    // → "a | b | c"
unescapeInline("path\\to\\file");   // → "path\\to\\file"  (t 不是可转义 token,原样保留)

readEscapedSequence(text, i, ctx?)

function readEscapedSequence(text: string, i: number, ctx?): [string | null, number]

字符级转义扫描器。在位置 i 检查是不是转义序列的开头。

  • 找到转义 → [转义的字面值, 序列结束位置]
  • 不是转义 → [null, i](位置不变,调用者自己推进)
readEscapedSequence("hello \\| world", 6);  // → ["|", 8]
readEscapedSequence("hello \\| world", 0);  // → [null, 0]

readEscaped(text, i, ctx?)

function readEscaped(text: string, i: number, ctx?): [string, number]

readEscapedSequence 的带兜底版本:找到转义就返回字面值,没找到就返回当前字符。永远有返回值,适合写扫描循环。

readEscaped("a\\|b", 0);  // → ["a", 1]
readEscaped("a\\|b", 1);  // → ["|", 3]  — 吃掉 \|,输出 |
readEscaped("a\\|b", 3);  // → ["b", 4]

unescapeInline 本质上就是循环调 readEscaped 拼结果。


createToken(draft, position?, ctx?)

function createToken(draft: TokenDraft, position?: SourceSpan, ctx?: DslContext | CreateId): TextToken

TokenDraft 构建最终的 TextToken——分配 ID、附加位置。所有 token 创建最终都经过这个函数。

ID 解析顺序:

  1. ctx 是函数 → 直接当 CreateId 用
  2. ctx 是 DslContext 且有 createId → 用它
  3. 模块级 activeCreateId(已弃用路径)
  4. 兜底:顺序计数器 rt-0, rt-1, ...

新代码请传 DslContext,别传裸函数。


性能

测量环境:鲲鹏 920 aarch64 / Node v24.14.0——5000 次迭代取平均。

unescapeInline(自 1.1.0)

重写为批量 slice() 非转义区间,代替逐字符 readEscaped() + +=。没有转义序列时直接返回原字符串(零分配快速路径)。

场景 输入大小 1.0.x 1.1.0 提升
无转义(最常见) 4950 字符 0.164 ms 0.054 ms 3.0x
大量转义(\(\)|\\ 2500 字符 0.151 ms 0.127 ms 1.2x

"无转义"是实际使用中的主要路径——大部分文本内容不包含 DSL 转义序列。3x 加速来自消除了 ~5000 次单独的 readEscaped 调用(每次都做一个 text.slice(i, i+1) 单字符分配),替换为检测到无 escapeChar 后直接 return str

splitTokensByPipe(自 1.1.0)

重写为追踪 run 起始位置,代替逐字符 buffer += val[i]。一次性 slice 整段非 divider/非转义区间。

场景 输入大小 1.0.x 1.1.0 提升
50 个管道分段 587 字符 0.068 ms 0.051 ms 1.3x

extractText

测试了 string[] + join("") 作为递归 += 的替代方案。基准测试显示 V8 的 ConsString 优化在典型 token 树规模(300 token)下更快。与 1.0.x 行为一致——保留了原始实现。

这对你的应用意味着什么

这些工具函数在 handler 代码内部调用——unescapeInlineparsePipeArgsmaterializeTextTokensparsePipeTextList 中运行。如果你的 DSL 文档有 1000 个标签且每个都有管道参数,3x 的 unescapeInline 提升在 ~50 KB 文档上节省约 100 ms 的 handler 处理时间。

对大多数应用来说差异不可感知。如果你在构建逐键重解析的实时编辑器,收益会累积。