@rudra-js/attested 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/numerals.d.ts.map +1 -1
- package/dist/numerals.js +62 -88
- package/dist/numerals.js.map +1 -1
- package/dist/phrases.d.ts +3 -2
- package/dist/phrases.d.ts.map +1 -1
- package/dist/phrases.js +30 -27
- package/dist/phrases.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +67 -66
- package/dist/verify.js.map +1 -1
- package/package.json +6 -3
- package/src/index.ts +1 -1
- package/src/numerals.ts +52 -98
- package/src/phrases.ts +32 -31
- package/src/verify.ts +73 -77
package/dist/verify.js
CHANGED
|
@@ -1,43 +1,47 @@
|
|
|
1
1
|
import { numeralsIn, supportedValues } from './numerals.js';
|
|
2
2
|
import { BANNED_PHRASES, indexPhrasing, normalisePhrasing, spansIn, } from './phrases.js';
|
|
3
|
-
const
|
|
4
|
-
function
|
|
5
|
-
if (
|
|
3
|
+
const cache = new WeakMap();
|
|
4
|
+
function sameValues(a, b) {
|
|
5
|
+
if (a.length !== b.length)
|
|
6
6
|
return false;
|
|
7
|
-
for (let i = 0; i <
|
|
8
|
-
if (
|
|
7
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
8
|
+
if (a[i] !== b[i])
|
|
9
9
|
return false;
|
|
10
10
|
}
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function supportedOf(facts) {
|
|
14
14
|
const values = facts.values;
|
|
15
15
|
if (!Array.isArray(values))
|
|
16
16
|
return supportedValues(values);
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
return
|
|
17
|
+
const cached = cache.get(values);
|
|
18
|
+
if (cached !== undefined && sameValues(cached.copy, values))
|
|
19
|
+
return cached.supported;
|
|
20
20
|
const supported = supportedValues(values);
|
|
21
|
-
|
|
21
|
+
cache.set(values, { copy: [...values], supported });
|
|
22
22
|
return supported;
|
|
23
23
|
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
for (const phrase of
|
|
24
|
+
function normaliseAll(phrases) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const phrase of phrases ?? []) {
|
|
27
27
|
const normalised = normalisePhrasing(phrase);
|
|
28
28
|
if (normalised.length > 0)
|
|
29
|
-
|
|
29
|
+
out.push(normalised);
|
|
30
30
|
}
|
|
31
|
-
return
|
|
31
|
+
return out;
|
|
32
32
|
}
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
for (const phrase of facts.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return
|
|
33
|
+
function bannedOf(facts) {
|
|
34
|
+
const banned = new Set(BANNED_PHRASES);
|
|
35
|
+
for (const phrase of normaliseAll(facts.bannedPhrases))
|
|
36
|
+
banned.add(phrase);
|
|
37
|
+
return [...banned];
|
|
38
|
+
}
|
|
39
|
+
function readFacts(facts) {
|
|
40
|
+
return {
|
|
41
|
+
supported: supportedOf(facts),
|
|
42
|
+
banned: bannedOf(facts),
|
|
43
|
+
allowed: normaliseAll(facts.allowedPhrases),
|
|
44
|
+
};
|
|
41
45
|
}
|
|
42
46
|
function reasonFor(numeral) {
|
|
43
47
|
if (numeral.kind === 'other-numeral') {
|
|
@@ -53,12 +57,9 @@ function checkQuantities(text, supported) {
|
|
|
53
57
|
const findings = [];
|
|
54
58
|
const reported = new Set();
|
|
55
59
|
for (const numeral of numerals) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
backed = true;
|
|
60
|
-
}
|
|
61
|
-
if (backed || reported.has(numeral.token))
|
|
60
|
+
if (numeral.forms.some((form) => supported.has(form)))
|
|
61
|
+
continue;
|
|
62
|
+
if (reported.has(numeral.token))
|
|
62
63
|
continue;
|
|
63
64
|
reported.add(numeral.token);
|
|
64
65
|
findings.push({ layer: 'quantity', token: numeral.token, reason: reasonFor(numeral) });
|
|
@@ -70,41 +71,43 @@ function checkQuantities(text, supported) {
|
|
|
70
71
|
findings,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
function
|
|
74
|
-
for (const span of
|
|
74
|
+
function isInside(spans, hit) {
|
|
75
|
+
for (const span of spans) {
|
|
75
76
|
if (span.start <= hit.start && hit.end <= span.end)
|
|
76
77
|
return true;
|
|
77
78
|
}
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const byLook = normalisePhrasing(text, true);
|
|
84
|
-
if (byLook !== texts[0])
|
|
85
|
-
texts.push(byLook);
|
|
86
|
-
const scans = [];
|
|
87
|
-
const spans = [];
|
|
88
|
-
for (const normalised of texts) {
|
|
89
|
-
const indexed = indexPhrasing(normalised);
|
|
90
|
-
scans.push(indexed);
|
|
81
|
+
function forgivenInEitherReading(scans, allowed) {
|
|
82
|
+
const forgiven = [];
|
|
83
|
+
for (const scan of scans) {
|
|
91
84
|
for (const phrase of allowed) {
|
|
92
|
-
for (const span of spansIn(
|
|
93
|
-
if (!
|
|
94
|
-
|
|
85
|
+
for (const span of spansIn(scan, phrase)) {
|
|
86
|
+
if (!scan.text.slice(span.start, span.end + 1).includes('\n'))
|
|
87
|
+
forgiven.push(span);
|
|
95
88
|
}
|
|
96
89
|
}
|
|
97
90
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
91
|
+
return forgiven;
|
|
92
|
+
}
|
|
93
|
+
function isCaught(scans, phrase, forgiven) {
|
|
94
|
+
for (const scan of scans) {
|
|
95
|
+
for (const hit of spansIn(scan, phrase)) {
|
|
96
|
+
if (!isInside(forgiven, hit))
|
|
97
|
+
return true;
|
|
106
98
|
}
|
|
107
|
-
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
function checkWording(text, prepared) {
|
|
103
|
+
const { banned, allowed } = prepared;
|
|
104
|
+
const plain = normalisePhrasing(text);
|
|
105
|
+
const byLook = normalisePhrasing(text, true);
|
|
106
|
+
const scans = byLook === plain ? [indexPhrasing(plain)] : [indexPhrasing(plain), indexPhrasing(byLook)];
|
|
107
|
+
const forgiven = forgivenInEitherReading(scans, allowed);
|
|
108
|
+
const findings = [];
|
|
109
|
+
for (const phrase of banned) {
|
|
110
|
+
if (!isCaught(scans, phrase, forgiven))
|
|
108
111
|
continue;
|
|
109
112
|
findings.push({
|
|
110
113
|
layer: 'wording',
|
|
@@ -115,34 +118,32 @@ function checkWording(text, phrases, allowed) {
|
|
|
115
118
|
return {
|
|
116
119
|
supported: findings.length === 0,
|
|
117
120
|
strength: 'best-effort',
|
|
118
|
-
checked:
|
|
121
|
+
checked: banned.length,
|
|
119
122
|
findings,
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
|
-
function check(text,
|
|
123
|
-
const quantity = checkQuantities(text, supported);
|
|
124
|
-
const wording = checkWording(text,
|
|
125
|
+
function check(text, prepared) {
|
|
126
|
+
const quantity = checkQuantities(text, prepared.supported);
|
|
127
|
+
const wording = checkWording(text, prepared);
|
|
125
128
|
return { supported: quantity.supported && wording.supported, quantity, wording };
|
|
126
129
|
}
|
|
127
130
|
export function verify(text, facts) {
|
|
128
|
-
return check(text,
|
|
131
|
+
return check(text, readFacts(facts));
|
|
129
132
|
}
|
|
130
133
|
export function verifyFields(fields, facts) {
|
|
131
|
-
const
|
|
132
|
-
const phrases = phrasesOf(facts);
|
|
133
|
-
const allowed = allowedOf(facts);
|
|
134
|
+
const prepared = readFacts(facts);
|
|
134
135
|
const results = [];
|
|
135
|
-
const
|
|
136
|
+
const texts = [];
|
|
136
137
|
let allSupported = true;
|
|
137
138
|
for (const field of Object.keys(fields)) {
|
|
138
139
|
const text = fields[field] ?? '';
|
|
139
|
-
const result = check(text,
|
|
140
|
+
const result = check(text, prepared);
|
|
140
141
|
if (!result.supported)
|
|
141
142
|
allSupported = false;
|
|
142
143
|
results.push({ field, result });
|
|
143
|
-
|
|
144
|
+
texts.push(text);
|
|
144
145
|
}
|
|
145
|
-
const acrossFields = checkWording(
|
|
146
|
+
const acrossFields = checkWording(texts.join(' '), prepared);
|
|
146
147
|
return { supported: allSupported && acrossFields.supported, fields: results, acrossFields };
|
|
147
148
|
}
|
|
148
149
|
//# sourceMappingURL=verify.js.map
|
package/dist/verify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAgB,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,OAAO,GAGR,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/attested",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.6",
|
|
4
|
+
"description": "From rudra-js: checks model-written product copy against the facts a shop stands behind",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"rudra-js",
|
|
7
|
+
"product-recommendations",
|
|
6
8
|
"llm",
|
|
7
9
|
"hallucination",
|
|
8
10
|
"grounding",
|
|
9
11
|
"verification",
|
|
10
12
|
"ecommerce",
|
|
11
|
-
"guardrails"
|
|
13
|
+
"guardrails",
|
|
14
|
+
"typescript"
|
|
12
15
|
],
|
|
13
16
|
"license": "MIT",
|
|
14
17
|
"author": "Clive Dsouza",
|
package/src/index.ts
CHANGED
package/src/numerals.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import { stripInvisible, stripMarks } from './hidden.js';
|
|
2
2
|
|
|
3
3
|
const AMBIGUOUS = '.,';
|
|
4
|
-
|
|
5
4
|
const DECIMAL_ONLY = '\u066b';
|
|
6
|
-
|
|
7
5
|
const GROUPING_ONLY = '\u066c\u0020\u00a0\u2007\u2008\u2009\u202f\u0027\u2019';
|
|
8
|
-
|
|
9
6
|
const SEPARATORS = AMBIGUOUS + DECIMAL_ONLY + GROUPING_ONLY;
|
|
10
7
|
|
|
11
8
|
const RUN = `\\p{Nd}+(?:[${SEPARATORS}]\\p{Nd}+)*`;
|
|
12
|
-
|
|
13
9
|
const MAGNITUDE = '(?:[十百千万萬億亿兆]|[kKMB](?!\\p{L}))';
|
|
14
|
-
|
|
15
10
|
const OTHER_NUMERAL = '[\\p{No}\\p{Nl}]+';
|
|
16
11
|
|
|
17
12
|
const SCAN = new RegExp(`${RUN}${MAGNITUDE}|${RUN}|${OTHER_NUMERAL}`, 'gu');
|
|
18
|
-
const
|
|
13
|
+
const GROUPING_MARK = new RegExp(`[${GROUPING_ONLY}]`, 'u');
|
|
14
|
+
const SEPARATOR = new RegExp(`[${SEPARATORS}]`, 'gu');
|
|
19
15
|
|
|
20
16
|
const IS_DIGIT = /^\p{Nd}$/u;
|
|
21
17
|
const STARTS_DIGIT = /^\p{Nd}/u;
|
|
@@ -23,25 +19,24 @@ const ENDS_DIGIT = /\p{Nd}$/u;
|
|
|
23
19
|
|
|
24
20
|
export interface Numeral {
|
|
25
21
|
token: string;
|
|
26
|
-
|
|
27
22
|
forms: string[];
|
|
28
|
-
|
|
29
23
|
kind: 'digits' | 'magnitude' | 'other-numeral';
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
const
|
|
26
|
+
const runStartOf = new Map<number, number>();
|
|
33
27
|
|
|
34
28
|
function digitValue(char: string): number {
|
|
35
29
|
const code = char.codePointAt(0) ?? 0;
|
|
36
30
|
|
|
37
|
-
let
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
while (
|
|
41
|
-
|
|
31
|
+
let runStart = runStartOf.get(code);
|
|
32
|
+
if (runStart === undefined) {
|
|
33
|
+
runStart = code;
|
|
34
|
+
while (runStart > 0 && IS_DIGIT.test(String.fromCodePoint(runStart - 1))) runStart -= 1;
|
|
35
|
+
runStartOf.set(code, runStart);
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
const offsetInRun = code - runStart;
|
|
39
|
+
return offsetInRun % 10;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
function toAscii(token: string): string {
|
|
@@ -66,119 +61,78 @@ function canonical(whole: string, fraction: string): string {
|
|
|
66
61
|
|
|
67
62
|
function isGrouping(groups: string[]): boolean {
|
|
68
63
|
if (groups.length === 1) return true;
|
|
69
|
-
if ((groups
|
|
70
|
-
|
|
71
|
-
let three = true;
|
|
72
|
-
let two = true;
|
|
73
|
-
for (let index = 1; index < groups.length - 1; index += 1) {
|
|
74
|
-
const size = (groups[index] ?? '').length;
|
|
75
|
-
if (size !== 3) three = false;
|
|
76
|
-
if (size !== 2) two = false;
|
|
77
|
-
}
|
|
64
|
+
if ((groups.at(-1) ?? '').length !== 3) return false;
|
|
78
65
|
|
|
79
66
|
const lead = (groups[0] ?? '').length;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
67
|
+
const middle = groups.slice(1, -1);
|
|
68
|
+
if (middle.every((group) => group.length === 3)) return lead >= 1 && lead <= 3;
|
|
69
|
+
if (middle.every((group) => group.length === 2)) return lead >= 1 && lead <= 2;
|
|
83
70
|
return false;
|
|
84
71
|
}
|
|
85
72
|
|
|
86
|
-
function
|
|
87
|
-
for (const
|
|
88
|
-
if (
|
|
89
|
-
if (DECIMAL_ONLY.includes(separator)) return false;
|
|
73
|
+
function sameGroupingMark(marks: string[]): boolean {
|
|
74
|
+
for (const mark of marks) {
|
|
75
|
+
if (mark !== marks[0] || mark === DECIMAL_ONLY) return false;
|
|
90
76
|
}
|
|
91
77
|
return true;
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
function formsOf(token: string): string[] {
|
|
95
81
|
const ascii = toAscii(token);
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
let current = '';
|
|
100
|
-
for (const char of ascii) {
|
|
101
|
-
if (SEPARATORS.includes(char)) {
|
|
102
|
-
groups.push(current);
|
|
103
|
-
separators.push(char);
|
|
104
|
-
current = '';
|
|
105
|
-
} else {
|
|
106
|
-
current += char;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
groups.push(current);
|
|
110
|
-
|
|
111
|
-
if (separators.length === 0) return [canonical(groups[0] ?? '', '')];
|
|
82
|
+
const groups = ascii.split(SEPARATOR);
|
|
83
|
+
const marks = ascii.match(SEPARATOR) ?? [];
|
|
84
|
+
if (marks.length === 0) return [canonical(ascii, '')];
|
|
112
85
|
|
|
113
86
|
const forms: string[] = [];
|
|
114
|
-
|
|
87
|
+
const grouped = sameGroupingMark(marks) && isGrouping(groups);
|
|
88
|
+
if (grouped) forms.push(canonical(groups.join(''), ''));
|
|
115
89
|
|
|
116
90
|
const whole = groups.slice(0, -1);
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
allGrouping(leading) &&
|
|
127
|
-
leading[0] !== point &&
|
|
128
|
-
isGrouping(whole)
|
|
129
|
-
) {
|
|
130
|
-
forms.push(canonical(whole.join(''), fraction));
|
|
131
|
-
}
|
|
91
|
+
const fraction = groups.at(-1) ?? '';
|
|
92
|
+
const groupingMarks = marks.slice(0, -1);
|
|
93
|
+
const decimalMark = marks.at(-1) ?? '';
|
|
94
|
+
|
|
95
|
+
if (grouped && groupingMarks.length === 0 && fraction.length === 3) return forms;
|
|
96
|
+
if (GROUPING_ONLY.includes(decimalMark)) return forms;
|
|
97
|
+
if (!sameGroupingMark(groupingMarks)) return forms;
|
|
98
|
+
if (groupingMarks[0] === decimalMark) return forms;
|
|
99
|
+
if (!isGrouping(whole)) return forms;
|
|
132
100
|
|
|
101
|
+
forms.push(canonical(whole.join(''), fraction));
|
|
133
102
|
return forms;
|
|
134
103
|
}
|
|
135
104
|
|
|
136
|
-
function
|
|
137
|
-
if (!STARTS_DIGIT.test(token)) {
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (!ENDS_DIGIT.test(token)) {
|
|
142
|
-
found.push({ token, forms: [], kind: 'magnitude' });
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
105
|
+
function numeralsOf(token: string): Numeral[] {
|
|
106
|
+
if (!STARTS_DIGIT.test(token)) return [{ token, forms: [], kind: 'other-numeral' }];
|
|
107
|
+
if (!ENDS_DIGIT.test(token)) return [{ token, forms: [], kind: 'magnitude' }];
|
|
145
108
|
|
|
146
109
|
const forms = formsOf(token);
|
|
147
|
-
if (forms.length > 0) {
|
|
148
|
-
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
110
|
+
if (forms.length > 0) return [{ token, forms, kind: 'digits' }];
|
|
111
|
+
if (!GROUPING_MARK.test(token)) return [{ token, forms: [toAscii(token)], kind: 'digits' }];
|
|
151
112
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
found.push({ token, forms: [toAscii(token)], kind: 'digits' });
|
|
113
|
+
return token
|
|
114
|
+
.split(GROUPING_MARK)
|
|
115
|
+
.filter((part) => part.length > 0)
|
|
116
|
+
.flatMap((part) => numeralsOf(part));
|
|
160
117
|
}
|
|
161
118
|
|
|
162
119
|
export function numeralsIn(text: string): Numeral[] {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
return found;
|
|
120
|
+
const visible = stripMarks(stripInvisible(text));
|
|
121
|
+
return [...visible.matchAll(SCAN)].flatMap((match) => numeralsOf(match[0]));
|
|
166
122
|
}
|
|
167
123
|
|
|
168
124
|
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/;
|
|
169
|
-
|
|
170
125
|
const DIGIT_LIMIT = 1000;
|
|
171
126
|
|
|
172
127
|
function positional(written: string): string {
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
const power = Number(written.slice(marker + 1));
|
|
128
|
+
const exponentAt = written.search(/[eE]/);
|
|
129
|
+
if (exponentAt < 0) return written;
|
|
177
130
|
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
131
|
+
const power = Number(written.slice(exponentAt + 1));
|
|
132
|
+
const mantissa = written.slice(0, exponentAt).replace(/^[+-]/, '');
|
|
133
|
+
const dot = mantissa.indexOf('.');
|
|
134
|
+
const digits = mantissa.replace('.', '');
|
|
135
|
+
const place = (dot < 0 ? mantissa.length : dot) + power;
|
|
182
136
|
|
|
183
137
|
if (place <= 0) {
|
|
184
138
|
if (digits.length - place > DIGIT_LIMIT) return '';
|
|
@@ -191,7 +145,7 @@ function positional(written: string): string {
|
|
|
191
145
|
return `${digits.slice(0, place)}.${digits.slice(place)}`;
|
|
192
146
|
}
|
|
193
147
|
|
|
194
|
-
function
|
|
148
|
+
function textOf(value: string | number | bigint): string {
|
|
195
149
|
if (typeof value === 'number') return positional(String(value));
|
|
196
150
|
if (typeof value !== 'string') return String(value);
|
|
197
151
|
|
|
@@ -202,7 +156,7 @@ function digitsOf(value: string | number | bigint): string {
|
|
|
202
156
|
export function supportedValues(values: readonly (string | number | bigint)[]): Set<string> {
|
|
203
157
|
const supported = new Set<string>();
|
|
204
158
|
for (const value of values) {
|
|
205
|
-
for (const numeral of numeralsIn(
|
|
159
|
+
for (const numeral of numeralsIn(textOf(value))) {
|
|
206
160
|
for (const form of numeral.forms) supported.add(form);
|
|
207
161
|
}
|
|
208
162
|
}
|
package/src/phrases.ts
CHANGED
|
@@ -158,7 +158,6 @@ const CONFUSABLES: Record<string, string> = {
|
|
|
158
158
|
ѵ: 'v',
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
// Capitals that lowercasing reads wrong: Ν becomes ν, which reads as v.
|
|
162
161
|
const CAPITALS: Record<string, string> = {
|
|
163
162
|
Β: 'b',
|
|
164
163
|
Ζ: 'z',
|
|
@@ -175,23 +174,28 @@ const CAPITALS: Record<string, string> = {
|
|
|
175
174
|
Ԍ: 'g',
|
|
176
175
|
};
|
|
177
176
|
|
|
177
|
+
const BEFORE_NFKC: Record<string, string> = {
|
|
178
|
+
ϲ: 'c',
|
|
179
|
+
Ϲ: 'c',
|
|
180
|
+
};
|
|
181
|
+
|
|
178
182
|
const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
184
|
+
function foldChars(text: string, table: Record<string, string>): string {
|
|
185
|
+
let folded = '';
|
|
186
|
+
for (const char of text) folded += table[char] ?? char;
|
|
187
|
+
return folded;
|
|
188
|
+
}
|
|
183
189
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
for (const char of compatible) cased += CAPITALS[char] ?? char;
|
|
188
|
-
}
|
|
190
|
+
export function foldLookalikes(text: string, byLook = false): string {
|
|
191
|
+
const compatible = foldChars(stripInvisible(text), BEFORE_NFKC).normalize('NFKC');
|
|
192
|
+
const cased = byLook ? foldChars(compatible, CAPITALS) : compatible;
|
|
189
193
|
const lowered = stripMarks(cased.toLowerCase().normalize('NFC'));
|
|
194
|
+
return foldChars(lowered, CONFUSABLES);
|
|
195
|
+
}
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return folded
|
|
197
|
+
export function normalisePhrasing(text: string, byLook = false): string {
|
|
198
|
+
return foldLookalikes(text, byLook)
|
|
195
199
|
.replace(/[\u2018\u2019]/g, "'")
|
|
196
200
|
.replace(/[-_\u2010-\u2015]+/g, ' ')
|
|
197
201
|
.replace(/\s+/g, (run) => (LINE_BREAK.test(run) ? '\n' : ' '))
|
|
@@ -206,37 +210,34 @@ function isGlued(char: string | undefined): boolean {
|
|
|
206
210
|
|
|
207
211
|
export interface Span {
|
|
208
212
|
start: number;
|
|
209
|
-
|
|
210
213
|
end: number;
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
export interface Indexed {
|
|
214
217
|
text: string;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
spots: number[];
|
|
218
|
+
squeezed: string;
|
|
219
|
+
offsets: number[];
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
export function indexPhrasing(text: string): Indexed {
|
|
222
|
-
const
|
|
223
|
-
const
|
|
223
|
+
const kept: string[] = [];
|
|
224
|
+
const offsets: number[] = [];
|
|
224
225
|
for (let index = 0; index < text.length; index += 1) {
|
|
225
226
|
const char = text[index] ?? '';
|
|
226
227
|
|
|
227
228
|
if (char === ' ' || char === '\n') continue;
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
kept.push(char);
|
|
230
|
+
offsets.push(index);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
return { text,
|
|
233
|
+
return { text, squeezed: kept.join(''), offsets };
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
export function spansIn(indexed: Indexed, phrase: string): Span[] {
|
|
236
|
-
const { text,
|
|
237
|
+
const { text, squeezed, offsets } = indexed;
|
|
237
238
|
|
|
238
239
|
const spans: Span[] = [];
|
|
239
|
-
const needle = phrase
|
|
240
|
+
const needle = indexPhrasing(phrase).squeezed;
|
|
240
241
|
if (needle.length === 0) return spans;
|
|
241
242
|
|
|
242
243
|
const guardStart = ASCII_WORD.test(needle[0] ?? '');
|
|
@@ -244,18 +245,18 @@ export function spansIn(indexed: Indexed, phrase: string): Span[] {
|
|
|
244
245
|
|
|
245
246
|
let from = 0;
|
|
246
247
|
for (;;) {
|
|
247
|
-
const at =
|
|
248
|
+
const at = squeezed.indexOf(needle, from);
|
|
248
249
|
if (at === -1) return spans;
|
|
249
250
|
|
|
250
|
-
const start =
|
|
251
|
-
const end =
|
|
251
|
+
const start = offsets[at] ?? 0;
|
|
252
|
+
const end = offsets[at + needle.length - 1] ?? 0;
|
|
252
253
|
const next = text[end + 1];
|
|
253
254
|
|
|
254
255
|
const plural = next === 's' && !isGlued(text[end + 2]);
|
|
255
256
|
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
if (!
|
|
257
|
+
const gluedBefore = guardStart && isGlued(text[start - 1]);
|
|
258
|
+
const gluedAfter = guardEnd && isGlued(next) && !plural;
|
|
259
|
+
if (!gluedBefore && !gluedAfter) spans.push({ start, end });
|
|
259
260
|
|
|
260
261
|
from = at + 1;
|
|
261
262
|
}
|