Joplin HTML 转 Markdown 转换规则解析:下标、上标、下划线与删除线的处理边界

Joplin HTML 转 Markdown 转换规则解析:下标、上标、下划线与删除线的处理边界 Joplin HTML 转 Markdown 转换规则解析下标、上标、下划线与删除线的处理边界【免费下载链接】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 测试夹具 sub_sup_insert_strikethrough.md 及其配套输入 sub_sup_insert_strikethrough.html 为核心剖析 Joplin 官方 HTML 到 Markdown 转换器HtmlToMd底层为 turndown 的 Joplin fork对sub、sup、ins、下划线span和s这几类行内标签的转换规则哪些格式被有意保留为 HTML哪些被转换成了 GFM 扩展语法以及测试框架如何逐字节校验这些行为。读完后你能理解 Joplin 富文本笔记在导入、剪藏clipper场景下格式保真背后的取舍逻辑。一、测试夹具一行期望输出定义了一组转换契约该文档本身是 Joplin HTML 转 Markdown 测试目录下的一个“期望输出”文件。与它同名同目录的.html文件是输入二者共同构成一组“输入 → 期望输出”契约。输入 HTMLsub_sup_insert_strikethrough.htmlXsub1/sub Xsup1/sup insInsert/ins span styletext-decoration: underline;Insert alt/span sStrike/s期望 Markdown 输出即本文档 sub_sup_insert_strikethrough.md 的唯一一行内容Xsub1/sub Xsup1/sup insInsert/ins insInsert alt/ins ~~Strike~~把这组输入输出放在一起五种标签的归宿一目了然输入标签期望输出处理方式sub1/subsub1/sub原样保留为 HTMLsup1/supsup1/sup原样保留为 HTMLinsInsert/insinsInsert/ins原样保留为 HTMLspan styletext-decoration: underline;insInsert alt/ins归一化为inssStrike/s~~Strike~~转换为 GFM 删除线前四项保留为 HTML最后一项转成 Markdown 扩展语法——这个不对称正是 Joplin 转换器的设计意图所在。二、sub/sup有意保留为 HTML 的原因在 turndown fork 的 commonmark-rules.js 中Joplin 为下标和上标显式注册了两条规则rules.superscript { filter: sup, replacement: function (content, node, options) { return sup content /sup } } rules.subscript { filter: sub, replacement: function (content, node, options) { return sub content /sub } }replacement 函数不做任何语法转换只是把内容重新包回原标签效果等价于“透传”。为什么不用~x~之类的下标语法同文件上方 第 104-108 行 的注释给出了明确解释下划线/下标语法并不普及而且~在 GitHub 上恰恰是删除线的语法若把sub转成~...~会造成歧义因此“best to keep it as HTML to avoid any ambiguity”。这与本文档期望输出完全对应输入中的Xsub1/sub Xsup1/sup在输出中原封不动。由于 Joplin 的笔记正文支持行内 HTML这种保留是无损的往返编辑round-trip也不会丢失格式。三、ins与下划线span两条入口同一个归一化出口期望输出中insInsert alt/ins这一段的输入其实是一个带内联样式的span而不是ins标签。这正是 commonmark-rules.js 第 110-129 行rules.insert规则要解决的问题rules.insert { filter: function (node, options) { // TinyMCE represents this either with an INS tag (when pressing the // toolbar button) or using style text-decoration (when using shortcut // CmdU) // // https://github.com/laurent22/joplin/issues/5480 if (node.nodeName INS) return true; if (node.nodeName A ( node.getAttribute(href) || node.getAttribute(name) || node.getAttribute(id) )) return false; return getStyleProp(node, text-decoration) underline; }, replacement: function (content, node, options) { return ins content /ins } }从源码结构看这条规则的 filter 接受两种形态INS标签本身——Joplin 富文本编辑器 TinyMCE 在用户点击工具栏“下划线”按钮时产生text-decoration: underline样式的节点——用户用 CmdU 快捷键时 TinyMCE 产生的形态。规则还专门排除了带href/name/id属性的A标签避免把真正的超链接误判为下划线文本。两种入口最终都归一化为ins内容/ins这也是为什么输入里写的是span styletext-decoration: underline;期望输出却是insInsert alt/ins。值得注意的是Markdown 原生语法中没有“下划线强调”_text_表示斜体所以这里同样选择保留 HTML 而非发明私有语法——这与 Joplin 为高亮注册的mark→内容规则第 96-102 行形成对比mark有自定义语法可用而 insert/sub/sup 没有故保留 HTML。四、s→~~删除线~~GFM 插件接管与前三者不同sStrike/s被转换成了~~Strike~~。该行为来自 GFMGitHub Flavored Markdown插件gfm.js 将 strikethrough 规则 一并启用turndownService.addRule(strikethrough, { filter: [del, s, strike], replacement: function (content) { return ~~ content ~~ } })filter 同时覆盖del、s、strike三种历史标签写法。由于~~是 GFM 删除线的事实标准、无歧义Joplin 的渲染端 renderer 包支持它这里选择“真转换”而非保留 HTML。这也印证了第二节提到的取舍~单波浪线留给被保留为 HTML 的下标语境以避免冲突双波浪线~~则安全地分配给删除线。五、测试框架如何校验这条期望输出以上规则最终由 tests/HtmlToMd.ts 中的集成测试批量验证。该测试的执行机制扫描夹具目录第 10-11 行const basePath ${__dirname}/html_to_md; const files await shim.fsDriver().readDirStats(basePath);对每个.html文件按同名约定找到期望的.md文件第 18-19 行mdPath由filename(htmlFilename) .md推导本例即sub_sup_insert_strikethrough.html对应sub_sup_insert_strikethrough.md。调用转换器并做精确字符串比对第 51-61 行const html await readFile(htmlPath, utf8); let expectedMd await readFile(mdPath, utf8); let actualMd await htmlToMd.parse(div${html}/div, htmlToMdOptions);输入被包在一层div中模拟真实文档片段比对前会对 Windows 的\r\n做归一化。不一致时测试会把“Got / Expected”两栏逐行加引号打印出来第 62-75 行便于定位差异出现在哪一行、哪个字符——对这种逐字节敏感的夹具如本例中ins与~~的混合非常关键。本夹具未命中任何特殊分支anchorNames、preserveImageTagsWithSize、tightLists等选项tests/HtmlToMd.ts 第 25-49 行均按默认值运行因此它验证的就是转换器默认行为下 sub/sup/ins/删除线的契约。转换器本体位于 packages/lib/HtmlToMd.ts。在parse()中可以看到默认参数设置第 23-44 行ATX 标题、fenced 代码块、-项目符号、*/**强调分隔符以及br: 行尾两个空格因为启用软换行时br/需要尾部空格才能在 Markdown 中渲染。随后turndown.use(turndownPluginGfm)注入 GFM 规则即删除线、表格、任务列表再移除script/style节点。本文档期望输出中的~~Strike~~正是这一链路的产物。六、复现与扩展验证该测试随app-cli包的 Jest 套件运行。在 packages/app-cli 目录下执行cd packages/app-cli npx jest tests/HtmlToMd.ts若想手动验证本文的规则结论也可以复用同一入口HtmlToMd是公共导出类joplin/lib/HtmlToMdlib 包入口parse(html, options)接受字符串或 DOM 节点返回 Markdown 字符串。如果将来要为转换器新增标签行为参考本夹具的组织方式即可在 html_to_md 目录 下放一对同名.html/.md文件在 commonmark-rules.jsJoplin 私有格式或 turndown-plugin-gfmGFM 扩展中实现规则集成测试会自动将其纳入全量比对。七、小结这个仅一行的期望输出文件浓缩了 Joplin HTML 转 Markdown 的核心设计哲学无标准 Markdown 语法承载的格式sub、sup、ins及下划线样式→ 归一化后保留为 HTML利用 Joplin 笔记的行内 HTML 支持实现无损往返且刻意规避~与删除线的歧义GFM 已有标准语法的格式s/del/strike删除线→ 转换为~~...~~获得跨平台渲染兼容所有行为由“输入-期望输出”夹具对 精确字符串比对的集成测试锁定任何规则回归都会以逐行差异的形式在测试输出中暴露。理解这条契约也就理解了 Joplin 富文本编辑器TinyMCE 产生的多种等价写法、网页剪藏和 Markdown 导入路径能够格式互通的底层机制。【免费下载链接】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),仅供参考