// ── Utilities ── export function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } export function escRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } let csrfToken = ''; export function setCsrf(token) { csrfToken = token; } export function getCsrf() { return csrfToken; } export async function api(path, opts = {}) { const headers = { 'Content-Type': 'application/json' }; if (csrfToken) headers['X-CSRF-Token'] = csrfToken; const res = await fetch(path, { headers, ...opts, body: opts.body ? JSON.stringify(opts.body) : undefined, }); return res.json(); } export function $(id) { return document.getElementById(id); } export function fuzzyMatch(needle, haystack) { if (needle.length < 3) return null; let ni = 0, hi = 0, score = 0, streak = 0; while (ni < needle.length && hi < haystack.length) { if (needle[ni] === haystack[hi]) { streak++; score += streak * 2; if (hi === 0 || /[\s\-_./]/.test(haystack[hi - 1])) score += 10; if (hi < 5) score += 5 - hi; ni++; } else { streak = 0; } hi++; } if (ni < needle.length) return null; return score - (haystack.length - needle.length) * 0.3; } export function countSubstring(str, sub) { let c = 0, p = 0; while ((p = str.indexOf(sub, p)) !== -1) { c++; p += sub.length; } return c; } export function highlight(html, words) { let result = html; for (const w of words) result = result.replace(new RegExp('(' + escRe(w) + ')', 'gi'), '$1'); if (!result.includes('')) { const full = words.join(''); let fi = 0, out = ''; for (let i = 0; i < html.length && fi < full.length; i++) { if (html[i].toLowerCase() === full[fi]) { out += '' + html[i] + ''; fi++; } else { out += html[i]; } } out += html.substring(out.replace(/<\/?mark>/g, '').length); result = out; } return result; }