// Browser port of cascade.py: RuleChef's executor semantics for regex NER rules, and the gate. // Checked against the Python executor on every request and every rule set of this demo (see verify_engine.js). (function (root) { "use strict"; const cache = new Map(); function compile(src) { if (!cache.has(src)) { let re = null; try { re = new RegExp(src, "g"); } catch (e) { re = null; } cache.set(src, re); } return cache.get(src); } function sortKey(a, b) { const pa = a.priority ?? 5, pb = b.priority ?? 5; if (pa !== pb) return pb - pa; const qa = a.validated_precision ?? a.confidence ?? 0.5, qb = b.validated_precision ?? b.confidence ?? 0.5; if (qa !== qb) return qb - qa; const na = (a.name || "").toLowerCase(), nb = (b.name || "").toLowerCase(); if (na !== nb) return na < nb ? -1 : 1; const ia = a.id || "", ib = b.id || ""; return ia < ib ? -1 : ia > ib ? 1 : 0; } // rules: RuleChef rule dicts (content, priority, output_template {text, type}, ...) function applyRules(rules, text) { const out = []; for (const r of rules.slice().sort(sortKey)) { if ((r.format || "regex") !== "regex") continue; const re = compile(r.content); if (!re) continue; const tpl = r.output_template || {}; const t = tpl.text || "$0"; re.lastIndex = 0; for (const m of text.matchAll(re)) { const mt = m[0]; let s = m.index, e = m.index + mt.length, val = mt; if (/^\$\d+$/.test(t)) { const g = parseInt(t.slice(1), 10); val = g === 0 ? mt : (m[g] ?? ""); } if (val && val !== mt) { const off = mt.toLowerCase().indexOf(val.toLowerCase()); if (off >= 0) { s = s + off; e = s + val.length; } } if (out.some((x) => x.start === s && x.end === e)) continue; out.push({ start: s, end: e, type: tpl.type, text: val, rule_id: r.id }); } } const uniq = []; for (const it of out) { const dup = uniq.some((ex) => { const inter = Math.max(0, Math.min(it.end, ex.end) - Math.max(it.start, ex.start)); const uni = (it.end - it.start) + (ex.end - ex.start) - inter; return uni > 0 && inter / uni > 0.7 && it.type === ex.type; }); if (!dup) uniq.push(it); } return uniq; } const TOK = /ยง|[A-Za-z]+|\d+(?:[.,\/:-]\d+)*/g; function tokens(text) { const out = []; let prev = ""; for (const m of text.matchAll(TOK)) { out.push([m.index, m.index + m[0].length, m[0], prev]); prev = m[0].toLowerCase(); } return out; } const shape = (tok) => tok.replace(/\d+/g, "9"); // gate: {trigger_words: [...], skip: [[prev, shape], ...]} function unexplained(gate, text, spans) { const words = new Set(gate.trigger_words), skip = new Set(gate.skip.map((k) => k[0] + "\u0000" + k[1])); const out = []; for (const [s, e, tok, prev] of tokens(text)) { const digit = /^\d/.test(tok); if (!(digit || words.has(tok))) continue; if (spans.some((sp) => sp.start < e && s < sp.end)) continue; if (digit && skip.has(prev + "\u0000" + shape(tok))) continue; out.push(tok); } return out; } root.RCEngine = { applyRules, unexplained, compile }; })(typeof window !== "undefined" ? window : globalThis);