Joplin 富文本编辑器中的 joplinLists 插件:TinyMCE 列表插件的分支改造与构建流程
2026/9/7 14:11:57 网站建设 项目流程

Joplin 富文本编辑器中的 joplinLists 插件:TinyMCE 列表插件的分支改造与构建流程

【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin

Joplin 桌面端(Electron)在富文本模式下使用 TinyMCE 作为底层编辑器,而其中处理列表缩进、键盘删除与列表切换的lists插件并不直接使用官方原版,而是由 Joplin 仓库内 fork 改造而来的joplinLists插件(注册名为joplinLists)。本文基于仓库文档Assets/TinyMCE/JoplinLists/README.md,完整梳理该插件的来历、构建与调试流程,并结合插件源码(Plugin.tsCommands.tsJoplinListUtil.ts)与 Gruntfile 构建脚本,说明 Joplin 复选框(checklist)在富文本列表体系中的实现方式与产物去向。读完本文,读者将理解该插件如何注册编辑器命令、joplin-checklist类名如何驱动“插入/勾选清单条目”两个自定义命令,以及如何用npm run buildbuildAndStart完成编译与桌面端联调。

插件定位:官方 TinyMCE lists 插件的 Joplin 分支

README 开门见山地说明了该模块的两点关键信息:

  1. 代码来源:它基于 TinyMCE 官方lists插件的某一历史提交(tinymce/tinymce仓库中modules/tinymce/src/plugins/lists路径下的代码),在 Joplin 仓库内以独立目录Assets/TinyMCE/JoplinLists/维护;
  2. 改造目标:在保留官方列表能力的基础上,增加对Joplin 复选框(checklist)的支持——这正是 Joplin 笔记中- [ ]/- [x]语法在富文本编辑器内的表现载体。

从源码结构看,该插件是一个标准的 TinyMCE 插件工程:src/main/ts/下按 TinyMCE 官方插件的典型目录组织划分为actions/(如ToggleList.tsIndendation.ts)、api/Api.tsCommands.tsEvents.tsSettings.ts)、core/Keyboard.tsMouse.tsDelete.tsSplitList.ts等)与listModel/(列表模型层,含ParseLists.tsNormalizeEntries.ts等)。官方插件中的核心逻辑(键盘操作、鼠标拖拽缩进、列表拆分与规范化)被整体保留,Joplin 的改造集中在两处:api/Commands.tslistModel/JoplinListUtil.ts

一个需要特别留意的仓库事实来自 README 顶部的警告:自 2020-11-02 起该模块已无法直接构建(出现大量 TypeScript 报错),且作者表示未查明原因。这意味着当前仓库中的Assets/TinyMCE/JoplinLists/处于“源码在位、但构建链路已断”的状态;桌面端实际使用的插件产物,是此前构建并拷贝进去的编译文件。这一点决定了后文“构建”一节只能作为流程性参考,而非当前可直接跑通的构建脚本。

插件注册机制:joplinLists名称从何而来

入口文件 Main.ts 仅做一件事——调用Plugin()完成副作用式注册,并明确注释“不要导出任何内容,否则 Rollup 会在页面上留下全局变量”:

import Plugin from './Plugin'; Plugin();

Plugin.ts 通过 TinyMCE 的PluginManager.add把插件注册为joplinLists(而非官方lists),这是 Joplin 能够与官方列表插件并存、互不覆盖的关键:

export default function () { PluginManager.add('joplinLists', function (editor) { Keyboard.setup(editor); Mouse.setup(editor); Buttons.register(editor); Commands.register(editor); return Api.get(editor); }); }

注册时依次挂载了四个子系统:键盘快捷键(Tab/Shift+Tab 缩进、Delete/Backspace 删除)、鼠标行为、工具栏按钮与编辑器命令。其中Commands.register(editor)是本文重点,它把官方的InsertUnorderedList/InsertOrderedList/InsertDefinitionList/RemoveList命令桥接到ToggleList.toggleList与缩进动作上,并在末尾调用了 Joplin 的扩展入口addJoplinChecklistCommands(editor, ToggleList)(见 Commands.ts)。

