From 12c70b06de58c34acfd0bc9073d19d70ef126975 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 14 Oct 2025 00:05:43 +0200 Subject: [PATCH] fix(highlights): improve text matching to handle multi-node selections - Add tryMultiNodeMatch function to find text spanning multiple DOM nodes - Build combined text from all text nodes for comprehensive matching - Handle highlighting across node boundaries with proper offsets - Falls back to multi-node matching when single-node match fails - Fixes issue where selections with inline formatting couldn't be matched --- src/utils/highlightMatching/domUtils.ts | 93 ++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/src/utils/highlightMatching/domUtils.ts b/src/utils/highlightMatching/domUtils.ts index 44db8283..de6875e4 100644 --- a/src/utils/highlightMatching/domUtils.ts +++ b/src/utils/highlightMatching/domUtils.ts @@ -52,6 +52,7 @@ export function tryMarkInTextNodes( ): boolean { const normalizedSearch = normalizeWhitespace(searchText) + // First try: Single text node match for (const textNode of textNodes) { const text = textNode.textContent || '' const searchIn = useNormalized ? normalizeWhitespace(text) : text @@ -79,6 +80,96 @@ export function tryMarkInTextNodes( return true } - return false + // Second try: Multi-node match (for text spanning multiple elements) + return tryMultiNodeMatch(textNodes, searchText, highlight, useNormalized, highlightStyle) +} + +/** + * Try to find and mark text that spans multiple text nodes + */ +function tryMultiNodeMatch( + textNodes: Text[], + searchText: string, + highlight: Highlight, + useNormalized: boolean, + highlightStyle: 'marker' | 'underline' = 'marker' +): boolean { + const normalizedSearch = normalizeWhitespace(searchText) + + // Build a combined text from all nodes + let combinedText = '' + const nodeMap: Array<{ node: Text; start: number; end: number; originalText: string }> = [] + + for (const node of textNodes) { + const text = node.textContent || '' + const start = combinedText.length + const end = start + text.length + nodeMap.push({ node, start, end, originalText: text }) + combinedText += text + } + + // Search in combined text + const searchIn = useNormalized ? normalizeWhitespace(combinedText) : combinedText + const searchFor = useNormalized ? normalizedSearch : searchText + const matchIndex = searchIn.indexOf(searchFor) + + if (matchIndex === -1) return false + + // Map normalized index back to original if needed + let startIndex = matchIndex + let endIndex = matchIndex + searchText.length + + if (useNormalized) { + // This is a simplified mapping - for normalized matches we approximate + const ratio = combinedText.length / searchIn.length + startIndex = Math.floor(matchIndex * ratio) + endIndex = Math.min(combinedText.length, startIndex + searchText.length) + } + + // Find which nodes contain the match + const affectedNodes: Array<{ node: Text; startOffset: number; endOffset: number }> = [] + + for (const nodeInfo of nodeMap) { + if (startIndex < nodeInfo.end && endIndex > nodeInfo.start) { + const nodeStart = Math.max(0, startIndex - nodeInfo.start) + const nodeEnd = Math.min(nodeInfo.originalText.length, endIndex - nodeInfo.start) + affectedNodes.push({ node: nodeInfo.node, startOffset: nodeStart, endOffset: nodeEnd }) + } + } + + if (affectedNodes.length === 0) return false + + // Apply highlighting across multiple nodes + for (let i = 0; i < affectedNodes.length; i++) { + const { node, startOffset, endOffset } = affectedNodes[i] + const text = node.textContent || '' + + if (i === 0 && i === affectedNodes.length - 1) { + // Single node (shouldn't happen as this is the multi-node case, but handle it) + const before = text.substring(0, startOffset) + const match = text.substring(startOffset, endOffset) + const after = text.substring(endOffset) + const mark = createMarkElement(highlight, match, highlightStyle) + replaceTextWithMark(node, before, after, mark) + } else if (i === 0) { + // First node + const before = text.substring(0, startOffset) + const match = text.substring(startOffset) + const mark = createMarkElement(highlight, match, highlightStyle) + replaceTextWithMark(node, before, '', mark) + } else if (i === affectedNodes.length - 1) { + // Last node + const match = text.substring(0, endOffset) + const after = text.substring(endOffset) + const mark = createMarkElement(highlight, match, highlightStyle) + replaceTextWithMark(node, '', after, mark) + } else { + // Middle nodes - wrap entire text + const mark = createMarkElement(highlight, text, highlightStyle) + replaceTextWithMark(node, '', '', mark) + } + } + + return true }