'use strict'; /** * Keyword matching — which of Overflow's keywords a slide's notes carry. * * Pure functions, no I/O, so keywords.selftest.js can pin every rule down: * * cleanNotes(raw) → the notes with ProPresenter's invisible padding removed * keywordMatches(notes, keyword) → boolean * matchKeywords(notes, keywords) → every keyword (in the order given) the notes carry * sameSet(a, b) → the two keyword lists hold the same keywords * * The rule is Overflow's own helper's: the notes are split into whitespace- * separated words, each stripped of surrounding punctuation and lower-cased, * and a keyword fires when it is one of those words. So `give` fires on * "give", "Give!", "#give" and "(give)", and not on "forgiveness", "giveaway" * or "give_now". Phrases work too, bounded the same way on both ends — * something the helper cannot do, but nothing it would have matched is lost. */ const ZERO_WIDTH = /[​-‍⁠]/g; function cleanNotes(raw) { return String(raw ?? '').replace(ZERO_WIDTH, '').replace(/\r\n?/g, '\n'); } function norm(s) { return String(s ?? '').trim().toLowerCase().replace(/\s+/g, ' '); } function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function keywordMatches(notes, keyword) { const n = cleanNotes(notes); const k = norm(keyword); if (!k || !n.trim()) return false; const flat = n.toLowerCase().replace(/\s+/g, ' '); const re = new RegExp(`(?:^|[^\\p{L}\\p{N}_])${escapeRe(k)}(?=$|[^\\p{L}\\p{N}_])`, 'u'); return re.test(flat); } /** Every keyword the notes carry, in the order the list gives them, each once. Entries may be strings or objects with a `keyword`. */ function matchKeywords(notes, keywords) { const out = []; const seen = new Set(); for (const raw of keywords || []) { const k = typeof raw === 'string' ? raw : raw && raw.keyword; const key = norm(k); if (!key || seen.has(key)) continue; if (keywordMatches(notes, k)) { seen.add(key); out.push(String(k).trim()); } } return out; } function sameSet(a, b) { const x = [...(a || [])].map(norm).sort(); const y = [...(b || [])].map(norm).sort(); return x.length === y.length && x.every((v, i) => v === y[i]); } module.exports = { cleanNotes, norm, keywordMatches, matchKeywords, sameSet };