tiptap OrderedList 有序列表扩展完全指南:从@tiptap/extension-ordered-list到@tiptap/extension-list的演进与迁移
【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap
导读
本文以 tiptap 仓库中 @tiptap/extension-ordered-list 的版本变更日志 为主线,结合 monorepo 中列表扩展的真实实现源码,系统讲解 tiptap 有序列表扩展的架构演进、配置项语义、Schema 与快捷键行为,以及从分散列表包迁移到聚合包@tiptap/extension-list的完整操作步骤。读完本文,你将能理解为何当前extension-ordered-list包是一个"瘦身"后的重导出兼容层,掌握OrderedList全部选项的默认值与底层影响,并能在 v3 时代正确安装、配置和迁移列表相关依赖。
适用前提说明:本文涉及的命令与包结构以当前仓库(v3.30.3 时代、基于 pnpm workspace 的 monorepo)为准。仓库采用 pnpm workspace 管理多包,且各列表扩展已被整合进
@tiptap/extension-list,@tiptap/extension-ordered-list仅作为兼容性入口存在(见 packages/extension-ordered-list/src/index.ts)。
一、版本日志揭示的事实:一个被"合并"进扩展聚合包的独立扩展
@tiptap/extension-ordered-list的 CHANGELOG.md 记录了该包完整的发布轨迹:从2.0.0-alpha时代、到 v2 稳定期(2.0.x~2.12.0)、再到 v3 时代的3.0.0-beta.x/3.0.0-next.x与最终稳定版 3.30.3。该日志可以提炼出三个关键事实:
- v3 稳定版的绝大多数发布都是"Patch Changes",并且几乎全部内容仅是一行依赖说明,例如
3.30.3对应@tiptap/extension-list@3.30.3。这说明有序列表的真实实现早已不在本包,而是一路跟随聚合包@tiptap/extension-list的版本号同步发布。 - v3.0.0 是一次决定性的架构调整(变更哈希
2c911d2):官方将所有列表相关扩展的代码搬进@tiptap/extension-list,本包变成纯转发层,同时引入ListKit作为一次性注册/配置全部列表扩展的推荐方式。 - 少数非纯版本号条目揭示出重要的行为变更:v2.11.6 将有序列表默认
type值改为null以便于 Schema 扩展(9abb019);beta.26 增加itemTypeName选项(3d7c8e6);2.0.0-beta.219 支持在列表上保留 marks(#3540/#3541,提交36bb1e1)。
1.1 为什么这个包还在,却几乎"没有代码"
对照仓库结构即可验证上述判断。当前 packages/extension-ordered-list 包内:
src/index.ts全文仅做三件事:从@tiptap/extension-list导入OrderedList、再导出其类型OrderedListOptions、最后将其作为默认导出——即保持对老式import OrderedList from '@tiptap/extension-ordered-list'的兼容;- package.json 中
@tiptap/extension-list被声明为唯一的peerDependencies(workspace 内联解析); - 真实实现位于 packages/extension-list/src/ordered-list/ordered-list.ts,并在同一包的
kit/目录下通过 ListKit 统一装配。
也就是说,对升级到 v3 的用户而言,直接使用聚合包即可拿到与旧包完全等价的OrderedList,同时还能减少对等依赖冲突。
二、v3 官方迁移指南:合并列表包与 ListKit
变更日志在3.0.0/3.0.0-next.6条目中给出了完整的官方迁移说明。官方明确的推荐做法是:使用ListKit一次性配置所有列表扩展。
import { ListKit } from "@tiptap/extension-list"; new Editor({ extensions: [ ListKit.configure({ bulletList: { HTMLAttributes: "bullet-list", }, orderedList: { HTMLAttributes: "ordered-list", }, listItem: { HTMLAttributes: "list-item", }, taskList: { HTMLAttributes: "task-list", }, taskItem: { HTMLAttributes: "task-item", }, listKeymap: {}, }), ], });ListKit的可配置子项与源码完全对应:ListKitOptions 依次暴露bulletList、listItem、listKeymap、orderedList、taskItem、taskList六个键,每个键的类型是Partial<选项> | false——即传入false可整体关闭某个子扩展(例如纯文本编辑器不需要任务列表时设taskList: false、taskItem: false),其余键则透传给对应的.configure()。
2.1 依赖清理:卸载旧包、安装聚合包
官方在日志中明确给出了 npm 层面的替换命令。由于代码已全部迁移到聚合包,可移除下列旧依赖:
npm uninstall @tiptap/extension-ordered-list @tiptap/extension-bullet-list @tiptap/extension-list-keymap @tiptap/extension-list-item @tiptap/extension-task-list再安装聚合包作为替代:
npm install @tiptap/extension-list值得注意的是:当前仓库本身仍保留着@tiptap/extension-ordered-list这一空壳包,目的就是为上述旧依赖做平滑过渡;若你直接以聚合包为目标,未来升级时就不会再碰到日志3.22.4条目中提到的"依赖更新后触发 peer dependency 解析冲突"(27ea931)这一类连锁问题。
2.2 需要更细粒度控制?也可以单独使用各扩展
官方迁移指南同时指出,如果不想使用聚合式ListKit,也可以单独引入各扩展,以下是日志给出的逐包迁移对照与用法:
OrderedList(本主题核心)
- import OrderedList from '@tiptap/extension-ordered-list' + import { OrderedList } from '@tiptap/extension-list'import { OrderedList } from "@tiptap/extension-list";BulletList
- import BulletList from '@tiptap/extension-bullet-list' + import { BulletList } from '@tiptap/extension-list'import { BulletList } from "@tiptap/extension-list";ListItem
- import ListItem from '@tiptap/extension-list-item' + import { ListItem } from '@tiptap/extension-list'import { ListItem } from "@tiptap/extension-list";TaskList
- import TaskList from '@tiptap/extension-task-list' + import { TaskList } from '@tiptap/extension-list'import { TaskList } from "@tiptap/extension-list";TaskItem
- import TaskItem from '@tiptap/extension-task-item' + import { TaskItem } from '@tiptap/extension-list'import { TaskItem } from "@tiptap/extension-list";ListKeymap
- import ListKeymap from '@tiptap/extension-list-keymap' + import { ListKeymap } from '@tiptap/extension-list'import { ListKeymap } from "@tiptap/extension-list";源码层面,单独的 OrderedList 通过Node.create<OrderedListOptions>({ name: 'orderedList', ... })注册;其 content 定义为${itemTypeName}+,即有序列表内必须含至少一个列表项节点,且官方注释明确:使用 OrderedList 依赖同时启用 ListItem 扩展。
三、OrderedList 选项深度解析(结合源码确认默认值)
日志记录的关键演进——新增itemTypeName(beta.26,2021-12-10)、parseHTML属性直接返回值(beta.16,修复 #1863)、保留列表上的 marks(2.0.0-beta.219,#3540/#3541)——最终沉淀为OrderedListOptions的四个选项。其类型定义与默认值可从 ordered-list.ts 的addOptions()与接口注释中完整确认:
| 选项 | 默认值 | 说明 | 来源/演进 |
|---|---|---|---|
itemTypeName | 'listItem' | 列表项节点类型名,用于content声明与切换命令 | beta.26(3d7c8e6)新增 |
HTMLAttributes | {} | 渲染到<ol>标签上的自定义 HTML 属性(如class) | 各框架通用 |
keepMarks | false | 拆分列表项时是否保留当前 marks | 2.0.0-beta.219(#3540/#3541) |
keepAttributes | false | 拆分列表项时是否保留属性 | 与keepMarks配套演进 |
3.1 keepMarks / keepAttributes 在命令中的真实作用路径
查看 addCommands() 的实现可更准确地理解这两个开关。toggleOrderedList命令在keepAttributes为真时走命令链,先toggleList再用textStyle的属性回填到listItem;否则直接执行commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks):
toggleOrderedList: () => ({ commands, chain }) => { if (this.options.keepAttributes) { return chain() .toggleList(this.name, this.options.itemTypeName, this.options.keepMarks) .updateAttributes(ListItemName, this.editor.getAttributes(TextStyleName)) .run() } return commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks) },同理,addInputRules() 会在任一开关为真时改用携带keepMarks/keepAttributes参数及textStyle属性的wrappingInputRule。这解释了日志#3541"Ability to preserve marks on lists"的含义:当用户把带有加粗/斜体等样式的段落转换为有序列表时,这些样式不会丢失。
3.2 HTMLAttributes 的典型用法
HTMLAttributes与 OrderedList 节点自身的start/type属性会被 renderHTML() 通过mergeAttributes(this.options.HTMLAttributes, attributesWithoutType)合并后渲染到<ol>上。例如给所有有序列表加一个用于样式的类:
OrderedList.configure({ HTMLAttributes: { class: 'my-ordered-list' }, })start与type属于节点内置属性,会被从自定义属性中剥离并按规则输出(见下一节),不会和用户自定义 HTML 属性冲突。
四、Schema 内建属性:start 与 type
有序列表节点在 addAttributes() 中声明了两个属性,理解它们对"粘贴外部富文本"场景尤其重要:
start:默认1。解析 HTML 时读取<ol start>,无该属性则回落为1;渲染时仅在start !== 1时输出start属性。type:默认null。解析 HTML 时按三级策略探测编号类型:- 读取
<ol>上的type属性; - 读取
<ol>style 中的list-style-type(通过cssListStyleTypeToHtmlType映射); - 读取第一个
<li>上的list-style-type——官方注释指出这是Google Docs 的典型写法。
- 读取
映射规则集中在源码注释清晰的cssListStyleTypeToHtmlType函数中:
CSSlist-style-type值 | 输出的 HTMLtype值 |
|---|---|
upper-roman | I |
lower-roman | i |
upper-alpha/upper-latin | A |
lower-alpha/lower-latin | a |
| 其他值 | 不输出(返回null) |
渲染时仅当type存在且不等于'1'才输出type属性。这一设计正是日志 v2.11.6 条目"Use null in ordered list's default type value for better schema extension support"(9abb019)的直接体现:把默认值从某种"数字类型"改为null,使parseHTML探测到的type值(如'a'、'i')能干净地写入 Schema,避免与默认类型混淆,也方便后续扩展覆盖属性。
type相关行为在聚合包测试 orderedListType.spec.ts 中有覆盖;parseHTML特性本身最早由 beta.16 的"属性解析直接返回值"变更(修复 #1863)奠定。
五、快捷键、输入规则与粘贴处理
5.1 快捷键
addKeyboardShortcuts() 为Mod-Shift-7绑定了toggleOrderedList()(Mod在 macOS 对应 Cmd、在其他平台对应 Ctrl)。也就是说默认编辑器里按下Ctrl/Cmd + Shift + 7即可切换有序列表。
5.2 输入规则:从"数字加点和空格"开始
有序列表支持"输入即转换",其识别正则在源码中直接导出,便于扩展复用:
export const orderedListInputRegex = /^(\d+)\.\s$/即当用户输入诸如1.时,wrappingInputRule会命中,并把start属性设为该数字。值得注意的是 joinPredicate 的合并条件:只有当现有列表的type为空或为'1'(未定制编号风格),且现有项数加start等于新输入的数字时,才会把输入合并进已有列表;而像a)、i)这类带类型的列表则刻意保持独立,不参与合并。该输入规则能力源自 2.0.0-beta.17 的"把 input rules 与 paste rules 整合进 core"(#1997)。
5.3 纯文本粘贴:识别并重建有序列表结构
源码通过 addProseMirrorPlugins() 注册了一个自定义handlePaste:当剪贴板**只有纯文本(无 HTML)**且能按parsePlainTextOrderedListPaste解析出列表内容时,直接构造orderedList节点并replaceSelectionWith替换选区,从而实现"从外部复制一段带编号的文本粘贴进来自动成为有序列表"。若剪贴板含 HTML 或文本无法解析,则返回false走 ProseMirror 默认路径。
与"误判列表"相关的回归测试存在于 orderedListPhoneNumber.spec.ts——电话号(216) 555-1234这类行中出现的216)不应被识别为列表起点。
5.4 Markdown 协同:数字、字母与罗马数字标记
v3 的列表实现还内置了 Markdown 的解析与渲染:
markdownTokenName: 'list',parseMarkdown只处理ordered的 token,并把 token 的start、typeMarker(如a、i、I)映射为节点属性;renderMarkdown负责把节点内容按换行递归输出;markdownTokenizer使用 utils.ts 与 roman.ts 中的ORDERED_LIST_ITEM_REGEX等工具处理数字/字母/罗马数字多种标记,并支持带缩进的嵌套列表(buildNestedStructure以首项缩进为基准构造层级);markdownOptions.indentsContent: true指示内容需缩进表示层级。
相关测试参见聚合包测试目录 packages/extension-list/tests,其中 listItemMarkdown.spec.ts 与 orderedList 系列测试共同覆盖了这些行为。
六、工程化变更脉络:从 2.x 到 3.x 的关键节点
变更日志中有多条非版本号条目,勾勒出列表包跨越大版本演进的工程化路线:
| 版本区间 | 变更内容 | 工程含义 |
|---|---|---|
| 3.0.0(Major) | a92f4a6:改用 tsup 构建,不再产出 UMD | 依赖 UMD 产物者需自行重新打包(rollup/esbuild 等) |
| 3.0.0(Major) | 2c911d2:全部列表包并入@tiptap/extension-list,引入ListKit | 前文迁移指南的根源 |
| 3.0.x 系列 | 1b4c82b:使用 pnpm 包别名做版本锁定;89bd9c7:强制 type-only import 以让打包器在生成 dist/index.js 时忽略类型导入;8c69002:beta 与 stable 功能对齐 | monorepo 依赖管理、产物体积与稳定性的内部治理 |
| 2.11.6 | 9abb019:有序列表默认type用null | Schema 扩展更友好 |
| 2.0.0-beta.219 | 36bb1e1:#3540/#3541,列表保留 marks | 促成keepMarks/keepAttributes能力 |
| 2.0.0-beta.210 | f387ad3:新增 prosemirror 依赖解析包 | 统一 PM 依赖版本,避免多版本冲突 |
| 2.0.0-beta.26 | 3d7c8e6:新增itemTypeName选项 | 支持自定义列表项节点名 |
| 2.0.0-beta.17 | 723b955:#1997,input rules 与 paste rules 收归 core | 输入/粘贴能力统一由 core 调度 |
需要留意的是,日志中 v2 段落在版本号上存在明显的分支/回填痕迹:例如2.11.6之后直接出现2.5.8、2.5.x等更早的版本,中间穿插大量 "Version bump only" 的占位条目。这属于多发布线合并时常见的补录现象,不影响功能判断。另外 v2 段中 2024-05 的2.4.0条目记录了 "added jsdocs"(b941eea),即从该版本起为扩展补充了完整的 JSDoc 注释——这正是如今OrderedListOptions各字段都带@default/@example注释的由来。
七、当前仓库中的上手资源
若要在真实环境验证上述行为,仓库内已有可直接参考的实现与示例:
- 有序列表实现全量源码:packages/extension-list/src/ordered-list/ordered-list.ts(选项、属性、命令、快捷键、粘贴、输入规则、Markdown),配套辅助逻辑见同目录 utils.ts 与 roman.ts;
ListKit装配逻辑与每个子扩展的开关语义:packages/extension-list/src/kit/index.ts;- 聚合包导出入口与各扩展文件索引:packages/extension-list/src/index.ts、packages/extension-list/src/ordered-list/index.ts;
- 兼容性重导出包(本文主题包):packages/extension-ordered-list/src/index.ts;
- 行为回归测试:packages/extension-list/tests/orderedListType.spec.ts、packages/extension-list/tests/orderedListPhoneNumber.spec.ts、packages/extension-list/tests/listItemMarkdown.spec.ts、packages/extension-list/tests/listKeymapTab.spec.ts;
- 可运行的前端示例(Vue/React/JSX 三套入口):demos/src/Nodes/OrderedList。
八、小结:升级到 v3 时的三句话结论
- 依赖上:用
@tiptap/extension-list取代@tiptap/extension-ordered-list等六个旧包,并按需通过ListKit.configure({ orderedList: { ... } })统一配置;老包作为兼容层仍可在升级过渡期使用。 - 配置上:
itemTypeName、HTMLAttributes、keepMarks、keepAttributes四个选项的默认值与行为全部可在 ordered-list.ts 中追溯,start/type属性负责承载编号起点与罗马/字母编号风格。 - 行为上:
Ctrl/Cmd+Shift+7、1.输入转换、纯文本粘贴解析、Markdown 双向转换都已内建;若需要 UMD 构建产物,则需自行使用打包器重新封装(v3 已移除 UMD 输出)。
【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考