Poprawka w rozszerzeniu pliku highlightUtils

This commit is contained in:
dm
2025-12-21 18:32:32 +01:00
parent ed513957c3
commit b065db4faf
2 changed files with 9 additions and 9 deletions

134
src/lib/highlightUtils.jsx Normal file
View File

@@ -0,0 +1,134 @@
/**
* 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;
}