zh CN 处理器辅助函数 - chiba233/yumeDSL GitHub Wiki

处理器辅助函数

自定义标签名字符 | ParseOptions 选项

你定义了一堆标签(bold、link、code……),每个都要写 handler 对象?太累了。 辅助函数帮你批量注册,一行搞定一个标签。

本页讲的是"怎么快速注册" 。如果你需要更底层的控制——手动拆管道、反转义、逐字符扫描——请看 处理器工具函数

签名注意: 本页的 createPipeHandlers 回调签名和底层 TagHandler 不同。createPipeHandlers 帮你预解析了管道参数,所以第一个参数是 PipeArgs,不是原始 tokensarg。底层 TagHandler 签名见 编写标签处理器


什么时候用哪个

                    "我要注册标签 handler"
                            │
              ┌─────────────┼──────────────┐
              ▼             ▼              ▼
         需要管道参数?    纯包装器?      只声明换行规范化?
         需要多种形式?   (无管道)       (不创建 handler)
              │             │              │
              ▼             ▼              ▼
     createPipeHandlers   createSimple*   declareMultilineTags
     (推荐,万能)       Handlers        (配合 blockTags 用)
                            │
                  ┌─────────┼─────────┐
                  ▼         ▼         ▼
              Inline     Block      Raw
              Handlers   Handlers   Handlers

一句话版本:

辅助函数 干嘛的 什么时候用
createPipeHandlers 一个定义覆盖 inline/raw/block 任意组合,自动帮你解析管道参数 大多数标签用这个就对了
createSimpleInlineHandlers 传个名字数组,批量生成 inline 包装器 ["bold", "italic", "underline"] 这种不需要参数的简单标签
createSimpleBlockHandlers 同上,但是 block 形式 简单 block 包装器
createSimpleRawHandlers 同上,但是 raw 形式 ["code", "math"] 这种内容不需要递归解析的
空对象 handlers 直接声明标签名,交给默认 materialization / fallback 只想零成本声明标签存在;这是推荐的标准隐式写法
declareMultilineTags 不创建 handler,只告诉解析器哪些标签要做换行规范化 只针对需要特殊处理的标签(和自动推导合并)

createPipeHandlers(definitions)

function createPipeHandlers<const T extends Record<string, PipeHandlerDefinition>>(
    definitions: T
): { [K in keyof T]: TagHandler }

推荐的 handler 辅助函数。它做的事很简单:帮你在回调执行前自动调一次 parsePipeArgs,这样你拿到的第一个参数就已经是拆好管道的 PipeArgs,而不是原始的 tokens / arg 字符串。

它到底帮你省了什么

下面两段代码完全等价。左边是手写 TagHandler,右边是用 createPipeHandlers

// ── 手写 TagHandler(你自己调 parsePipeArgs)──
const handlers = {
    link: {
        inline: (tokens, ctx) => {
            const args = parsePipeArgs(tokens, ctx);
            return { type: "link", url: args.text(0), value: args.materializedTailTokens(1) };
        },
    },
};

// ── createPipeHandlers(它帮你调 parsePipeArgs)──
const handlers = createPipeHandlers({
    link: {
        inline: (args, ctx) => ({
            type: "link", url: args.text(0), value: args.materializedTailTokens(1),
        }),
    },
});

区别只有一个:回调的第一个参数从 tokens: TextToken[] 变成了 args: PipeArgs。其余参数(contentctx)位置和类型都一样。

PipeHandlerDefinition — 你写的回调签名

interface PipeHandlerDefinition {
    inline?: (args: PipeArgs, ctx?: DslContext) => TokenDraft;
    raw?: (args: PipeArgs, content: string, ctx?: DslContext, rawArg?: string) => TokenDraft;
    block?: (args: PipeArgs, content: TextToken[], ctx?: DslContext, rawArg?: string) => TokenDraft;
}

和底层 TagHandler 的签名对照:

