Spaces:
Running
Running
File size: 3,327 Bytes
27e064c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | // 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);
|