全形态下划线(标记一个,点亮所有变形)
上级:key-designs
这是本产品阅读体验中最独特的一项功能:只要保存一个词的一种变形,该词元(lemma)的所有屈折形式就会在全书范围内被加上下划线。(内部代号"Task #2"。)
形态集合。 services/formMatching.ts 为每个词元构建一个匹配集合:entryFormSet = LLM/词典抓取得到的 forms ∪ 该词的表层形式 ∪ 词元本身;normalizeForms 负责转小写、去首尾空白,并丢弃多词元的表层形式(这样 "le château" 无法匹配页面上的单个词元,但 "château" 本身仍能匹配;"ils/elles méritent" 也不会造成误匹配)。随后 buildMarkedWordsMap(services/displayUtils.ts)用每一种形式作为键建立一张映射表,阅读器只要 markedWords.get(token.toLowerCase()) 命中,就为该页面词元加上下划线(ClickableText.tsx)。
分词器必须与其他一切保持一致。 services/tokenizationService.ts 是一个支持 Unicode 的分词器(拉丁标点单独处理;中日韩汉字 / 平假名 / 片假名 / 谚文各自归为一类,中日韩汉字逐字符切分),其中还有一道法语省音合并步骤,会把 l'、d'、j'、qu' 与其后的词重新合并为一个词元——这与写死在 LLM prompt 里的分词规则保持一致。这一点之所以关键,是因为词元边界必须在三个消费方之间完全一致:下划线匹配器、词性着色、以及 LLM 的单词分析。三者一旦不一致,下划线就会标错位置,或者颜色标错。
德语可分离动词在这里也有专门处理(客户端的 services/separableVerbService.ts 启发式规则),在 Python 引擎里则有正规处理(linguistic-two-engines)。
为什么它是承重构件而非装饰:下划线是整个 vocabulary-loop 的反馈界面——正是它让已保存的词在阅读时变得可见,而它依赖的正是"形态集合 + 一个与语言层保持一致的分词器"这套组合。分词器一旦出错,核心功能就会在无声无息中把标注画错地方。