fix(highlight): improve URL replacement spread and sentence alignment fallback

Distribute replacement characters across original URL spans to preserve positional mapping in highlights. In sentence alignment, add fallback to avoid misaligned word highlights when the sentence cannot be located, ensuring robustness in TTS word highlighting.
This commit is contained in:
Richard R 2026-06-09 09:01:48 -06:00
parent ff5a6b834d
commit a2cdc575ef
2 changed files with 22 additions and 6 deletions

View file

@ -44,8 +44,14 @@ const replaceMappedUrls = <TPos>(tokens: MappedChar<TPos>[]): MappedChar<TPos>[]
const anchor = tokens[start] ?? tokens[Math.max(0, end - 1)];
if (anchor) {
const replacement = `- (link to ${match[1]}) -`;
for (const char of replacement) {
replaced.push(cloneMappedChar(char, anchor));
// Spread the replacement characters across the original URL span so the
// mapped positions keep their positional spread instead of collapsing the
// whole URL onto a single DOM anchor.
const originalLength = Math.max(1, end - start);
for (let i = 0; i < replacement.length; i += 1) {
const sourceIndex = Math.min(end - 1, start + Math.floor((i * originalLength) / replacement.length));
const source = tokens[sourceIndex] ?? anchor;
replaced.push(cloneMappedChar(replacement[i], source));
}
}
cursor = end;

View file

@ -326,17 +326,27 @@ export function highlightHtmlWord(
// wider than the spoken sentence; rebasing keeps the alignment char offsets
// accurate regardless.
if (sentenceState.alignment !== alignment || sentenceState.base === null) {
sentenceState.alignment = alignment;
const found = sentenceState.text.indexOf(alignment.sentence);
sentenceState.base = found >= 0 ? found : 0;
if (found < 0) {
// Sentence not located in the wrap — fail closed (leave only the sentence
// highlight) rather than snapping the word highlight to offset 0. Don't
// commit the alignment so the next tick retries the lookup.
sentenceState.base = null;
return false;
}
sentenceState.alignment = alignment;
sentenceState.base = found;
}
const base = sentenceState.base;
if (base === null) return false;
const word = words[wordIndex];
const { charStart, charEnd } = word;
if (!Number.isInteger(charStart) || !Number.isInteger(charEnd) || charEnd <= charStart) return false;
const start = Math.max(0, sentenceState.base + charStart);
const end = Math.min(sentenceState.chars.length, sentenceState.base + charEnd);
const start = Math.max(0, base + charStart);
const end = Math.min(sentenceState.chars.length, base + charEnd);
if (end <= start) return false;
wordWraps = wrapCharRange(sentenceState.chars, start, end, HTML_WORD_CLASS);