Back to Marktext

marktext 老 muya → @muyajs/core 新 muya 迁移追踪表

packages/muya/MIGRATION.md

0.19.136.1 KB
Original Source

marktext 老 muya → @muyajs/core 新 muya 迁移追踪表

调研方案在 /Users/ransixi/.claude/plans/glimmering-hatching-lightning.md。 本表登记 P0~P3 共 ~80+ 条 commit 的迁移状态。

状态字段:

  • pending — 待评估
  • verified-not-applicable — 已验证新架构下 bug 不存在 / 无意义
  • test-only — bug 复现失败,仅添加防御性回归测试
  • fixed — 已实施修复 + 测试
  • skipped — 决定不做(如纯 marktext 应用层)

PR 分组对应方案第三节的 5 个系列 + 后续 PR-6 测试合规:

  • PR-1a 安全 + 已确认 crash(非 XSS) ✅
  • PR-1b XSS 四联(独立 PR 便于安全审计) ✅
  • PR-2 Parser 合规性(含 footnote 测试基线)
  • PR-3 编辑 / 光标 / IME
  • PR-4 Clipboard / 富文本
  • PR-5 P3 体验特性(按需)
  • PR-6 测试合规:选择性迁 marktext muya 测试 + 接 CommonMark/GFM spec(PR-2~4 落地后做)

P0 — 安全 / 数据损坏 / crash

