diff --git a/src/lib/client/highlight-char-map.ts b/src/lib/client/highlight-char-map.ts index a5abcfe..54e9814 100644 --- a/src/lib/client/highlight-char-map.ts +++ b/src/lib/client/highlight-char-map.ts @@ -44,8 +44,14 @@ const replaceMappedUrls = (tokens: MappedChar[]): MappedChar[] 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; diff --git a/src/lib/client/html/highlight.ts b/src/lib/client/html/highlight.ts index baec7d8..f9a35f6 100644 --- a/src/lib/client/html/highlight.ts +++ b/src/lib/client/html/highlight.ts @@ -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);