zh CN 快速开始 - chiba233/yumeDSL GitHub Wiki

快速开始

Home | 下一步:DSL 语法

安装

npm install yume-dsl-rich-text   # 或 pnpm add / yarn add

三步上手

1. 创建解析器

import {
    createParser,
    createSimpleInlineHandlers,
    createSimpleBlockHandlers,
    createSimpleRawHandlers,
    declareMultilineTags,
} from "yume-dsl-rich-text";

const dsl = createParser({
    handlers: {
        ...createSimpleInlineHandlers(["bold", "italic", "underline", "strike"]),
        ...createSimpleBlockHandlers(["info", "warning"]),
        ...createSimpleRawHandlers(["code"]),
    },
    blockTags: declareMultilineTags(["info", "warning", "code"]),
});

如果你只是想零成本声明几个标签名,也可以直接写空对象:

const dsl = createParser({
    handlers: {
        bold: {},
        italic: {},
        ...createSimpleBlockHandlers(["info", "warning"]),
        ...createSimpleRawHandlers(["code"]),
    },
    blockTags: declareMultilineTags(["info", "warning", "code"]),
});
  • bold: {} / italic: {} 表示“注册这个标签名,但交给解析器默认 materialization / fallback 处理”
  • 这是库文档正式推荐的标准隐式写法,也是一种很适合入门场景的零成本声明语法
  • 如果你想固定产出形状(例如明确得到 {type, value}),再用 createSimpleInlineHandlers(...)

实际行为可以直接理解成:

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 这条路:inline 会 fallback 成 { type: tagName, value: materializedChildren }
  • raw / block 不会因为写了空对象就自动启用:没有 raw / block 方法时,这两种 form 仍然会降级回源码文本
  • 如果同名标签既要 inline,又要 raw/block,请显式写出 inline + raw / block;不要指望 raw/block handler 会自动保留 inline

用到了什么:

辅助函数 创建什么 对应语法
createSimpleInlineHandlers inline handler(透传子 token) $$tag(content)$$
createSimpleBlockHandlers block handler(递归解析内容) $$tag(arg)* ... *end$$
createSimpleRawHandlers raw handler(原样捕获内容) $$tag(arg)% ... %end$$
declareMultilineTags 块级标签的换行规范化 raw / block / inline

另外一种常用写法:

写法 含义 适合什么场景
bold: {} / italic: {} 注册空 handler,交给默认 materialization / fallback 只想声明标签存在、先零成本跑起来的 inline 标签

2. 解析

const tokens = dsl.parse("Hello $$bold(world)$$!");

结果:

[
    {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"},
]

每个 token:

  • type — 纯文本是 "text",标签节点是 handler 返回的类型(通常就是标签名)
  • value — 文本 token 是字符串,inline/block 标签是子 token 数组
  • id — 解析内唯一标识

关于 ctx 你会在示例里看到 handler 回调带一个 ctx 参数。不需要知道它是什么——解析器传给你的,照着写就行。想深入了解的看 DslContext

3. 提取纯文本

const plain = dsl.strip("Hello $$bold(world)$$!");
// "Hello world!"

搜索索引、纯文本预览、无障碍回退都能用。

4. 使用简写语法(1.3 起)

在 inline 标签参数内部,可以省略 $$...$$ 包裹,使用 tag(...) 简写:

const dsl = createParser({
    handlers: {
        ...createSimpleInlineHandlers(["bold", "italic"]),
    },
    implicitInlineShorthand: true,   // 或 ["bold", "italic"] 白名单
});

const tokens = dsl.parse("$$bold(Hello italic(world))$$");
// 等价于 "$$bold(Hello $$italic(world)$$)$$"
// 简写只在 inline 参数内部生效,根级仍需完整语法
  • 简写仅在 inline 参数上下文内生效,根级文本不会触发简写
  • 完整 DSL 语法($$tag(...)$$)始终优先于简写
  • 字面括号用 \( / \) 转义
  • handlers 中注册的 inline 标签会被识别

详见 DSL 语法 — 隐式 inline 简写ParseOptions — implicitInlineShorthand

5. 大文档增量解析

文档比较大(几十 KB 往上)而且需要按键级别反复解析时,用增量会话避免每次全量重扫结构树。

增量 session 维护的是 structural 层StructuralNode[] + Zone[]),不是 TextToken[]。它的价值在于:每次编辑后你能立刻拿到最新的结构树,用来驱动高亮、outline、lint 等结构级功能,而不需要每次都从零重建。如果你还需要最终渲染用的 TextToken[],对编辑后文本调 dsl.parse() 即可。

import {
    createParser,
    createIncrementalSession,
    createSimpleInlineHandlers,
    createSimpleBlockHandlers,
    createSimpleRawHandlers,
} from "yume-dsl-rich-text";

// ── 1. 准备 handlers 和 parser ──

const handlers = {
    ...createSimpleInlineHandlers(["bold", "italic", "underline"]),
    ...createSimpleBlockHandlers(["info", "warning"]),
    ...createSimpleRawHandlers(["code"]),
};

const dsl = createParser({handlers});

// ── 2. 假设这是编辑器加载时的初始文本 ──

let currentSource = "Hello $$bold(world)$$!\n$$code()%console.log('hi')%end$$";

// ── 3. 建立增量会话 + 首次渲染 ──

const session = createIncrementalSession(currentSource, {handlers});

// 结构树:用于高亮 / outline / lint
let tree = session.getDocument().tree;
updateHighlighting(tree);
updateOutline(tree);

// TextToken[]:用于最终渲染
let tokens = dsl.parse(currentSource);
render(tokens);

// ── 4. 用户编辑:把 "world" 改成 "everyone" ──
//    旧文本: "Hello $$bold(world)$$!..."
//                         ^^^^^
//    startOffset = 14, oldEndOffset = 19, newText = "everyone"

const startOffset = 14;
const oldEndOffset = 19;
const newText = "everyone";

// 拼出编辑后的完整文本
currentSource =
    currentSource.slice(0, startOffset) +
    newText +
    currentSource.slice(oldEndOffset);

// ── 5. 推进增量 session + 重新渲染 ──

const result = session.applyEdit(
    {startOffset, oldEndOffset, newText},
    currentSource,
);

// result.mode: "incremental" | "full-fallback"

// 结构树已增量更新(大文档下比全量快数倍到 10 倍)
tree = result.doc.tree;
updateHighlighting(tree);
updateOutline(tree);

// TextToken[] 仍然走 dsl.parse()(它本身就很快)
tokens = dsl.parse(currentSource);
render(tokens);
// [
//   { type: "text", value: "Hello " },
//   { type: "bold", value: [{ type: "text", value: "everyone" }] },
//   { type: "text", value: "!\n" },
//   { type: "code", value: "console.log('hi')" },
// ]
  • session 会自动选择增量或全量——增量安全时走增量,否则静默回退全量,调用方不用操心。
  • 如果还需要知道"这次具体改了哪些结构",改用 session.applyEditWithDiff(...),返回值多一个 diff 字段。
  • 不信任缓存时随时调 session.rebuild(newSource) 强制全量重建。

详见 增量解析


接下来读什么

  1. DSL 语法 — 三种标签形式、管道参数、转义
  2. API 参考 — createParser、parseRichText、parseStructural 等
  3. 处理器辅助函数 — 批量注册标签的工厂函数
  4. 编写标签处理器 — 手写 handler 的详细指南
  5. 导出一览 — 所有导出的函数、类型、常量