Files
fuz-site/src/lib/highlightUtils.jsx

134 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 ", <mark class="f-hl">World</mark>]
*/
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(<mark class="f-hl">{matches[i]}</mark>);
}
}
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 = "<p>Hello World</p>";
* highlightHtml(html, "world")
* // => "<p>Hello <mark class="f-hl">World</mark></p>"
*/
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 <mark>
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;
}