该插件名最终在桌面端编辑器配置中生效。TinyMCE.tsx 在初始化 TinyMCE 时加载本地脚本gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js(约 L382),并将插件列表配置为plugins: 'link joplinLists searchreplace codesample table'(约 L732)。从源码结构看,joplinLists替代了官方lists的位置,成为富文本编辑器中列表行为的唯一提供方。

Joplin 复选框扩展:joplin-checklist类名与两条自定义命令

Joplin 清单在 HTML 中的表示方式是:在无序列表<ul>上添加joplin-checklist类,被勾选的条目<li>上再添加checked类。整个扩展逻辑集中在 JoplinListUtil.ts,核心函数包括:

  • isCheckboxListItem(element):判断某个元素是否带有joplin-checklist类,以此区分“普通列表容器”与“清单容器”;
  • findContainerListTypeFromEvent/findContainerListTypeFromElement:从事件元素或 DOM 节点向上查找最近的UL/OL祖先,返回'joplinChecklist''regular',供点击、键盘等交互判断当前所处列表类型;
  • isJoplinChecklistItem(element):确认某<li>是否属于清单(节点名必须是LI且容器类型为清单)。

在此基础上,addJoplinChecklistCommands(editor, ToggleList)向编辑器注入两条 TinyMCE 自定义命令:

editor.addCommand('ToggleJoplinChecklistItem', function (ui, detail) { const element = detail.element; if (!isJoplinChecklistItem(element)) return; if (!element.classList || !element.classList.contains('checked')) { element.classList.add('checked'); } else { element.classList.remove('checked'); } }); editor.addCommand('InsertJoplinChecklist', function (ui, detail) { detail = { ...detail, listType: 'joplinChecklist' }; ToggleList.toggleList(editor, 'UL', detail); });

两条命令的分工清晰:

命令触发语义实现行为
ToggleJoplinChecklistItem点击/切换已有清单条目的勾选状态校验目标LI确属清单后,对checked类做增删,实现勾选/取消勾选
InsertJoplinChecklist插入一个新的清单复用官方ToggleList.toggleList(editor, 'UL', ...),仅把detail.listType标记为joplinChecklist,使生成的UL带上清单类名

这种“复用官方 toggleList、仅改 detail 参数”的做法,保证了清单的插入路径与普通无序列表共用同一套列表规范化与缩进逻辑,Joplin 的侵入面被控制在Commands.ts的一个注册调用与JoplinListUtil.ts的少量工具函数内。与渲染层的呼应同样可见于 renderer 的复选框样式 与 MdToHtml 的 checkbox 规则:Markdown 侧的- [ ]/- [x]与 HTML 侧的joplin-checklist/checked类名构成一条完整的往返链路。

构建流程:npm i && npm run buildbuildAndStart

回到 README 给出的两条核心命令。对照 package.json,其含义如下:

npm i && npm run build
  • npm i安装该子工程的 devDependencies,关键依赖为 TinyMCE 官方开发链工具:grunt@ephox/swag(TinyMCE 构建工具集)、rollup(经 swag 提供)、ts-loader/awesome-typescript-loadertypescript@^3.1.6webpack@^4.25.1等;
  • npm run build实际执行grunt,即运行 Gruntfile.js 中注册的default任务:clean → shell → rollup → uglify → concat → copy

