refactor: DRY up highlightMatching to stay under 210 lines

- Extract helper functions: normalizeWhitespace, createMarkElement, replaceTextWithMark
- Consolidate duplicate exact/normalized matching logic into tryMarkInTextNodes
- Reduce from 242 lines to 209 lines
- Maintain all functionality while improving code reusability
This commit is contained in:
Gigi
2025-10-04 20:45:06 +01:00
parent c0de624fe6
commit 38b80bc85b

View File

@@ -96,16 +96,80 @@ export function applyHighlightsToText(
return <>{result}</>
}
// Helper to normalize whitespace for flexible matching
const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim()
// Helper to create a mark element for a highlight
function createMarkElement(highlight: Highlight, matchText: string): HTMLElement {
const mark = document.createElement('mark')
mark.className = 'content-highlight'
mark.setAttribute('data-highlight-id', highlight.id)
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
mark.textContent = matchText
return mark
}
// Helper to replace text node with mark element
function replaceTextWithMark(textNode: Text, before: string, match: string, after: string, mark: HTMLElement) {
const parent = textNode.parentNode
if (!parent) return
if (before) parent.insertBefore(document.createTextNode(before), textNode)
parent.insertBefore(mark, textNode)
if (after) {
textNode.textContent = after
} else {
parent.removeChild(textNode)
}
}
// Helper to find and mark text in nodes
function tryMarkInTextNodes(
textNodes: Text[],
searchText: string,
highlight: Highlight,
useNormalized: boolean
): boolean {
const normalizedSearch = normalizeWhitespace(searchText)
for (const textNode of textNodes) {
const text = textNode.textContent || ''
const searchIn = useNormalized ? normalizeWhitespace(text) : text
const searchFor = useNormalized ? normalizedSearch : searchText
const index = searchIn.indexOf(searchFor)
if (index === -1) continue
console.log(`✅ Found ${useNormalized ? 'normalized' : 'exact'} match:`, text.slice(0, 50))
let actualIndex = index
if (useNormalized) {
// Map normalized index back to original text
let normalizedIdx = 0
for (let i = 0; i < text.length && normalizedIdx < index; i++) {
if (!/\s/.test(text[i]) || (i > 0 && !/\s/.test(text[i-1]))) normalizedIdx++
actualIndex = i + 1
}
}
const before = text.substring(0, actualIndex)
const match = text.substring(actualIndex, actualIndex + searchText.length)
const after = text.substring(actualIndex + searchText.length)
const mark = createMarkElement(highlight, match)
replaceTextWithMark(textNode, before, match, after, mark)
return true
}
return false
}
/**
* Apply highlights to HTML content by injecting mark tags using DOM manipulation
*/
export function applyHighlightsToHTML(
html: string,
highlights: Highlight[]
): string {
export function applyHighlightsToHTML(html: string, highlights: Highlight[]): string {
if (!html || highlights.length === 0) return html
// Create a temporary DOM element to work with
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html
@@ -115,120 +179,23 @@ export function applyHighlightsToHTML(
highlightTexts: highlights.map(h => h.content.slice(0, 50))
})
// Process each highlight
for (const highlight of highlights) {
const searchText = highlight.content.trim()
if (!searchText) continue
console.log('🔍 Processing highlight:', searchText.slice(0, 50))
// Normalize whitespace for more flexible matching
const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim()
const normalizedSearch = normalizeWhitespace(searchText)
// Walk through all text nodes and replace matches
const walker = document.createTreeWalker(
tempDiv,
NodeFilter.SHOW_TEXT,
null
)
// Collect all text nodes
const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null)
const textNodes: Text[] = []
let node: Node | null
while ((node = walker.nextNode())) {
textNodes.push(node as Text)
}
while ((node = walker.nextNode())) textNodes.push(node as Text)
// Try exact match first, then normalized match
let found = false
const found = tryMarkInTextNodes(textNodes, searchText, highlight, false) ||
tryMarkInTextNodes(textNodes, searchText, highlight, true)
// First pass: exact match
for (const textNode of textNodes) {
const text = textNode.textContent || ''
const index = text.indexOf(searchText)
if (index !== -1) {
console.log('✅ Found exact match in text node:', text.slice(Math.max(0, index - 20), index + 50))
// Split the text node and insert the mark element
const before = text.substring(0, index)
const match = text.substring(index, index + searchText.length)
const after = text.substring(index + searchText.length)
const mark = document.createElement('mark')
mark.className = 'content-highlight'
mark.setAttribute('data-highlight-id', highlight.id)
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
mark.textContent = match
const parent = textNode.parentNode
if (parent) {
if (before) {
parent.insertBefore(document.createTextNode(before), textNode)
}
parent.insertBefore(mark, textNode)
if (after) {
textNode.textContent = after
} else {
parent.removeChild(textNode)
}
}
found = true
break
}
}
// Second pass: normalized whitespace match
if (!found) {
for (const textNode of textNodes) {
const text = textNode.textContent || ''
const normalizedText = normalizeWhitespace(text)
const index = normalizedText.indexOf(normalizedSearch)
if (index !== -1) {
console.log('✅ Found normalized match in text node:', text.slice(0, 50))
// Find the actual position in the original text
let actualIndex = 0
let normalizedIndex = 0
for (let i = 0; i < text.length && normalizedIndex < index; i++) {
if (!/\s/.test(text[i]) || (i > 0 && !/\s/.test(text[i-1]))) {
normalizedIndex++
}
actualIndex = i + 1
}
// Approximate the length in the original text
const actualLength = searchText.length
const match = text.substring(actualIndex, actualIndex + actualLength)
const before = text.substring(0, actualIndex)
const after = text.substring(actualIndex + actualLength)
const mark = document.createElement('mark')
mark.className = 'content-highlight'
mark.setAttribute('data-highlight-id', highlight.id)
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
mark.textContent = match
const parent = textNode.parentNode
if (parent) {
if (before) {
parent.insertBefore(document.createTextNode(before), textNode)
}
parent.insertBefore(mark, textNode)
if (after) {
textNode.textContent = after
} else {
parent.removeChild(textNode)
}
}
break
}
}
}
if (!found) console.log('⚠️ No match found for highlight')
}
const result = tempDiv.innerHTML