(function(){ function buildTokens(rawQuery) { const q = (rawQuery || '').trim(); if (!q) return []; // Normalize punctuation to spaces, trim non-alphanumerics at ends, dedupe const tokens = q .replace(/[,_;:]+/g, ' ') .split(/\s+/) .map(t => t.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, '')) .filter(Boolean); return Array.from(new Set(tokens)); } function escapeHtml(text) { try { return (window.htmlSanitizer && window.htmlSanitizer.escape) ? window.htmlSanitizer.escape(text) : String(text == null ? '' : text); } catch (_) { return String(text == null ? '' : text); } } function highlight(text, tokens) { const value = text == null ? '' : String(text); if (!value || !Array.isArray(tokens) || tokens.length === 0) return escapeHtml(value); try { const source = String(value); const haystack = source.toLowerCase(); const uniqueTokens = Array.from(new Set((tokens || []).map(t => String(t).toLowerCase()).filter(Boolean))); const ranges = []; uniqueTokens.forEach(t => { let from = 0; while (from <= haystack.length - t.length && t.length > 0) { const idx = haystack.indexOf(t, from); if (idx === -1) break; ranges.push([idx, idx + t.length]); from = idx + 1; // allow overlapping matches shift by 1 } }); if (ranges.length === 0) return escapeHtml(source); // Merge overlapping/adjacent ranges ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]); const merged = []; let [curStart, curEnd] = ranges[0]; for (let i = 1; i < ranges.length; i++) { const [s, e] = ranges[i]; if (s <= curEnd) { // overlap or adjacency curEnd = Math.max(curEnd, e); } else { merged.push([curStart, curEnd]); [curStart, curEnd] = [s, e]; } } merged.push([curStart, curEnd]); // Build output with escaping of text segments let out = ''; let pos = 0; merged.forEach(([s, e]) => { if (pos < s) out += escapeHtml(source.slice(pos, s)); out += '' + escapeHtml(source.slice(s, e)) + ''; pos = e; }); if (pos < source.length) out += escapeHtml(source.slice(pos)); return out; } catch (_) { return escapeHtml(String(text)); } } function formatSnippet(snippet, tokens) { if (!snippet) return ''; let html = String(snippet); try { const hasStrong = /<\s*strong\b/i.test(html); if (!hasStrong) { html = highlight(html, Array.isArray(tokens) ? tokens : []); } if (window.htmlSanitizer && typeof window.htmlSanitizer.sanitize === 'function') { html = window.htmlSanitizer.sanitize(html); } } catch (_) {} return html; } window.highlightUtils = { buildTokens, highlight, escape: escapeHtml, formatSnippet }; })();