原始/显示双坐标骨架(raw ↔ display)
上级:key-designs
阅读器里最承重的一个想法。每一条持久化的 CachedSentence(core/src/entities/CachedSentence.ts)都携带两套坐标系:
rawStart/rawText—— 相对于源文本的偏移量(分词、词形还原、稳定身份都建立在这上面);displayStart/displayText—— 相对于净化后文本的偏移量,也就是用户实际读到的内容(空白被折叠,软连字符/标记被去除);displayToRaw[]—— 每句一份的映射表,把 display 偏移量换算回 raw 偏移量(sentenceDisplayToBookRaw/sentenceBookRawToDisplay,用二分查找实现)。
规则: 书签、高亮的 startOffset/endOffset、生词的 rawStart、阅读位置 Anchor——所有需要持久保存的位置,都以全书绝对的 raw 偏移量存储;display 层则是在渲染时重新构建出来的(Page.ts 通过 pageDisplayToRaw/pageRawToDisplay 把页内坐标和全书 raw 坐标互相换算)。
为什么这是骨架: 阅读器会不断重新分页(字号、视口、窗口尺寸变化),而净化器又会改变显示出来的字符。如果位置是以 display 偏移量或页码存储的,那么每一个笔记/高亮/生词的锚点都会在版式一变就漂移。锚定到原始源文本偏移量,让这些位置与版式无关——这正是一个"用户会做标记、还会回来看"的阅读应用最在意的事。Anchor 甚至还存了一段不超过 200 字符的 display 前缀,靠前缀匹配重新解析出对应的 sentenceIdx,这样即便偏移量发生了变化,位置也依然能找回来。
(raw, display, displayToRaw) 这个三元组是按句子由 services/sentenceSanitize.ts 构建的,并通过句子缓存持久化(一个内存中的 building map 原子地提交到 sentences_real,并用 SANITIZER_VERSION + PAGINATION_VERSION 做版本门控,逻辑一升级,旧的行就作废)。这也是它被做成一个带类型的值对象边界、而不是散落各处的偏移量运算的原因:这个转换本身并不简单,而且是单向有损的。