形式 TagHandler(底层) PipeHandlerDefinition(你写的) 差异
inline (tokens, ctx) => TokenDraft (args, ctx) => TokenDraft tokensargs(自动 parsePipeArgs
raw (arg, content, ctx) => … (args, content, ctx, rawArg) => … argargs(自动 parsePipeTextArgs);原始 arg 移到 rawArg
block (arg, content, ctx) => … (args, content, ctx, rawArg) => … 同 raw

rawArg 就是底层 TagHandler 收到的那个原始 arg 字符串(管道分割前的),需要时才用。

PipeArgs — 你拿到的参数对象

interface PipeArgs {
    parts: TextToken[][];
    has: (index: number) => boolean;
    text: (index: number, fallback?: string) => string;
    materializedTokens: (index: number, fallback?: TextToken[]) => TextToken[];
    materializedTailTokens: (startIndex: number, fallback?: TextToken[]) => TextToken[];
}
方法 一句话
parts 按管道拆分的原始 token 段
has(i) 第 i 段存在吗?
text(i) 第 i 段的纯文本(已反转义、已修剪)
materializedTokens(i) 第 i 段的 token(文本已反转义,但保留结构)
materializedTailTokens(start) 从 start 开始的所有段合并成一个数组——适合"标签内容本身含管道"的场景

完整示例

import {createPipeHandlers} from "yume-dsl-rich-text";

const handlers = createPipeHandlers({
    // 仅 inline: $$link(https://example.com | click here)$$
    link: {
        inline: (args, ctx) => ({
            type: "link",
            url: args.text(0),
            value: args.materializedTailTokens(1),
        }),
    },

    // Inline + block: $$info(tip)$$ 或 $$info(tip)*\ncontent\n*end$$
    info: {
        inline: (args, ctx) => ({
            type: "info",
            title: args.text(0),
            value: args.materializedTailTokens(1),
        }),
        block: (args, content, ctx) => ({
            type: "info",
            title: args.text(0),
            value: content,
        }),
    },

    // 仅 raw: $$code(ts)%\nconst x = 1;\n%end$$
    code: {
        raw: (args, content, ctx) => ({
            type: "code",
            lang: args.text(0, "text"),
            value: content,
        }),
    },
});

createSimpleInlineHandlers(names)

function createSimpleInlineHandlers<const T extends readonly string[]>(
    names: T
): Record<T[number], TagHandler>

传个标签名数组进去,批量生成 inline handler。每个 handler 自动反转义子 token、包成 { type: tagName, value: materializedTokens }

const handlers = createSimpleInlineHandlers(["bold", "italic", "underline"]);
// 等价于手写三个 { inline: (tokens, ctx) => ({ type: "bold", value: materializeTextTokens(tokens, ctx) }) }

标准隐式写法:空对象 handlers

如果你并不需要显式写 inline / block / raw 回调,只是想声明“这些标签名存在”,最短写法就是直接写空对象:

const handlers = {
    bold: {},
    italic: {},
};

这表示:

  • 标签名已注册
  • 最终产物交给解析器默认的 materialization / fallback 逻辑
  • 你没有显式指定固定输出形状
  • 它实际只给你一条inline 默认输出路径

它和 createSimpleInlineHandlers(...) 的区别是:

写法 语义
createSimpleInlineHandlers(["bold"]) 显式安装 inline handler,固定产出 { type: "bold", value: materializedTokens }
bold: {} 只声明标签名存在,依赖默认 materialization / fallback

这套空对象写法是文档正式推荐的标准隐式写法createPassthroughTags 之所以被放进待弃用,是因为库不再想维护一个专门包装这层语义的 helper; 但空对象 handler 这种“零成本声明语法”本身仍然是推荐用法

实际效果(基于代码和运行结果):

parseRichText("$$bold(world)$$", {
    handlers: {bold: {}},
});
// => [{ type: "bold", value: [{ type: "text", value: "world" }] }]

parseRichText("$$code(js)%\nconst x = 1;\n%end$$", {
    handlers: {code: {}},
});
// => [{ type: "text", value: "$$code(js)%\nconst x = 1;\n%end$$" }]

parseRichText("$$info(note)*\nhello\n*end$$", {
    handlers: {info: {}},
});
// => [{ type: "text", value: "$$info(note)*\nhello\n*end$$" }]

结论:

  • 空对象写法的默认输出只覆盖 inline
  • raw / block 不会自动启用,没有对应 handler 时仍然降级回源码文本
  • 如果同名标签既要支持 inline,又要支持 raw 或 block,请显式写 inline + raw / block

createSimpleBlockHandlers(names)

function createSimpleBlockHandlers<const T extends readonly string[]>(
    names: T
): Record<T[number], TagHandler>

批量生成 block handler。每个 handler 直接透传 arg 和已递归解析的 content,不做反转义。输出 { type: tagName, arg, value: content }

const handlers = createSimpleBlockHandlers(["info", "warning", "collapse"]);

createSimpleRawHandlers(names)

function createSimpleRawHandlers<const T extends readonly string[]>(
    names: T
): Record<T[number], TagHandler>

批量生成 raw handler。和 block 类似,但 content 是 string(不递归解析)。

const handlers = createSimpleRawHandlers(["code", "math", "latex"]);

declareMultilineTags(names)

function declareMultilineTags<const T extends readonly BlockTagInput[]>(
    names: T
): BlockTagInput[]

它解决什么问题

对于具有块级/容器渲染语义的标签——对话框、代码块、折叠面板、信息卡片——不管它们在 DSL 里用的是 block()*...*end$$)、raw( )%...%end$$)还是 inline($$tag(...)$$)形式,只要最终渲染成块级元素,首尾换行都会产生同一个问题:凭空多出空行

