# 基于MdIt的无序列表折叠插件 当前`Markdown`已经成为最好的编程语言,同样的`Md`也成为了产品文档最需要支持的格式,特别是面向开发者的文档。实际上很多情况下编程和文档的场景是非常类似的,因此在时代的推动下,原生支持`Md`生产和消费的文档系统的需求重新出现。 在这里我们关注于`API`文档类型的展示,在`OpenAI`、`Claude`的`API`文档中,可以看到其表达参数列表的形式类似折叠列表。而观察原始的`Md`文档,就可以看出其参数列表的形式是无序列表,因此我们也实现类似的功能来将无序列表转换为折叠列表展示。 实际上,将无序列表渲染成折叠列表这件事,本身还是面向开发者阅读的,如果单纯是面向`AI`来消费,则仅提供纯文本的`Md`内容即可。目前来看,同时需要面向开发者和`AI`的状态应该还需要存在较长的时间,因此实现一套`Md`渲染器还是有必要的。
LLM Engineering 系列相关文章 - [基于 fetch 的 SSE 方案](./基于fetch的SSE方案.md) - [基于向量检索实现基础 RAG 服务](./基于向量检索实现基础RAG服务.md) - [流式 Markdown 增量富文本解析算法](./流式Markdown增量富文本解析算法.md) - [基于 NodeJs 实现任务队列与优雅停机](./基于NodeJs实现任务队列与优雅停机.md) - [仿照豆包实现Prompt变量模板输入框](./仿照豆包实现Prompt变量模板输入框.md) - [基于MdIt的无序列表折叠插件](./基于MdIt的无序列表折叠插件.md)
## 解析规则 首先我们需要分析无序列表结构及其解析后的`HTML`,基本的无序列表结构如下所示: ```md - 0 - 1 - 1.1 - 1.2 - 1.2.1 - 1.2.2 - 1.3 with desc - 1.3.1 - 1.3.2 - 2 ``` ```html ``` 可以看出示例中存在三级`ul`元素结构嵌套,以及描述内容的`li`元素,我们需要根据不同的情况来解析。理论上而言,只有存在嵌套结构的`li`元素才需要解析为折叠结构,其子元素内起始到`ul`之间的内容需要作为标题,`ul`内元素则作为折叠展开的内容。 通常来说,实现类似手风琴的效果,大概会主动管理状态,用`div`等元素来绘制折叠面板,然后主动处理点击事件,来切换折叠展开的状态。不过,`HTML`原生支持了`details`元素以及`summary`元素,我们可以借助原生元素来实现折叠列表的效果,其主要优点是: - 简单易用,通常情况下不需要主动管理状态,仅需要维护`DOM`结构。 - 无需处理事件,特别是在`SSR`的情况下,不需要再`hydrate`注入事件。 - 原生支持搜索,使用浏览器搜索时,可以自动展开包含搜索关键词的折叠列表。 ```html
Details Something more.
``` 那么根据以上的`HTML`结构,我们可以根据无序列表的结构,转换为`details+summary`元素的结构。观察其结构,我们可以实现如下转换规则: - `ul`元素作为折叠展开的内容,这里可以自定义为`block`元素,也可以保持`ul`元素。 - 当`li`元素内存在嵌套的直属`ul`元素时,该`li`元素需要转换为`details`元素。 - 转换的`details`元素的子元素,从起始到`ul`元素之间的内容,需要包装`summary`元素。 根据上述的转换规则,我们可以将最开始的无序列表`HTML`内容转换为`details + summary`元素的结构: ```html ``` ## 元素重建 在设计好`HTML`结构的转换规则后,我们需要在`MarkdownIt`的基础上实现转换逻辑。在`MdIt`中提供了诸多时机的`Hook`函数,我们需要根据处理的时机来实现转换逻辑,通常来说应该尽可能在后处理阶段来实现相关逻辑,这里我们分别实现解析后处理和渲染时处理。 ### 渲染时处理 因此,我们首先来看仅渲染阶段的`rule`处理逻辑,在上述的转换规则中,将`ul`元素转换为`block`元素,以及将`li`元素渲染为`details`元素,这两点是没什么问题的。然而,为子节点包装`summary`元素,则是比较麻烦的。 在仅渲染阶段,这件事并非不能实现,但是却容易破坏`MdIt`的线性解析模式。如果这是个递归结构,则仅需要将其节点包一层`DOM`元素即可,而在线性结构中,包装一层`summary`元素需要在`li_open`追加``元素,在`ul_open`前置``元素。 ```js mdIt.renderer.rules.bullet_list_open = (tokens: Token[], idx: number) => { const current = tokens[idx]; for (let i = idx - 1; i >= 0; i--) { const token = tokens[i]; if (token.level < current.level - 1) break; if (token.type === "list_item_open" && token.level === current.level - 1) { return "" + "