zh CN 快速开始 - chiba233/yumeDSL GitHub Wiki
快速开始
安装
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)强制全量重建。
详见 增量解析。