Hash范畴说明PR状态
9884342ftablenormalizeTable 行单元数 > 表头 crashPR-1afixed(含 3 个回归测试)
9ffc5b1bheading空 heading slug crashPR-1averified-not-applicable(新仓无 slugger)
bca2ed62imageloadImageAsync 失败永久缓存PR-1afixed(含 3 个回归测试)
36e825c2imagegetImageInfo 空 firstChildPR-1averified-not-applicable(新仓 getImageInfo 不读 firstChild)
fed1dac4xssHTML 表格粘贴 XSSPR-1bverified-not-applicableutils/paste.tssanitize 经 DOMPurify;anchor title 改用 textContent 比老版更安全)
0dd09cc6xsscode lang + 超链接 XSSPR-1bpartial-fixed:超链接路径 sanitizeHyperlink + htmlTag isValidAttribute 已就位;langInputContent 残留 XSS 此 PR 实修 + 3 测试
c959d185xssMermaid XSSPR-1bverified-not-applicablemarkdownToHtml.ts + diagramPreview.ts 都已 securityLevel:'strict',mermaid innerHTML 走 sanitize
dc54c7b6xss代码块未 escape HTMLPR-1bverified-not-applicableescapeHTML 已用含 & 的 5 字符版本,codeBlockContent 已 escape)+ 4 个防御测试
c47795e4xssXSS + Electron(部分电子相关跳过)PR-1bskipped(Electron-only)
0baf2e9e / 7de33f11xss#1390 XSSPR-13verified-not-applicable:marktext fix 给 inline html renderer 加 BLOCK_TYPE6.includes(tag) || !sanitize('<' + tag + '>') ? 'span' : tag 降级,并把 data-align 入白名单。新仓 inlineRenderer/renderer/htmlTag.ts:80-82 已完整保留该降级链路 + :110 isValidAttribute(tag, attr, val) 属性级过滤;config/index.ts:401 EXPORT_DOMPURIFY_CONFIG.ADD_ATTR: ['data-align'] 保留 data-align 白名单(PREVIEW 走 ALLOW_DATA_ATTR:false 因为预览从 state 重派生 data-align)。新增 16 个防御测试utils/__tests__/dompurifyXss.spec.ts):embed/object/iframe 降级触发、span/code/mark 不降级、href/onclick/onerror 属性过滤、data-align ADD_ATTR + 实际 sanitize 保留
sanitizeHyperlink 防御xss锁住 javascript:/vbscript:/data: 阻断PR-1btest-only(8 个防御测试)
6293d408table-ctrl老 tableBlockCtrl 删行/列后光标修复 (#572)PR-7bfixedTable.removeRow/removeColumn 现返回相邻 cell 的 firstChild,tableRowColumMenu.selectItem 拿到 cursorBlock 后 setCursor(0, 0)新增 8 个回归测试覆盖中间删/末尾删/整表删/越界 4 个分支
f99addd2table-ctrlselectedTableCells 清理 (#1900)PR-7bverified-not-applicable:新仓无 selectedTableCells 全局状态(grep 0 hit);跨 cell 选区在 editor/index.ts:93isSelectionInSameBlock 守卫早 return,不会进入 marktext 旧那条"删整 column 后引用悬空"的代码路径
0a3fda63 + 2754e393 + 4b362e52architecturepost-refactor 修复合集(已拆条)PR-13skipped(已拆 11 子条目登记到下方 "post-refactor 拆条" 节;三个原 hash 不单独迁移)
post-refactor: EventCenter listener 在 destroy 不清理event leakEventCenter.unsubscribeAll() 缺位PR-17fixedevent/index.ts 新增 unsubscribeAll() { this.listeners = {}; }muya.ts destroy()detachAllDomEvents() 之后调用,pub/sub 闭包随实例释放,宿主页面可 GC Muya/plugins/DOM。新增 2 个回归测试:unsubscribeAll 清空已订阅;不影响后续订阅
post-refactor: EventCenter.emit once-listener 迭代变更event correctnessforEachthis.off 跳元素PR-17fixedevent/index.ts emit() 改为 eventListener.slice().forEach(...) snapshot 迭代,once-listener 在回调里 off 只改原数组、不再令前进中的索引塌缩。新增 3 个回归测试:早 once-listener 移除自身后仍触发相邻 listener;多 once 单 emit 全清;once/regular 混合 — regular 多 emit 保留、once 单次后移除
post-refactor: selection document.querySelector vs this.dociframe/multi-docmarktext 改用 this.doc.querySelectorPR-13verified-not-applicable:marktext 改动是为 electron-vite 后的多文档场景;新仓没有 this.doc 字段也无 iframe/shadow-DOM 多 document 基建(selection/index.ts:559/format.ts:441/loadImageAsync.ts:30,77/markdownToHtml.ts:116 一致使用 document.*),结构上不假设多 document
post-refactor: selection/dom.js traverseUp / findOutMostParagraphselection老 contentState 辅助PR-13verified-not-applicable:新仓 selection/dom.ts 无这两个辅助(grep 0 hit),整套 contentState ctrl 已被 OT/JSON-state 替代
post-refactor: history.undo() 在 index 0 崩history访问 stack[-1]PR-13verified-not-applicable:新仓 history/index.ts:77 _change 早期 if (this._stack[source].length === 0) return;,redo/undo 都走同一 _change,无 stack[-1] 风险
post-refactor: MutationObserver 未 disconnectleakinputCtrl observer 泄漏PR-13verified-not-applicable:新仓全代码 0 MutationObserver(grep 0 hit),无 inputCtrl,结构上不存在
post-refactor: historyTimer 未取消leak定时器在 destroy 后 firePR-13verified-not-applicable:新仓 history/index.ts_lastRecorded 时间戳比较,无 setTimeout/Interval(grep 0 hit)
post-refactor: renderCodeBlockTimer 模块级状态leak/racemodule-level 计时器跨实例PR-13verified-not-applicable:新仓 grep 0 hit renderCodeBlockTimer;code-block 渲染走 Prism 同步路径,无延迟渲染计时器
post-refactor: Muya.destroy() 在无 plugins 时崩crash缺少 optional chainPR-13verified-not-applicable:新仓 muya.ts:145-146 destroy()if (this.ui) this.ui.hideAllFloatTools(); 守卫;_uiPlugins 容器在 init() 前就初始化为 {}
post-refactor: 应用层 IPC / preferences / autosave / editor.vueapp-layerelectron-vite/preload/main/rendererskipped:marktext renderer/main 应用层(electron.vite.config.js / src/main/* / src/preload/* / src/renderer/*),非 muya 内核范围
post-refactor: docs (ARCHITECTURE.md, BUILD.md, package.json main)docsmarktext 仓库文档skipped:marktext 仓库 docs / build 配置变更,不进 muya v0.x 包

P1 — Parser / 渲染正确性

Hash范畴说明PR状态
1ecc3601parserfootnote 解析 + 510 行测试基线PR-2afixed(marked v16 block 扩展 + 12 个回归测试;3 个 negative 用例 marked 自带 def 规则替代 paragraph fallback)
23435ce6parser任务列表缩进PR-2atest-only(marked v16 内置 list tokenizer 不共用旧 fork 的缩进 bug;2 个防御测试锁定嵌套)
57cd04c5parserCommonMark example 475 + 353/387/520/521 等PR-2afixed(canOpenEmphasis 阻断 mid-run _;link/reference_link 加 lowerPriority;5 个 CM spec 用例)
ad5ddbf9parserGFM example 558(link/image title 支持)PR-2atest-onlyparseSrcAndTitle 已就位;4 个回归测试锁定 link/image title)
372fe02fparserlist 解析 #870(task + bullet 混排拆分)PR-2atest-onlycompatibleTaskList 已就位;1 个回归测试)
8891287bparserparagraph → list 转换PR-7averified-not-applicable:marktext fix 是 updateCtrl.updateParagraphToList 的 line-splitting 旧逻辑(无 marker 时 listItemLines 为空 → text 丢失)。新仓 replaceBlockByLabel({label:'bullet-list', text}) 直接 state.children[0].children[0].text = text 整段保留,无 LIST_ITEM_REG 分行。新增 6 个回归测试锁住 contract
270d33f6parserlist item lexer/parser(CM 264/265 不同 marker 拆表)PR-2atest-only(marked v16 + compatibleTaskList 已就位;2 个 CM spec 测试)
04834032parsertab 缩进 listPR-7averified-not-applicable:commit title 误导,实际修的是 markup code-block 内 Tab → Emmet HTML 展开(parseSelector(undefined) 崩 + lastWord 未限定到光标前 + postText 丢失)。新仓 codeBlockContent.tabHandler 已含 lastWordBeforeCursor + postText + parseSelector(str='') 三重修复。新增 5 个回归测试(含 mid-text、empty、non-markup 分支)
240d64aaparser合并不同类型 list #706PR-7averified-not-applicable:marktext bug 在 pasteCtrl 合并 list-items 进现存 list 时未对齐 looseness。新仓 pasteHandler (clipboard/index.ts:631-635) 走 for (state of remaining) ScrollPage.loadBlock(state.name).create(...) + insertAfter每个粘贴状态独立成块,不会与既存 list 合并,结构上不存在 looseness 错配
02841ffdparserlist 后续段落归属(exportMarkdown 缩进配置)PR-2btest-only(stateToMarkdown 已实现 indent/listIndent 拆分;4 个 marktext 缩进 fixture + 4 个扩展 round-trip)
5f191681parserblockquote 内 list(exportMarkdown)PR-2btest-only(3 个 blockquote round-trip 测试)
insertLineBreak 行尾空格serializer列表项内空行带尾随空格PR-2bfixedinsertLineBreak 去掉尾随空格,保留 > 前缀;1 个回归测试)
70d49c30parser-foo 误识 list itemPR-2atest-only(marked v16 已要求 bullet 后接空格;2 个正负回归测试)
7b7a9424mathmath block 嵌套PR-7bverified-not-applicable:marktext insertContainerBlock 缺 anchor 校验导致 math 嵌套;新仓所有 container 创建路径(front menu / quick-insert / $$ enter convert)都门控在 paragraph.contentcanTurnIntoMenu 对 math/code/html/diagram/table 返回 []新增 7 个回归测试锁住 front-menu 门
d937fac0inlineinline 语法 (#1071 重复 **\x`**` 只末尾加粗)PR-2ctest-onlylowerPriorityignoreIndex 已就位;2 个回归测试)
57af8304inlinelink/image dest 含括号 (#1169)PR-2ctest-onlycorrectUrlfindClosingBracket 已就位;3 个回归测试)
9c2f6cb3inlineinline math 样式skipped(CSS-only,新仓样式体系自有 inline math 样式)
6dfa7938inlineinline math selectionskipped(CSS-only,新仓样式体系自有 selection 样式)
d9f64babinlinereference link 渲染PR-2atest-only(lexer.ts:357 labels.has(...) 已就位;2 个回归测试)
b8e2cd82inlineinline html rendererPR-13verified-not-applicable:marktext fix 给 marked textRendererscript(content, marker) 让 sup/sub 出现在 HTML 导出。新仓 utils/marked/extensions/superSubscript.ts:59-64 直接在 marked extension renderer 中发射 <sup>...</sup> / <sub>...</sub>,编辑器渲染与 renderToStaticHTML 走同一发射器,无独立 textRenderer 待对齐。新增 2 个 b8e2cd82 防御测试锁住段落 / heading / list 内 sup/sub 同时出现的 HTML 输出
962fdf35inlineheading emoji 偏移skipped(CSS-only,新仓样式体系自有 emoji 处理)
8e32838binline上/下标PR-2atest-onlysuper_sub_script token + 渲染器已就位;3 个正负回归测试)
c0853f64inlineauto link / extensionPR-2atest-only(auto_link + auto_link_extension + 边界 guard 已就位;4 个回归测试)
1c42555ablock粘贴多行进 headingPR-4afixed(提取 mergePasteIntoHeading 纯函数,6 个测试)
dec7502eblocksetext headingPR-2atest-only(marked v16 lheading + walkTokens headingStyle 已就位;3 个回归测试)
f00da152block嵌套块插表 crashPR-7bverified-not-applicable:marktext 老 createFigure 缺 anchor 校验导致 math/code/html/table 内插表崩;新仓 canTurnIntoMenu 同一道门把 table 也挡在外,/table quick-insert 只对 paragraph.content 触发。新增 6 个回归测试复用 canTurnIntoMenu 门同时锁住 table 不可嵌入
9cb2cbe8tocTOC 更新(如做 TOC 参考)PR-15fixed:新仓加 muya.getTOC() 公共 API,对齐 marktext tocCtrl.js(同步 9cb2cbe8 \s 正则修复,让 NBSP/tab 前后置都能正确剥离 atx # 标记)。state/getTOC.ts 委托实现,utils/slug.ts 导出 generateGithubSlug(与 marktext url.js 一致:ASCII \w only)。packages/core/src/index.ts 导出 ITocItem 类型。新增 10 个回归测试:空文档、单 atx、多层级、setext、raw inline 保留、\s 正则修复、CJK/emoji 退化、重复标题 slug 稳定但 githubSlug 同、跨调用 slug 稳定、跳过非 heading 顶层块
reference link/imageparser+statemarkdown 加载时 reference definition 丢失(marked v16 def block token 未处理)+ reference image 不走 loadImageAsync 解析后 src + 内联 label 查找漏大小写PR-16fixedmarkdownToState.ts 新增 case 'def',把 marked v16 提取的 [label]: url "title" block token 还原为 paragraph state(沿用 marktext "definition 是 paragraph text" 模型,引入新 state 节点);loadImageAsync.ts 缓存并返回 resolved url(marktext domsrc 等价),referenceImage.ts 用它作为 `` fallback;lexer.ts 两处 labels.has(...) 调用前 .toLowerCase()(CommonMark §6.5);ILinkReferenceDefinitionState@deprecated(unused,v0.3 移除)。新增 8 个回归测试:def→paragraph 保留、round-trip、inline tokenize、Full/Collapsed/Shortcut 三形式、title 透传、case-insensitive lookup、duplicate label 取首条、orphan ref-link

P2 — 编辑 / 光标 / 选择 / IME

Hash范畴说明PR状态
6f1e733ccursorcodeblock 光标 #2013PR-3averified-not-applicable(旧 bug 根因是 partialRender + singleRender 重渲流程,新仓不存在;codeBlockContent.backspaceHandler 已有 text[start.offset-1] === '\n' 分支显式处理 \n
0a3efbf8selection文本选区 #622PR-3averified-not-applicable(新仓 selection 通过 selection/index.ts:_listenSelectActions 独立监听 mouse 事件,不受 shownFloat 影响)
7936e3f4selection选区无法取消 #636PR-3averified-not-applicablecontent.ts:keydownHandler 已对 shownFloat 内每个浮层细粒度白名单化判定是否 preventDefault
02dbb8afsearch嵌套 block 搜索PR-3dverified-not-applicable(新仓 Search.searchscrollPage.depthFirstTraverse 自然遍历嵌套 block;无 CAN_NEST_RULES 白名单限制)
4c517b16searchsearch groupPR-3dverified-not-applicableutils/search.ts:buildRegexValue 已采用新仓正则 (?<!\\)\$\d + $0=full match 语义;新增 5 个防御测试)
1a4844f8historyundo/redo 不触发 changePR-3dverified-not-applicablehistory._changeeditor.updateContentsjsonState.dispatch 仍 emit json-change;selection 改变由 editor.updateContents 末尾 setCursor 触发 selection-change
16d61572renderpartialRender 光标已移除 blockPR-3averified-not-applicable(新仓 Editor.updateContents 走 ot-json1 + replaceWith 路径,无 partialRender,光标定位通过 setCursor 在已存在 block 上重置)
701fb9aetexttext 删除追加 soft-linePR-3atest-only(旧多段落删除路径不存在;autoPair 内 in-block soft-line 保留分支已就位,2 个防御测试;跨 block 删除依赖浏览器原生行为,需 examples/ 手测)
0dc4b415tablecell backspacePR-3dtest-only(`
X末尾 backspace 旧路径在 contentState 内被特化;新仓走Format.backspaceHandlertoken-based + 浏览器原生删除;建议 examples/ 手测
` 后字符删除)
5fb130d9tableshift+tab 表格导航PR-3dfixedtableCell.tabHandler 新增 event.shiftKey 分支 + previousContentInContext();3 个回归测试)
9e32c4a0tablecursor → next cell index 0PR-3dverified-not-applicabletableCell.tabHandlersetCursor(0, 0, true),不会选中整 cell 文本)
0028a4bctablecell copy 异常PR-7averified-not-applicable:marktext fix 是 paragraphCtrl.selectTableCells 单 cell descriptor 缺 text 字段。新仓无 selected-cells descriptor —— getClipboardData 同块分支直读 anchorBlock.text.substring(begin, end)clipboard/index.ts:133)。新增 3 个回归测试锁住 table.cell.content 单块 copy 路径
3fa8a9aeautopairinline code 内禁用PR-3bverified-not-applicablecontent.ts:autoPair 已有 isInInlineCode 参数 + format.ts 调用前用 _checkCursorInTokenType 计算;defensive 测试 2 个)
4278362fautopairinline math 内禁用PR-3bverified-not-applicable(同上,isInInlineMath 参数已就位;defensive 测试 1 个)
bbea7ecaautopair优化自动补全PR-3bverified-not-applicable!/[a-z0-9]/i.test(preInputChar) 已在 markdown-syntax 分支;defensive 测试 3 个)
358fa83dautopair引号自动配对PR-3bfixedcontent.ts:autoPairpostIsNotTouching 门控,5 个回归测试)
6a50b5cbtasklist切换 task-list 光标跳末尾PR-3dverified-not-applicabletaskListCheckbox click 已 event.stopPropagation(),不会触发 editor click → cursor restore;建议 examples/ 手测确认体感 OK)
c3f128e7tasklistcopy 保留勾选态PR-4bverified-not-applicable:marktext fix 是其 DOM-based copy 的 checkbox 注入边界,新仓走 marked 渲染(task-list [x]/[ ]<input checked>),渲染层一致
edbab6edime中文输入误删PR-3cverified-not-applicable(Ctrl+A 在新仓走浏览器原生,多 block 选区被 editor.ts 提前 return,format.inputHandler 期初也 if (isComposed) return;跨 block + IME 边角仍建议 examples/ 手测)
67e18176ime中文回车多行PR-3cverified-not-applicablecontent.ts:autoPair 软换行补齐分支已包含 event.type === 'compositionend' 条件,新增 1 个 compositionend 防御测试)
8a7e6559imecompose bugPR-3cverified-not-applicablecomposeHandler 翻转 isComposedkeyupHandler / inputHandler / Enter+Arrow 都已用 !this.isComposed 门控)
63642d39typing回车 typewriter 抖动PR-3dverified-not-applicable(新仓无 typewriter 模式,scrollIntoView 抖动场景不存在)
6b3ead95keyboard非 US 键盘PR-3dskipped(marktext 应用层 renderer keybindings 设置页,非 muya 内核)
ed1b3354keyboard图片选中按 deletePR-3dfixedselection/index.ts:_listenSelectActions/Backspace|Enter/ 替换为 /^(?:Backspace|Delete|Enter)$/,覆盖 Delete 键 + 锁住完整匹配避免子串碰撞;clipboard 路径无 selectedImage 副作用,无需同步修复)
b925f7d6clipboard移动端 cutPR-4bverified-not-applicable:新仓 cutHandler 起手即 if (selection == null) return;,等价 marktext 的 `if (!start
393139e5clipboardclipboard 过度 sanitizePR-4bverified-not-applicable:新仓 getClipboardData 单块/多块路径都 text = substring(...)/mdGenerator.generate(...) 直出,无 escapeHtml;含 2 个防御测试
54a3b585clipboard粘贴 HTML escapePR-4averified-not-applicableutils/paste.tssanitize(html, PREVIEW_DOMPURIFY_CONFIG, false)
485fcfe0clipboardimage paste handler 不执行PR-4averified-not-applicable:新仓 pasteHandler 无 image paste 路径;进入 paste handler 后不会因 !text && !html 早退
5b1cd85dclipboard末尾 html block 粘贴错误PR-13verified-not-applicable:marktext 老 pasteCtrlgetLastBlock(blocks) 在 fragment 树中递归找末叶并写 lastBlock.text += cacheText;如果末块是 editable === false 的 html-block,递归会进入 children 取错节点或崩。新仓 clipboard/index.ts:631-649 多段粘贴是 for (state of remaining) → ScrollPage.loadBlock(state.name).create(...) + insertAfter,结尾用 wrapperBlock.firstContentInDescendant() 取光标块(block/base/parent.ts:251-258,沿 children.head 向下找 Content 叶;html-block→html-container→code 是规则结构,永远命中一个可写 leaf)。无 fragment 末块的 cacheText 追加路径,结构上不触发 marktext bug
fb8fca7bclipboardcopy/paste listPR-4bverified-not-applicable:turndown paragraph/listItem 规则已在 utils/turndownService/index.ts;checkbox 注入是 marktext DOM-based copy 特有,新仓走 marked 渲染不需要
067ec485clipboardHTML paste handlerPR-4apartial-fixed:text-only <table>...</table> 现在升级到 html 槽走 HtmlToMarkdown;recursion 与 pasteImage 分支新架构不适用(无 pasteImage)
ef59a743clipboard富文本复制PR-4bverified-not-applicable:copyHandler 'normal' 已 setData('text/html', html); setData('text/plain', text)getClipBoardHtml 经 marked 渲染
c841facdclipboard空内容不写剪贴板PR-4bfixed(含 6 个回归测试)

P3 — 体验特性(PR-5 按需)

Hash类型说明状态
7377de3cfeatfootnote 完整链路PR-8a fixed(block class + 注册 + 嵌套子树解析;6 个测试)
ab97336efeathighlight 菜单PR-9 test-only<mark> 已在 inlineFormatToolbar/config.tstype: 'mark' + 快捷键 ⇧+Cmd+H;3 个防御测试锁住 7 个核心 inline-format type + icon 不被回退)
1ef0d016featlinkTools unlink/jumpPR-9 test-only(subscriber + selectItem dispatcher 已就位但 muya-link-tools 暂无 emitter;删 @ts-nocheck 补类型 + 2 个防御测试锁住 unlink / jump 分支)
cb25b3d4featlinkTools 支持 <a> 与 ref linkPR-11b fixed(渲染端 link.ts / referenceLink.ts / htmlTag.ts 早已带 dataset.{start,end,raw} + mu-link / mu-reference-link / mu-raw-html class,PR-9 也铺好了 linkTools 浮层订阅器;本 PR 补齐缺失的发射端——新增 editor/linkMouseEvents.ts 把 mouseover/mouseout 上的三种 link wrapper 都派发到 muya-link-tools(markdown / 引用链接需上一个兄弟节点 .mu-hide 处于预览态,HTML <a> 永久触发;mouseout 用 relatedTarget 守门,鼠标在 wrapper 内部移动不会误隐藏);新增纯函数 utils/getLinkInfo.ts 兼容读 <a href> 属性与 <span> snabbdom props.href 自定义 DOM 属性,data-start/data-endNumber.isFinite 守 NaN;examples 接 LinkTools 插件并加 jumpClick 回调;19 个红→绿测试 = 11 个 getLinkInfo 单测 + 8 个 mouse dispatch DOM 测试)
141d25d8feat粘贴链接抓页面标题PR-4c
d26f5092featimage resize + inline/block 切换PR-11a
cb7be189featinline image / small imagePR-11a
9eff8248featfocus / blur 事件PR-10a
8474a997featprism 语言别名verified-not-applicable(新仓 packages/core/src/utils/prism/index.ts:21-36,47 fuse 已含 alias key + loadLanguage.ts:24-55 transformAliasToOrigin 已实现)
8af9605efeatcode block Solidity 等语言verified-not-applicable(新仓 packages/core/src/utils/prism/loadLanguage.ts 已动态读 prismjs/components.js,删掉了上游那张白名单 JSON,Solidity 等天然可用)
47cb2bbefeatindent code blockverified-not-applicable(上游核心是"4-space 自动转换"路径,新仓 packages/core/src/block/base/format.ts:53 regex ^( {4,}) + _convertToIndentedCodeBlock() (:1163-1211) 已实现;UI 上无显式入口,但上游也没有)
7aa0d1bffeatcode block 复制按钮verified-not-applicable(新仓 packages/core/src/block/commonMark/codeBlock/code.ts:15-41 renderCopyButton + :88-101 点击经 editor.clipboard.copy('copyCodeContent', ...).mu-code-copy 样式见 blockSyntax.css
a028a7c2featcode block 行号PR-5a
ef9fe756featunderline 格式PR-9 test-only<u> 已在 inlineFormatToolbar/config.tstype: 'u' + 快捷键 Cmd+U;由 ab97336e 同一份 7-type 防御测试覆盖)
81af43befeatquick insert hint 隐藏PR-9 test-onlymuya.ts::getContainer 已读 options.hideQuickInsertHint 控制 mu-show-quick-insert-hint class;3 个防御测试锁住 默认 / false / true 三态)
c0c8ea4bfeat打开外链 / 本地 mdPR-12 skipped(Electron shell.openExternal 路径,跨 SDK 边界,应用层做)
afe68891featSM.MS 上传删除链接PR-12 skipped(uploader 专属,新仓不集成 uploader)
435dca74featUnsplash 搜图PR-12 skipped(网络依赖 + API key + UI 重大改动,对纯 markdown 库过重)
f3b53427feat跳光标到末尾再格式化PR-10b fixedformat.ts::_addFormat 在 paired marker (strong/em/inline_code/del/inline_math) 和 tag marker (u/sub/sup/mark) 分支按 wasCollapsed 分流:非空选区光标跳到闭合标记之后,单点光标保留在 marker 之间(toggle-then-type);link/image 保持原"光标落在 () 之间"行为;17 个单元测试覆盖每种 marker + 偏移 + collapsed 回归)
efd38644feat长 footnote 编号PR-8c fixed(renderToStaticHTML 收集 + inline 编号 sup + <section class="footnotes"> 反链;6 个测试)
318bfc6a / fc89d04a / 37b96c88featfootnote 系列PR-8b fixed(footnoteTool TS 重写 + click 接线 + Create/Go to;4 个测试)

P4 — 明确不迁

  • marktext 应用层(Electron / preferences / IPC / 文件系统 / theme / print / 键位设置 UI)
  • 老 muya CSS 主题
  • 已被新架构结构性解决的 partialRender / contentState ctrl 系列
  • 纯 lint / 格式化 / 依赖升级
  • marktext i18n 文案

PR-6 — 测试合规迁移(PR-2~4 落地后做)

PR-6a 已落地(2026-05-20):CommonMark 0.31 + GFM 0.29-gfm spec 合规基础设施。

PR-6a 交付

  • 新增公开同步 API:renderToStaticHTML(markdown, options?),18 个单元测试(happy-path + DOMPurify XSS 处理 + 全部 5 选项覆盖 + mermaid/diagram 占位 + sanitize: false 关闭路径)
  • commonmark-spec@^0.31.2 devDep(652 个 example)
  • 自解析 GFM spec:packages/core/test/spec/fixtures/gfm-spec-0.29-gfm.json(672 个 example,含 5 个 GFM extension section)
  • spec runner:test/spec/runner.ts(normalizeHtml 折叠 cosmetic 空白 + 防回归 expected-failures 锁)+ 8 个 normalizer 单测
  • 两份 spec 测试:commonmark.spec.ts + gfm.spec.ts,每 example 一条 it.each,共 1324 测试,全部锁定通过
  • 修复 getHighlightHtmlfootnote 选项未连线的 no-op bug
  • baseline 报告:test/spec/conformance.md(按 section 拆 pass-rate)
  • expected-failures.json:CM 80 个 + GFM 92 个待修 example_id
  • vitest 配置拆分:默认 pnpm test 仅跑 unit;新增 pnpm test:spec / test:spec:commonmark / test:spec:gfm,独立 vitest.spec.config.ts
  • CI:.github/workflows/ci-test.yml 增加 pnpm test:spec 步骤

PR-6a baseline 通过率

Suite通过总数通过率
CommonMark 0.3157265287.7%
GFM 0.29-gfm58067286.3%

合并后通过率只能涨不能跌:expected-failures.json 中的 example 若开始通过,测试会以 "unexpected pass" 报错,要求 reviewer 把它从列表里移除。

Spec runner 用 renderToStaticHTML(..., { sanitize: false }) 跑——衡量的是 parser 的合规度,不是 DOMPurify sanitizer 行为(sanitizer 该激进就激进,spec 的 Raw HTML allowance example 会被它合法地剥离)。DOMPurify sanitize 行为由 renderToStaticHTML 默认 sanitize: true 单元测试覆盖。

normalizeHtml 规范化:兼属性名排序、self-closing void 标签统一、相邻 tag 间空白折叠(>(WS)<><)、void 标签后空白剥离。<pre>/<code> 内容(如行末 \n)保留——因为 collapse 只匹配纯空白 token 间隔,content 字符不动。

PR-6b 交付(2026-05-20)

完成 marktext 测试补齐三件套:

  • footnote 510 行补齐utils/marked/extensions/__tests__/footnote.spec.ts 从 13 → 21 个测试,新增 8 个 multi-line body 场景(next-line / next-paragraph / 多段落 body / 嵌套 list / 嵌套 code block / 终止于非缩进段落 / 终止于不足 4-space 缩进)。顺手修了 footnote.ts cleanup 路径的小 bug:第一行 4-space 缩进未剥离导致 multi-line body 被误识别成 indented code block(解决方案:cleanup 加一道 ^ {4} strip)。
  • markdown-basic round-trip:新增 test/spec/roundTrip.spec.ts + 11 个 marktext fixture(test/spec/fixtures/marktext-round-trip/{common,gfm}/)。15 个测试(11 stability + 4 strict identity round-trip)。
  • list-indentation 5 个策略state/__tests__/listSerialization.spec.ts 原 11 个 + 新增 dfm (Daring Fireball) 策略 → 12 个。

合计 PR-6b 新增 24 个测试,1 个 footnote parser bug fix。

PR-6b 待办(旧;保留为后续追踪参考)

目标:补足"非 bug regression"的测试覆盖。PR-2~5 的每条 fix 都自带回归测试,但 happy-path、合规性、广覆盖的测试目前稀疏。

值得迁(高信号)

  • marktext test/unit/specs/parser/marked 系列的 lexer / tokenizer 单元测试 —— 对应新仓 state/markdownToState.ts + inlineRenderer/lexer/,纯输入→输出,架构无关
  • markdown ↔ state ↔ html 的 round-trip 测试
  • footnote / table / list / emoji / math 块和内联的 happy path 测试(区别于 bug regression)

值得替代(不搬 marktext 的,直接接上游)

  • CommonMark 0.31 合规:把 spec.json 接进 vitest 驱动 markdownToHtml。约 670 个 example,比 marktext 自带合规测试更广更权威
  • GFM 合规:用 GFM spec example list 同样做法

可接受 fail rate 阶梯:初期允许 5%(先有 baseline 看见缺口),逐步降到 1%;按 spec section 拆分单独跟踪。

不迁

  • 针对 ContentState.prototype.* / 旧 ctrl 方法的行为测试(API 不存在)
  • 光标位置 / DOM 交互 / partialRender 的实现细节测试(OT 架构后机制完全不同)

实施

  • 拆 PR-6a(marktext 选择性测试搬运)+ PR-6b(CommonMark/GFM spec 集成 + baseline 报告)
  • 单独的 vitest project 配置(test:spec 命令),与现有 unit test 分开,可独立看通过率
  • 在 CI 加 spec compliance 趋势报告(每次 PR 不要求 100%,但不能下降)

进度统计

PR计划条数已完成占比
PR-1a6467%(2 fixed + 2 verified-not-applicable,2 转 PR-3)
PR-1b7686%(1 fixed + 4 verified-not-applicable + 1 skipped;防御测试 15 个)
PR-2261558%(PR-2a 8 commits = 2 fixed bugs + 9 test-only;PR-2b 3 commits = 1 fixed bug + 2 test-only;PR-2c 1 commit = 2 test-only + 3 skipped;3 条转 PR-3/PR-4;新增 57af8304 入册)
PR-3a55100%(4 verified-not-applicable + 1 test-only;防御测试 2 个 soft-line)
PR-3b44100%(1 fixed 358fa83d + 3 verified-not-applicable;回归测试 13 个)
PR-3c33100%(3 verified-not-applicable;+1 compositionend 防御测试;跨 block+IME 留 examples/ 手测)
PR-3d1111100%(2 fixed 5fb130d9+ed1b3354 + 6 verified-not-applicable + 2 test-only + 1 转 PR-4;回归测试 8 个)
PR-4a (粘贴)55100%(2 fixed + 3 verified-not-applicable;5b1cd85d 末尾 html-block 经 PR-13 代码路径验证)
PR-4b (复制)77100%(1 fixed + 6 verified-not-applicable;防御测试 8 个)
PR-4c (P3 抓标题)11100%(fixed,5 个测试)
PR-518+528%(PR-5a 1 fixed a028a7c2(行号,10 个测试)+ 4 verified-not-applicable:8474a997/8af9605e/47cb2bbe/7aa0d1bf
PR-5a (P3 code block 行号)11100%(fixed,10 个测试)
PR-6adonespec 合规基础设施落地(CM 572/652 = 87.7%, GFM 580/672 = 86.3%,1324 spec 测试 + 8 normalizer 单测 + 18 个 renderToStaticHTML 单测)
PR-6bdonemarktext 测试补齐落地(footnote 8 个新测试 + 1 个 parser fix;round-trip 15 个测试 + 11 fixture;list-indent +1 dfm 策略)
PR-13 (residuals)5480%(4 项 A 组遗留收尾:3 verified-not-applicable b8e2cd82/5b1cd85d/0baf2e9e+7de33f11 + 1 skipped (已拆条) 0a3fda63+2754e393+4b362e529cb2cbe8 原计入此 PR 为 skipped,已转入 PR-15 处理;新增 18 个防御测试: 2 sup/sub HTML + 16 DOMPurify XSS;post-refactor 拆出 11 个子条目: 2 pending + 6 verified-not-applicable + 3 skipped 应用层/docs)
PR-15 (TOC)11100%(1 fixed 9cb2cbe8:新增 muya.getTOC() 公共 API + generateGithubSlug helper + ITocItem 类型导出;10 个回归测试)
PR-16 (reference link/image)11100%(fixed:markdownToStatecase 'def' + loadImageAsync 返回 resolved url + referenceImage 用它 + lexer.ts label lookup 大小写规范化 + ILinkReferenceDefinitionState@deprecated;8 个回归测试 + examples demo)
PR-17 (EventCenter fixes)22100%(2 fixed:unsubscribeAll() + muya.destroy() 调用消除 pub/sub 闭包泄漏;emit() .slice() snapshot 修复 once-listener 迭代索引塌缩;5 个回归测试)

最后更新:2026-05-21(PR-17 落地:EventCenter listener 泄漏 + once-listener 迭代变更修复,5 个回归测试,全套 386 测试通过)