尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Readest 校对替换规则失效排查:Selection 作用域规则的 Section 身份锚定修复(Issue 6148 深度解析)

Readest 校对替换规则失效排查:Selection 作用域规则的 Section 身份锚定修复(Issue 6148 深度解析) 桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载本文围绕 Readest 阅读器校对Proofread替换功能中一个棘手的回归问题展开当用户为一段选中的文本创建selection作用域的替换规则后一旦切换开关、编辑规则或重新打开书籍规则便不再生效。文章以 apps/readest-app/.claude/memory/proofread-selection-section-anchor-6148.md 记录的问题诊断为主线深入源码剖析根因TOC href 与 spine item href 两套节身份标识的错位、修复方案基于 CFI 的节匹配以及配套的验证与审查过程。读完本文你将理解 Readest 校对替换管线的完整链路掌握规则创建时生效、重放时失效这一类问题的通用排查方法并能复现文档中给出的 vitest 浏览器级验证流程。一、问题表象规则只生效一次的假象Issue #6148 描述的现象非常典型用户选中一段文本创建校对替换规则点击Apply后替换立刻生效但一旦切换规则的启用开关、编辑规则内容或者关闭书籍后重新打开这条规则就再也无法复现替换效果。从 ProofreadPopup.tsx 的源码可以看到规则创建后先做了一次实时 DOM 编辑再通过addRule持久化规则// handleApply 中scope selection 分支 const textNode range.startContainer as Text; const text textNode.textContent ?? ; textNode.textContent text.slice(0, range.startOffset) replacement text.slice(range.endOffset);也就是说用户点击 Apply 后看到的效果其实是ProofreadPopup直接修改了当前已加载的活文档与后续渲染所依赖的转换器transformer没有任何关系。而书籍每一次重新加载、章节每一次重新渲染时内容都会经过 proofreadTransformer 重新处理。问题诊断结论该记忆文档原话规则在创建时一直生效但原因是错误的——selection 作用域规则在 transformer 路径上从来就没有真正工作过只是被实时 DOM 编辑掩盖了。二、根因剖析两套相互冲突的节身份标识记忆文档给出了清晰的根因定位ProofreadPopup与proofreadTransformer对当前节采用了两种不同的身份标识。2.1 存储端TOC href修复前ProofreadPopup将progress?.sectionHref存入规则的sectionHref字段。而progress.sectionHref来自 foliate 的TOCProgress.getProgress语义是当前阅读位置之前最近的一条目录TOC导航条目——它是一个tocItem.href。2.2 匹配端spine item hrefproofreadTransformer拿rule.sectionHref与转换上下文的ctx.sectionHref比较。在 FoliateViewer.tsx 中这个值被赋为 foliate 的detail.name来自Loader.createURL即spine/manifest 清单中的章节条目 hrefbookDoc.sections[i].id item.href。2.3 两者何时不一致两种 href 只有在每个 spine 条目恰好都有独立 TOC 条目时才相等。而现实中的 EPUB 往往不满足这一假设。记忆文档记录了作者用仓库自带 epub 测试夹具实测的数据sample-table-layout.epub12 个节中有 10 个不匹配sample-alice.epub16 个节中有 2 个不匹配索引 15 是OPS/feedbooks.xml其 TOC 条目却是OPS/main11.xml大多数书籍的第一个 spine 条目不匹配因为封面cover通常没有 TOC 条目此时toc为undefined。这解释了为何规则偶尔生效、大多数情况下静默失效——匹配成功纯属巧合。2.4 佐证TransformContext 的注释与字段演化修复后的 TransformContext 类型定义 用注释直接记录了这段历史sectionHref?: string; // Spine CFI of the section being transformed (sections[i].cfi), when the // format has one. Selection-scoped proofread rules match against this rather // than sectionHref, which is the TOC href at the reading position and so // names a different file whenever a spine item has no TOC entry (#6148). sectionCfi?: string;三、修复方案用规则自带的 CFI 锚定节身份修复落地于 PR #61522026-09-09 以 squash 方式合并提交1b681939dCI 全 15 项检查通过。方案分两步核心思路是放弃用 href 判断当前节改为用规则自身携带的 CFI 判断。3.1 第一步给 TransformContext 增加 sectionCfiTransformContext新增sectionCfi字段在FoliateViewer装配上下文时从书文档的 sections 索引中按 spine id 查找sectionHref: detail.name, sectionCfi: bookData.bookDoc?.sections?.find((s) s.id detail.name)?.cfi,bookDoc.sections中每一项都带有自己的 spine CFI形如epubcfi(/6/14)这正是节身份的权威表达。3.2 第二步inThisSection 改用 CFI 的 spine step 比较proofread.ts 中新增了cfiSpineStep辅助函数与inThisSection判定// Spine step of a CFI: everything before the first indirection (!), or the // whole path for a section CFI, which has none. sections[i].cfi is // epubcfi(/6/14) and a selection made in it is epubcfi(/6/14!/4/2,...). const cfiSpineStep (cfi?: string): string | undefined cfi?.match(/^epubcfi\((.*?)(?:!|\)$)/)?.[1]; const inThisSection (rule: ProofreadRule, ctx: TransformContext): boolean { const ruleSpine cfiSpineStep(rule.cfi); const ctxSpine cfiSpineStep(ctx.sectionCfi); if (ruleSpine ctxSpine) return ruleSpine ctxSpine; return ctx.sectionHref?.split(#)[0] rule.sectionHref?.split(#)[0]; };这里的技巧是提取 CFI 中第一个!间接寻址指示符之前的spine step一条在章节内做出的选择其 CFI 是epubcfi(/6/14!/4/2,...)而章节本身的 CFI 是epubcfi(/6/14)二者去掉!后的 spine step 完全一致从而可以可靠判断这条规则属于当前正在转换的章节。无需迁移旧规则虽然存的是 TOC href但其cfi字段天然携带 spine 信息新逻辑自动治愈heal已存储的旧规则优雅降级没有 spine CFI 的格式如 markdown 书籍见 utils/md.ts其section.id是String(index)回退到旧的 href 比较行为不变。3.3 修正存储端ProofreadPopup 存 spine hrefProofreadPopup.tsx 现在优先从视图的书对象中取 spine 条目 id让sectionHref字段名副其实// Anchor to the spine item href, which is what every section load hands // the proofread transformer (foliates detail.name). progress carries // the TOC href ... const sectionHref getView(bookKey)?.book?.sections?.[selection.index]?.id ?? progress?.sectionHref;3.4 规则数据模型ProofreadRule的完整结构见 types/book.ts其中与本次修复直接相关的字段是cfi选择锚点与sectionHref节身份order字段决定规则应用顺序数值小者先应用enabled/deletedAt/updatedAt则支撑规则的启用状态与 CRDT 同步book/selection 作用域规则随书配置同步library 作用域规则走设置副本的整体字段 LWW 合并。四、排查过的死胡同非根因项记忆文档明确记录了一组被验证过、但并非本 bug 成因的候选路径对后续排查有极高的参考价值text/html与 XHTML 解析差异transformContent调用时未传docType选项所以转换器内docType恒为text/html——无害foliate 跨recreateViewer的 blob-URL 缓存不是成因cfi-inert无障碍跳过链接注入foliate 的 CFI 索引会过滤这些节点不是成因enabled/onlyForTTS过滤逻辑见 proofread.ts规则需enabled !deletedAt pattern.trim()且通过onlyForTTS分流后才参与处理——非成因viewSettings.proofreadRules持久化数据能正常落盘非成因。五、验证配方只有浏览器级测试才是忠实复现记忆文档强调jsdom 之外的唯一可靠复现方式是 vitest 浏览器测试。给出的验证步骤值得完整保留用DocumentLoader加载sample-alice.epub测试夹具创建真实的foliate-view与paginator.js把transformContent挂到book.transformTarget的data事件上逐个捕获detail.name→ 原始字符串的映射执行view.goTo(15)跳转到目标节用getContents().find((c) c.index INDEX)选取目标文档——注意不能直接取getContents()[0]那通常是相邻章节会静默得到错误文档的 CFI计算view.getCFI(index, range)再对捕获的原始字符串重跑transformContent验证替换是否命中。技术要点jsdom 能复现除sanitizer之外的全部行为因为 DOMPurify 在 jsdom 环境下会输出重复的xmlns属性——在 jsdom 中验证时应把sanitizer从转换器列表中剔除。六、CodeRabbit 审查7 项发现与三项关键修正PR 的 CodeRabbit 审查共提出 7 项发现作者全部认可其有效性其中 3 项以不同于建议的方式解决提交3b5b086d3、cc59920a56.1 真实 bugtrim 不一致导致的同会话漂移实时拼接live splice使用的是原始replacementText而规则持久化的是replacementText.trim()。结果是同一替换本会话显示一个样子后续每次重放又是另一个样子——这正是本次 PR 要消除的漂移。修复方式在handleApply顶部统一 trim 一次两处复用见 ProofreadPopup.tsx 的注释与const replacement replacementText.trim();。6.2 拒绝提示语与实际约束不符审查指出规则的实际约束是单个文本节点——一个em或链接就会在一个段落内打破这个前提因此请在同一段落内选择是可照做却被拒绝的建议。作者没有采纳建议中的single text node对读小说的人来说是 DOM 术语而是把提示改写为面向用户的自然语言This selection spans formatting or paragraph boundaries. Select a smaller piece of text, or choose another replacement scope.并重新翻译了全部 34 个语言文件。6.3 术语一致性翻译必须复用既有词条两条术语审查sv、bo都先对照语言文件核实sv用Scope → Omfattningbo用Paragraph → དོན་ཚན།/Chapter → ལེའུ།而新造的ཚིག་ལེའུ混淆了段落与章节。由此确立一条规则新翻译必须复用该语言文件已有的术语Scope、Paragraph等不得自造新词。该规则推广到全部 34 个语言文件随后又揪出德语生造词Ersetzungs-Geltungsbereich最终采用与作用域选择器标签一致的Geltungsbereich für die Ersetzung。七、同轮落地的两个关联修复7.1 跳转到选择位置的可见入口#6109 遗留Selection作用域徽章曾被直接做成跳转目标但它在视觉上与旁边的 Regex、Case sensitive 徽章完全一样用户根本发现不了。修复后徽章恢复为普通RuleChip跳转变成编辑/删除按钮旁独立的btn btn-ghost btn-sm h-8 w-8 p-0按钮使用MdOutlineArrowOutward图标与已有翻译串_(Jump to Location)FootnotePopup 已在用无需新增 key见 ProofreadRules.tsx行内为绝对定位的操作簇预留宽度所以额外按钮需要把pe-28改为pe-36——这个数值是在apps/readest-app/src/__tests__/components/proofread-rule-row-clearance.browser.test.tsx中实测出来的pe-28加第三个按钮确实重叠不是拍脑袋。7.2 deleteContents 的标记丢失ProofreadPopup原先用deleteContents()insertNode()做实时替换这会剥离选择触达的标记并把文本节点拆成三段——后续该章节内所有 CFI 计算依赖的文本节点索引因此偏移。修复后改为在单个文本节点内原地拼接跨元素的选择会被 toast 拒绝因为applyReplacementSingle要求startContainer endContainer此类规则根本无法重放过去只是靠实时 DOM 编辑假装生效。顺带说明这一改动使ProofreadPopup.test.tsx中共享的defaultProps.selection.range在测试间泄漏旧夹具是带deleteContents: vi.fn()的假对象现在改为在beforeEach中重建。八、小结与排查经验Issue #6148 的完整闭环可以浓缩为几条可迁移的经验创建时生效不能证明重放路径正确——任何在创建时直接改动活 DOM 的功能都必须额外验证持久化后的重放路径节身份锚定要选权威表达EPUB 中 spine item及其 CFI是每章的权威身份TOC href 只是最近前驱导航条目两者在封面、合并章节等场景必然错位CFI 的 spine step!之前的部分是判断规则属于哪个节的可靠依据修复要让旧数据自动愈合基于规则已携带的cfi做匹配无需迁移存量规则同时为无 spine CFI 的格式保留 href 回退提示语面向用户而非 DOM把单一文本节点翻译成选择更小文本或换一种替换作用域并保持 34 个语言文件的术语一致性验证必须有浏览器级测试jsdom 只差sanitizer一个转换器即可完整复现选文档时用getContents().find((c) c.index INDEX)而非getContents()[0]避免拿到相邻章节的 CFI。如需继续深入可进一步阅读关联的记忆文档 proofread-panel-design-pass-6109.mdselection 徽章与面板设计演进与 proofread-gate-reflowable-formats.md重排格式的校对功能门控以及 proofreadStore.ts 中规则的增删改查与排序持久化实现。赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐commitlint核心规则解析深度理解30校验规则commitlint核心规则解析深度理解30校验规则 commitlint是一个强大的Git提交信息校验工具通过30多种核心规则确保团队代码提交的规范性和开发工具Lint代码质量eslint-plugin-unicorn prefer-string-slice 规则深度解析用 Stringslice() 替换 substr 与 substring 的自动修复机制eslint plugin unicorn prefer string slice 规则深度解析用 String slice 替换 substr 与 subsLint代码质量Complete-Python-3-Bootcamp变量作用域LEGB规则深度解析Complete Python 3 Bootcamp变量作用域LEGB规则深度解析 在Python编程中变量作用域Scope是每个开发者必须掌握的核心概教程示例工程上一篇Cuckoo Sandbox开源自动化恶意软件分析系统详解下一篇NBA_API v1.9.0版本发布新增实时数据接口与构建优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表