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
This commit is contained in:
Gigi
2025-10-14 00:05:43 +02:00
parent c7c82954ad
commit 12c70b06de

View File

@@ -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
}