2026-09-23·by Sijie Wang#lucerna#software#architecture

raw-display-cursor

原始/显示双坐标骨架(raw ↔ display)

上级:key-designs

阅读器里最承重的一个想法。每一条持久化的 CachedSentencecore/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 做版本门控,逻辑一升级,旧的行就作废)。这也是它被做成一个带类型的值对象边界、而不是散落各处的偏移量运算的原因:这个转换本身并不简单,而且是单向有损的。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →