zh CN API 参考 - chiba233/yumeDSL GitHub Wiki

API 参考

Home | DSL 语法 | 自定义语法

四个核心函数,做四件事:

                        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[] handlersallowFormsimplicitInlineShorthandsyntaxtagNamedepthLimittrackPositions
print StructuralNode[] + overrides? string syntax——overrides 与 defaults 深合并。无损序列化器,不进行门控

按调用覆盖合并(自 1.0.11 起)

按调用的 overrides 会浅合并到默认值上,但 syntaxtagName 还会额外进行一层深合并,因此部分覆盖可以保留其余默认值:

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;
};

注意:一层深合并仅在 defaultsoverrides 包含该字段时才会发生。如果只有 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-0rt-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_CLOSEDSHORTHAND_NOT_CLOSEDBLOCK_NOT_CLOSEDRAW_NOT_CLOSED)。周围内容不会被破坏。
  • 格式错误的闭合标记:通过 BLOCK_CLOSE_MALFORMEDRAW_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 共享相同的语言配置(handlersallowFormssyntaxtagNamedepthLimittrackPositions ),因此你无需维护两套独立的 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 包含 createIdblockTagsmodeonError——这些是仅属于渲染管线的 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: trueprintStructural 在该节点位于其他 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 深合并,与 parsestructural 行为一致:

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 + rawClosecontent 字段原样输出,不进行递归处理。
  • 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。在编辑器管线中 parseStructural1.1.6 ~24.6 ms)是瓶颈,printStructural / buildZones / walkTokens 相当于免费。

parser 本体的性能基准集中放在性能: 正常 ~200 KB 文档基准、parseSlice 增量解析,以及 5000 层深链 stress case 都在那里统一维护。

parseRichTextparseStructural 基准见性能createEasyStableId 基准见稳定 Token ID——性能unescapeInline / splitTokensByPipe 基准见处理器工具函数——性能