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);