各步骤职责(依据 Gruntfile.js):

  1. clean:清理distscratch目录;
  2. shell:执行tsc --project tsconfig.json,将src/main/ts编译为lib/Main.js
  3. rollup:把lib/Main.js打包为 IIFE 模块scratch/compiled/joplinLists.js,外部化(external)tinymce/core/api/*一系列模块并映射到tinymce.PluginManagertinymce.util.VK等运行时全局——这保证产物不内嵌 TinyMCE 核心,体积小且不产生双实例;
  4. uglify:压缩生成scratch/compiled/joplinLists.min.js
  5. concat:拼接src/text/license-header.js头注释,替换@BUILD_NUMBER@占位符,产出dist/joplinLists.jsdist/joplinLists.min.js
  6. copy(对 Joplin 而言最关键的一步):将dist/joplinLists.js拷贝到
../../../packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js

即 packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js。这正是上文TinyMCE.tsxsrc字段指向的文件——构建脚本直接完成了“插件产物进入桌面应用”的最后一公里,无需人工搬运。

第二条命令用于插件改动后的端到端联调:

npm run buildAndStart

在 package.json 中其定义为:

"buildAndStart": "yarn build && cd .. && cd .. && cd .. && cd packages/app-desktop && npm start"

先完成上述整套插件构建(含拷贝到 app-desktop),再进入packages/app-desktop执行npm start启动桌面端应用。流程上这是“改插件 → 自动落位 → 起桌面端验证”的闭环,对应 README 所说的 “build the plugin and start the desktop application”。

需要再次强调的前提:由于 README 明确记载 2020-11-02 后该模块出现大量 TypeScript 错误而无法构建,上述流程描述的是仓库中脚本所定义的设计流程;在当前代码状态下直接执行npm run build是否可通过,以实际运行结果为准,仓库并未给出修复说明。

对修改该插件的实操建议

综合以上源码证据,若未来需要再次修改该插件,可以按以下路径推进:

  1. 定位行为归属:列表切换/缩进/命令层改动看 src/main/ts/api/Commands.ts 与 actions/ 目录(ToggleList.tsIndendation.ts);清单专属逻辑集中在 listModel/JoplinListUtil.ts;键盘/删除行为在 core/ 目录(Keyboard.tsDelete.tsSplitList.ts等);
  2. 保持注册名不变Plugin.ts中注册的joplinLists名称与TinyMCE.tsxplugins配置一一对应,改名会直接导致插件失效;
  3. 保持产物路径不变:构建后必须仍落在packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js,否则桌面端加载的是旧产物,调试时会误判“改动未生效”;
  4. 验证闭环:优先用npm run buildAndStart在真实桌面端验证(键盘缩进、清单勾选、跨列表复制粘贴等行为依赖编辑器运行时上下文),单看单元测试无法覆盖 TinyMCE 的 DOM 交互细节——该子工程的test脚本(bedrock-auto)同样依赖 TinyMCE 官方测试框架,且受构建链路阻塞影响,实际可用性需以运行时行为为准;
  5. 注意 TypeScript 版本约束:devDependencies 锁定typescript@^3.1.6webpack@^4.25.1,README 所述“大量 TS 错误”很可能与 TinyMCE 类型定义升级后不兼容有关,重建构建链时应优先对齐这些旧版本约束或使用transpileOnly类策略绕过严格检查(webpack 配置中ts-loader已设置transpileOnly: true即为同类思路的体现)。

小结

Assets/TinyMCE/JoplinLists/是 Joplin 桌面端富文本编辑器列表能力的定制层:它以 TinyMCE 官方lists插件为基座,在 Plugin.ts 中以joplinLists之名注册,在 JoplinListUtil.ts 中用joplin-checklist/checked类名和InsertJoplinChecklistToggleJoplinChecklistItem两条命令扩展出 Joplin 清单能力,并通过 Gruntfile.js 的clean → tsc → rollup → uglify → concat → copy流水线把产物直接投放到packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.jsnpm i && npm run buildnpm run buildAndStart分别对应“纯构建落位”和“构建 + 启动桌面端”两种工作方式。由于仓库文档明确标注该模块自 2020-11-02 起构建已中断,当前仓库中桌面端使用的lists.js属于历史构建产物;理解上述注册、命令与拷贝链路,是后续排查清单行为或重建构建流程的基础。

【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询