Etherpad标题插件ep_headings2:从安装到二次开发的完整指南

发布时间:2026/10/21 22:24:21
Etherpad标题插件ep_headings2:从安装到二次开发的完整指南
简介ep_headings2 是一款面向 Etherpad 协作编辑平台的标题插件使用 JavaScript 编写主要解决在焊盘pad中应用 h1 等标题样式的问题适合需要为 Etherpad 扩展富文本标题能力的开发者与运维人员。资源包共 54 个文件以 38 个 json 语言包、5 个 js 脚本、3 个 yml 配置为主另含 md 文档、css 样式、ejs 模板与 png 截图等压缩后约 86KB结构紧凑。插件具备测试覆盖率、代码检查、i18n 多语言翻译、导入导出与复制粘贴支持并可显示活动标题由 Etherpad 基金会维护版权归 ep_headings2 作者及贡献者所有。目前已有 290 人学习下载。通过该资源读者可获取完整的插件源码与多语言配置理解 Etherpad 插件在标题渲染、国际化及导入导出链路中的实现方式并可直接用于二次开发或集成部署。1. 协作编辑器里的标题插件为什么一个“#”能决定文档能不能用如果你维护过 Etherpad 实例大概遇到过这种场景团队用它做会议纪要、需求草稿、接口文档写着写着有人抱怨“这编辑器怎么连标题都没有”。Etherpad 默认只认加粗、斜体、列表你敲# 一级标题它原样显示导出 HTML 后整篇文档没有h1扔进知识库或者转 PDF 时层级全乱。ep_headings2就是补这个缺口的插件——它给 Etherpad 的工具栏加一组标题按钮把选中的行标记成 H1 到 H6同时保证多人同时编辑时标题属性不打架。适合两类人一是自建 Etherpad 做内部协作、需要结构化输出的团队二是想学 Etherpad 插件机制、拿它当模板改造成自定义格式插件的开发者。它不解决排版美观问题只解决“文档有没有语义层级”这个底层问题。2. 插件架构拆解从工具栏按钮到 OT 属性的完整链路2.1 Etherpad 插件体系里 ep_headings2 的位置Etherpad 的插件不是外挂脚本而是通过ep_plugin机制注册到服务端和客户端两侧。服务端用hooks挂载客户端用client_hooks挂载。ep_headings2属于“内容格式类”插件和ep_font_size、ep_align是同一类核心工作是往 Etherpad 的富文本属性模型里塞一个heading属性。Etherpad 的文档模型是纯文本加属性区间。你看到的加粗底层是[[0,5,bold,true]]这样的 attribute pool 记录。标题同理ep_headings2注册的属性名是heading取值h1到h6。这个设计决定了它的行为边界标题是行级属性不是块级元素所以它不会像 Word 那样把整段包进h1标签而是给行首到行尾的字符打上headingh1标记导出时再由导出器翻译成h1。理解这一点很关键因为后面所有“为什么标题按钮点了没反应”“为什么导出后标题丢了”的问题根源都在属性有没有正确落到 attribute pool 里。2.2 目录结构与关键文件职责插件安装后在node_modules/ep_headings2/下核心文件不多但每个都有明确分工ep_headings2/ ├── index.js # 服务端入口注册 hooks ├── static/ │ ├── js/ │ │ ├── index.js # 客户端入口注册按钮和事件 │ │ └── headings.js # 标题按钮的具体逻辑 │ └── css/ │ └── headings.css # 工具栏按钮样式 ├── templates/ │ └── headings.ejs # 按钮的 HTML 模板 └── package.jsonindex.js是服务端钩子注册点主要挂eejsBlock_editbarMenuLeft把按钮模板注入编辑栏以及exportHtmlAdditionalTags让导出器认识heading属性。static/js/headings.js是客户端核心处理按钮点击、调用pad.setAttributeOnLine设置标题。templates/headings.ejs定义按钮的 DOM 结构Etherpad 的编辑栏是动态渲染的按钮必须通过模板注入而不是硬写 HTML。常见做法是先用npm install ep_headings2装到 Etherpad 根目录再在settings.json的plugins里确认启用。如果你是从源码改直接把仓库 clone 到node_modules下再改static/js/headings.js改完重启 Etherpad 即可客户端资源会重新打包。2.3 标题属性的注册与 OT 兼容处理Etherpad 用 OTOperational Transformation做协同任何新属性都必须让 OT 引擎认识否则多人编辑时会报unknown attribute或者属性被静默丢弃。ep_headings2在服务端index.js里通过padUpdate钩子确保heading属性被注册到 attribute pool。// index.js 服务端关键片段 exports.eejsBlock_editbarMenuLeft function (hook_name, args, cb) { // 把标题按钮模板注入编辑栏左侧 args.content eejs.require(ep_headings2/templates/headings.ejs); return cb(); }; exports.exportHtmlAdditionalTags function (hook_name, pad, cb) { // 告诉 HTML 导出器heading 属性要转成 h1-h6 标签 cb([heading]); };eejsBlock_editbarMenuLeft是 Etherpad 提供的编辑栏注入点args.content是当前编辑栏 HTML 字符串追加模板内容即可。exportHtmlAdditionalTags返回一个属性名数组导出器遇到这些属性时会调用对应的转换逻辑。注意这里只返回heading具体 h1 还是 h2 由属性值决定导出器内部会做映射。参数说明hook_name是钩子名Etherpad 自动传入args包含当前渲染上下文cb是回调必须调用否则钩子链会断。这三个参数是 Etherpad 服务端钩子的标准签名写其他插件也照这个来。2.4 客户端按钮事件与 setAttributeOnLine 调用客户端逻辑在static/js/headings.js核心是监听按钮点击拿到当前光标所在行调用pad.setAttributeOnLine设置或取消标题。// static/js/headings.js 核心逻辑 function applyHeading(level) { var rep pad.getRep(); var lineNumber rep.selStart[0]; // 当前光标所在行号 var currentHeading rep.lines.atIndex(lineNumber).heading; if (currentHeading level) { // 再次点击同一级别则取消标题 pad.setAttributeOnLine(lineNumber, heading, ); } else { pad.setAttributeOnLine(lineNumber, heading, level); } } // 绑定按钮事件 $(#heading-h1).on(click, function () { applyHeading(h1); }); $(#heading-h2).on(click, function () { applyHeading(h2); });pad.getRep()拿到当前编辑器的 representation 对象selStart[0]是光标行号。rep.lines.atIndex(lineNumber).heading读取该行已有的 heading 属性值用来做 toggle 判断。pad.setAttributeOnLine是 Etherpad 客户端 API第一个参数行号第二个属性名第三个属性值传空字符串表示移除。这里有个容易翻车的点selStart在选区跨行时只取起始行如果用户选中多行再点标题只有第一行生效。这是插件当前实现的限制不是 bug。要支持多行需要遍历selStart[0]到selEnd[0]逐行设置改起来不难但要注意 OT 操作合并时的顺序。3. 跑起来从零安装到验证标题生效的完整步骤3.1 环境准备与 Etherpad 基础部署ep_headings2不能独立运行必须先有一个能跑的 Etherpad。Etherpad 依赖 Node.js 和 npm版本要求随 Etherpad 主版本变化常见做法是用 Node 16 或 18太新的 Node 20 在某些老版本 Etherpad 上会有依赖编译问题。# 1. 克隆 Etherpad 主程序 git clone https://github.com/ether/etherpad-lite.git cd etherpad-lite # 2. 安装依赖这一步会编译 sqlite3 等原生模块耗时较长 npm install # 3. 启动 Etherpad默认端口 9001 npm start启动后浏览器打开http://localhost:9001能看到默认编辑器界面说明基础环境 OK。如果npm install卡在 node-gyp 编译检查系统有没有装 python3 和 make这是原生模块编译的常见依赖。提示Etherpad 默认用 SQLite 存数据文件在var/dirty.db。开发阶段够用生产环境建议换 MySQL 或 PostgreSQL改settings.json里的dbType和连接串即可。3.2 安装 ep_headings2 并启用Etherpad 插件安装有两种方式从 npm 装或者手动放到node_modules。开发调试推荐手动放改完直接重启不用反复npm install。# 方式一npm 安装 npm install ep_headings2 # 方式二手动 clone 到 node_modules适合改源码 cd node_modules git clone https://github.com/ether/ep_headings2.git cd ..装完后编辑settings.json确认plugins字段里包含ep_headings2。Etherpad 默认会自动加载node_modules下所有ep_开头的插件但显式写进配置更稳妥。{ plugins: { ep_headings2: { enabled: true } } }改完配置必须重启 Etherpad插件在启动时注册钩子热重载不生效。重启后刷新编辑器页面工具栏左侧应该出现 H1 到 H6 的按钮。3.3 验证标题功能与导出结果点一下 H1 按钮当前行应该变大加粗。再点一次取消。多人协同验证开两个浏览器窗口进同一个 pad一个窗口设标题另一个窗口应该实时同步。导出验证更关键因为标题的最终价值在导出。点导出 HTML查看源码里有没有h1标签。# 用 curl 直接拉导出接口方便脚本验证 curl http://localhost:9001/p/你的pad名/export/html | grep -o h[1-6]如果导出结果里没有h1说明exportHtmlAdditionalTags钩子没生效检查index.js是否被正确加载。常见原因是插件目录名和package.json里的name不一致Etherpad 按package.json的 name 注册钩子。3.4 常见配置项与参数调整ep_headings2本身配置项不多但有几个地方可以按需改配置位置参数作用默认值static/js/headings.jsapplyHeading的 level 参数控制按钮对应级别h1-h6templates/headings.ejs按钮 DOM 结构增删按钮或改图标6 个按钮static/css/headings.css按钮样式调整工具栏外观跟随主题index.jsexportHtmlAdditionalTags返回值控制导出映射[heading]想只保留 H1-H3改headings.ejs删掉后三个按钮同时headings.js里对应的事件绑定也删掉。想改导出标签比如把 h1 映射成h1 classtitle需要改 Etherpad 的导出器逻辑插件层面只能控制属性名控制不了最终标签结构。4. 避坑与排查标题插件最容易翻车的五个场景4.1 按钮点了没反应控制台报 pad 未定义现象工具栏按钮渲染出来了点击后控制台报pad is not defined或Cannot read property getRep of undefined。原因headings.js在 Etherpad 的pad全局对象初始化之前执行了。Etherpad 客户端脚本加载有顺序插件脚本如果没等padready 就绑定事件拿不到对象。解决把事件绑定包在pad.getPadId()可用之后或者监听 Etherpad 的pad_ready事件。常见做法是用setTimeout延迟绑定但更稳的是用client_hooks的aceInitialized钩子。// 正确做法等 ace 初始化完成再绑定 exports.aceInitialized function (hook, context) { var pad context.pad; $(#heading-h1).on(click, function () { applyHeading(pad, h1); }); };4.2 多人编辑时标题属性丢失现象A 设了标题B 同时编辑同一行保存后标题没了。原因OT 操作冲突。如果插件设置属性的操作没有正确走pad.setAttributeOnLine的 OT 通道而是直接改 DOM协同时会以最后写入的为准属性被覆盖。解决所有属性修改必须走pad.setAttributeOnLine或pad.setAttribute不要直接操作 DOM。Etherpad 的 OT 引擎会处理属性操作的合并前提是操作从正确入口进来。4.3 导出 HTML 没有标题标签现象编辑器里标题显示正常导出 HTML 后全是p。原因exportHtmlAdditionalTags钩子没注册成功或者返回的属性名和实际设置的不一致。比如设置时用了heading导出钩子返回[headings]对不上。解决检查index.js里exportHtmlAdditionalTags的返回值确保和setAttributeOnLine的第二个参数完全一致。改完重启 Etherpad导出器在启动时读取钩子。4.4 标题按钮样式错乱或位置不对现象按钮出现在工具栏但样式全无或者挤到其他按钮中间。原因headings.css没被加载或者headings.ejs的 DOM 结构不符合 Etherpad 编辑栏的 CSS 约定。Etherpad 编辑栏按钮有固定的 class 命名规范比如buttonicon、editbar-button。解决参考 Etherpad 自带按钮的 HTML 结构改headings.ejsclass 名对齐。CSS 文件确认在static/css/下且被index.js的eejsBlock_styles钩子引入。4.5 升级 Etherpad 后插件失效现象Etherpad 从 1.x 升到 2.x标题按钮消失或报错。原因Etherpad 大版本升级会改钩子名或 API 签名。比如eejsBlock_editbarMenuLeft在某些版本里改成了eejsBlock_editbarMenuLeft的参数结构变化。解决对照目标 Etherpad 版本的插件开发文档检查index.js里所有钩子名和参数。ep_headings2的 GitHub 仓库通常有版本兼容说明升级前先看 changelog。5. 二次开发把标题插件改造成自定义格式插件的通用套路ep_headings2最大的价值不是标题本身而是它提供了一个“往 Etherpad 加行级属性”的最小完整模板。你把它拆透之后加下划线、加高亮、加自定义标记都是同一套流程。我一般按四步走注册属性、注入按钮、绑定事件、导出映射。第一步服务端index.js里加钩子。假设你要加一个highlight属性先复制exportHtmlAdditionalTags改成返回[highlight]再在eejsBlock_editbarMenuLeft里注入新按钮模板。第二步客户端headings.js复制一份改名为highlight.js把applyHeading里的heading换成highlight级别参数换成颜色值。pad.setAttributeOnLine(lineNumber, highlight, yellow)这一行是核心属性名和值都从这里进 OT 通道。第三步导出映射。Etherpad 的 HTML 导出器对未知属性默认忽略你需要在服务端加一个exportHtmlAdditionalTagsWithData钩子把highlight映射成span stylebackground:yellow。这个钩子比exportHtmlAdditionalTags更灵活能带数据。// 自定义属性导出映射示例 exports.exportHtmlAdditionalTagsWithData function (hook_name, pad, cb) { cb([highlight]); }; // 导出器内部会把 highlightyellow 转成 span style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />