/** * Utility functions do podświetlania wyszukiwanych fraz * Używane przez komponenty search */ /** * Escape RegExp special characters */ export function escapeRegExp(str) { return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Podświetlenie w czystym tekście (np. title) * * @param {string} text - Tekst do podświetlenia * @param {string} query - Fraza do wyszukania * @returns {string|Array} - Tekst lub array z elementami JSX * * @example * highlightText("Hello World", "world") * // => ["Hello ", World] */ export function highlightText(text, query) { const trimmedQuery = (query || '').trim(); if (!trimmedQuery) return text; const re = new RegExp(escapeRegExp(trimmedQuery), 'ig'); const parts = String(text || '').split(re); if (parts.length === 1) return text; // split() gubi match – więc budujemy przez exec/match const matches = String(text || '').match(re) || []; const result = []; for (let i = 0; i < parts.length; i++) { result.push(parts[i]); if (i < matches.length) { result.push({matches[i]}); } } return result; } /** * Podświetlenie wewnątrz HTML (po markdown) * Omija tagi PRE, CODE, SCRIPT, STYLE * * @param {string} html - HTML do podświetlenia * @param {string} query - Fraza do wyszukania * @returns {string} - HTML z podświetleniem * * @example * const html = "
Hello World
"; * highlightHtml(html, "world") * // => "Hello World
" */ export function highlightHtml(html, query) { const trimmedQuery = (query || '').trim(); if (!trimmedQuery) return html; const re = new RegExp(escapeRegExp(trimmedQuery), 'ig'); const doc = new DOMParser().parseFromString(html, 'text/html'); const root = doc.body; const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); // Funkcja sprawdzająca czy node jest w tagu do pominięcia const shouldSkipNode = (node) => { const parent = node.parentElement; if (!parent) return true; const tagName = parent.tagName; return tagName === 'SCRIPT' || tagName === 'STYLE' || tagName === 'CODE' || tagName === 'PRE'; }; // Zbierz wszystkie text nodes const textNodes = []; let node; while ((node = walker.nextNode())) { textNodes.push(node); } // Podświetl każdy text node for (const textNode of textNodes) { if (shouldSkipNode(textNode)) continue; const text = textNode.nodeValue || ''; if (!re.test(text)) continue; // Reset RegExp state (test() z /g/ przesuwa lastIndex) re.lastIndex = 0; const fragment = doc.createDocumentFragment(); let lastIndex = 0; let match; while ((match = re.exec(text))) { const matchStart = match.index; const matchEnd = matchStart + match[0].length; // Tekst przed match if (matchStart > lastIndex) { fragment.appendChild( doc.createTextNode(text.slice(lastIndex, matchStart)) ); } // Match w const mark = doc.createElement('mark'); mark.className = 'f-hl'; mark.textContent = text.slice(matchStart, matchEnd); fragment.appendChild(mark); lastIndex = matchEnd; } // Tekst po ostatnim match if (lastIndex < text.length) { fragment.appendChild(doc.createTextNode(text.slice(lastIndex))); } // Zastąp text node fragmentem textNode.parentNode?.replaceChild(fragment, textNode); } return root.innerHTML; }