以 block 形式为例:

$$speaker(Alice)*
Hello!
*end$$

人类会很自然地把 )**end$$ 各占一行。但从解析器的视角看,)* 之后紧接着的是一个 \n*end$$ 之前紧接着的也是一个 \n——于是原始内容变成了 "\nHello!\n" 而不是 "Hello!"。这两个边界换行不是作者想要的内容,而是多行语法的书写副产物。

即使是 inline 形式,只要标签渲染为块级元素,内容中的首尾换行同样会导致渲染出多余空行。关键不在于 DSL 用了哪种语法形式,而在于 标签的渲染语义是不是块级的

如果不做规范化,这些多余换行会在每个块级标签的渲染输出中反复出现,是一类极其隐蔽且难以排查的视觉 bug。

换行规范化做了什么

解析器对声明过的标签执行首尾各剥一个换行的规范化:

位置 原始内容 规范化后
)* / )% 之后 \n\r\n → 剥掉 内容从第一行实际文字开始
*end$$ / %end$$ 之前 \n\r\n → 剥掉 内容到最后一行实际文字结束

只剥恰好一个换行,不会多吃。如果作者有意写了多个空行,只有紧贴边界的那一个被剥掉,其余保留。

同时,剥离产生的偏移量会精确回传给 position tracker,所以即使在开启 trackPositions 的场景下,源码定位依然准确。

默认行为:自动推导

大多数情况下你不需要手动调用。 解析器在创建时会自动扫描 handlers:

  • handler 有 raw 方法 → 该标签在 raw 形式下做规范化
  • handler 有 block 方法 → 该标签在 block 形式下做规范化

也就是说,只要你用 createSimpleBlockHandlerscreateSimpleRawHandlerscreatePipeHandlers 等注册了多行 handler,规范化就已经自动生效了。

blockTags 和自动推导的合并

自动推导始终作为基底运行。传 blockTags 时,覆盖是按标签的,不是全局的

  • 显式列出的标签 → 你的声明完全替换该标签的自动推导(替换的是所有形式,不是只替换你列出的——没列出的形式对该标签变为禁用)
  • 没提到的标签 → 自动推导继续生效,不受影响

这意味着你只需要声明需要特殊处理的标签,不用为了给一个标签加 inline 规范化就把所有 raw/block 标签重新列一遍。但如果你列了某个标签,确保把你想要的所有形式都写上——自动推导不会帮你补齐剩下的。

// 只声明 center — 其他所有 raw/block 标签保留自动推导
blockTags: declareMultilineTags([{tag: "center", forms: ["inline"]}])

经验法则

对具有块级/容器渲染语义的标签,通常都应该确保它出现在 blockTags 中(不管是自动推导还是手动声明)。否则首尾换行会被算进内容,导致渲染时出现多余空白行。

什么时候需要手动声明

当自动推导的结果不是你想要的时候:

场景 做法
标签只用空对象 handlers 注册,但你知道它会以 block 形式使用 手动声明
标签渲染为块级元素,但只注册了 inline handler——自动推导不会覆盖到它 { tag, forms: ["inline"] } 声明
标签同时有 raw 和 block handler,但你只想在 raw 形式下规范化 { tag, forms: ["raw"] } — 只覆盖该标签的自动推导

用法

传字符串: 一次声明三种形式全部规范化(raw + block + inline)——最常用的方式。

blockTags: declareMultilineTags(["info", "warning", "center"])

传对象:{ tag, forms } 精细控制只在哪种形式下规范化。

blockTags: declareMultilineTags([
    "info",                                // 字符串:三种形式全部规范化
    {tag: "code", forms: ["raw"]},       // 仅 raw 形式
    {tag: "center", forms: ["inline"]},  // 仅 inline 形式
])

forms 接受的值:

规范化行为 适用场景
"raw" 剥掉 )% 后的前导 \n%end$$ 前的尾随 \n 多行 raw 标签($$code(ts)%\n...\n%end$$
"block" 剥掉 )* 后的前导 \n*end$$ 前的尾随 \n 多行 block 标签($$info()*\n...\n*end$$
"inline" 剥掉 inline close $$ 后紧跟的 \n 用 inline 语法但渲染为块级元素的标签($$center(...)$$

省略 forms 的对象形式默认为 ["raw", "block"](向后兼容)。

注意: declareMultilineTags 不创建 handler,也不注册标签——它只控制换行规范化策略。标签注册请用本页其他辅助函数或手写 handler。


已弃用

已弃用 用这个替代
createPipeBlockHandlers createPipeHandlers + block 方法
createPipeRawHandlers createPipeHandlers + raw 方法
createPassthroughTags 空对象 handlers / 本地 helper(如果你就是要保留隐式 fallback)