@rudra-js/attested 0.7.3 → 0.7.5

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/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 readings = new WeakMap();
4
- function unchanged(held, values) {
5
- if (held.length !== values.length)
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 < held.length; i += 1) {
8
- if (held[i] !== values[i])
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 valuesOf(facts) {
13
+ function supportedOf(facts) {
14
14
  const values = facts.values;
15
15
  if (!Array.isArray(values))
16
16
  return supportedValues(values);
17
- const read = readings.get(values);
18
- if (read !== undefined && unchanged(read.held, values))
19
- return read.supported;
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
- readings.set(values, { held: [...values], supported });
21
+ cache.set(values, { copy: [...values], supported });
22
22
  return supported;
23
23
  }
24
- function phrasesOf(facts) {
25
- const phrases = new Set(BANNED_PHRASES);
26
- for (const phrase of facts.bannedPhrases ?? []) {
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
- phrases.add(normalised);
29
+ out.push(normalised);
30
30
  }
31
- return [...phrases];
31
+ return out;
32
32
  }
33
- function allowedOf(facts) {
34
- const allowed = [];
35
- for (const phrase of facts.allowedPhrases ?? []) {
36
- const normalised = normalisePhrasing(phrase);
37
- if (normalised.length > 0)
38
- allowed.push(normalised);
39
- }
40
- return allowed;
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
- let backed = false;
57
- for (const form of numeral.forms) {
58
- if (supported.has(form))
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 standsBehind(allowed, hit) {
74
- for (const span of allowed) {
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 checkWording(text, phrases, allowed) {
81
- // Read twice, since Ν reads as N but ν as v; both readings are the same length, so spans line up.
82
- const texts = [normalisePhrasing(text)];
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(indexed, phrase)) {
93
- if (!normalised.slice(span.start, span.end + 1).includes('\n'))
94
- spans.push(span);
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
- const findings = [];
99
- for (const phrase of phrases) {
100
- let caught = false;
101
- for (const indexed of scans) {
102
- for (const hit of spansIn(indexed, phrase)) {
103
- if (!standsBehind(spans, hit))
104
- caught = true;
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
- if (!caught)
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: phrases.length,
121
+ checked: banned.length,
119
122
  findings,
120
123
  };
121
124
  }
122
- function check(text, supported, phrases, allowed) {
123
- const quantity = checkQuantities(text, supported);
124
- const wording = checkWording(text, phrases, allowed);
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, valuesOf(facts), phrasesOf(facts), allowedOf(facts));
131
+ return check(text, readFacts(facts));
129
132
  }
130
133
  export function verifyFields(fields, facts) {
131
- const supported = valuesOf(facts);
132
- const phrases = phrasesOf(facts);
133
- const allowed = allowedOf(facts);
134
+ const prepared = readFacts(facts);
134
135
  const results = [];
135
- const written = [];
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, supported, phrases, allowed);
140
+ const result = check(text, prepared);
140
141
  if (!result.supported)
141
142
  allSupported = false;
142
143
  results.push({ field, result });
143
- written.push(text);
144
+ texts.push(text);
144
145
  }
145
- const acrossFields = checkWording(written.join(' '), phrases, allowed);
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
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rudra-js/attested",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Checks model-written product copy against the facts a shop stands behind",
5
5
  "keywords": [
6
6
  "llm",
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ export {
10
10
  type VerifyResult,
11
11
  } from './verify.js';
12
12
 
13
- export { BANNED_PHRASES } from './phrases.js';
13
+ export { BANNED_PHRASES, foldLookalikes } from './phrases.js';
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 LOOSE = new RegExp(`[${GROUPING_ONLY}]`, 'u');
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 zeros = new Map<number, number>();
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 zero = zeros.get(code);
38
- if (zero === undefined) {
39
- zero = code;
40
- while (zero > 0 && IS_DIGIT.test(String.fromCodePoint(zero - 1))) zero -= 1;
41
- zeros.set(code, zero);
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
- return (code - zero) % 10;
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[groups.length - 1] ?? '').length !== 3) return false;
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
- if (three) return lead >= 1 && lead <= 3;
81
-
82
- if (two) return lead >= 1 && lead <= 2;
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 allGrouping(separators: string[]): boolean {
87
- for (const separator of separators) {
88
- if (separator !== separators[0]) return false;
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 groups: string[] = [];
98
- const separators: string[] = [];
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
- if (allGrouping(separators) && isGrouping(groups)) forms.push(canonical(groups.join(''), ''));
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 leading = separators.slice(0, -1);
118
- const point = separators[separators.length - 1] ?? '';
119
- const fraction = groups[groups.length - 1] ?? '';
120
-
121
- const groupingWins = forms.length > 0 && leading.length === 0 && fraction.length === 3;
122
-
123
- if (
124
- !groupingWins &&
125
- !GROUPING_ONLY.includes(point) &&
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 collect(found: Numeral[], token: string): void {
137
- if (!STARTS_DIGIT.test(token)) {
138
- found.push({ token, forms: [], kind: 'other-numeral' });
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
- found.push({ token, forms, kind: 'digits' });
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
- if (LOOSE.test(token)) {
153
- for (const part of token.split(LOOSE)) {
154
- if (part.length > 0) collect(found, part);
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 found: Numeral[] = [];
164
- for (const match of stripMarks(stripInvisible(text)).matchAll(SCAN)) collect(found, match[0]);
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 marker = written.search(/[eE]/);
174
- if (marker < 0) return written;
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 body = written.slice(0, marker).replace(/^[+-]/, '');
179
- const point = body.indexOf('.');
180
- const digits = point < 0 ? body : body.slice(0, point) + body.slice(point + 1);
181
- const place = (point < 0 ? body.length : point) + power;
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 digitsOf(value: string | number | bigint): string {
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(digitsOf(value))) {
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
@@ -61,6 +61,9 @@ export const BANNED_PHRASES: readonly string[] = [
61
61
  'limited time',
62
62
  'today only',
63
63
  'ends soon',
64
+ 'ends today',
65
+ 'ends tonight',
66
+ 'ends at midnight',
64
67
  'hurry',
65
68
  'act fast',
66
69
  'do not miss',
@@ -81,7 +84,45 @@ export const BANNED_PHRASES: readonly string[] = [
81
84
  'star rating',
82
85
  'customer favourite',
83
86
  'customer favorite',
87
+ 'trending',
88
+ 'others are looking',
89
+ 'people are looking',
90
+ 'others are viewing',
91
+ 'people are viewing',
92
+ 'others are buying',
93
+ 'people are buying',
94
+ 'in other carts',
95
+ 'in other baskets',
84
96
  'money back',
97
+ 'eco friendly',
98
+ 'environmentally friendly',
99
+ 'planet friendly',
100
+ 'earth friendly',
101
+ 'nature friendly',
102
+ 'climate friendly',
103
+ 'carbon friendly',
104
+ 'good for the planet',
105
+ 'kind to the planet',
106
+ 'gentle on the environment',
107
+ 'ecological',
108
+ 'ecologically',
109
+ 'environmentally correct',
110
+ 'eco conscious',
111
+ 'environmentally conscious',
112
+ 'sustainable',
113
+ 'sustainably',
114
+ 'biodegradable',
115
+ 'compostable',
116
+ 'biobased',
117
+ 'energy efficient',
118
+ 'carbon neutral',
119
+ 'climate neutral',
120
+ 'net zero',
121
+ 'carbon negative',
122
+ 'carbon positive',
123
+ 'climate positive',
124
+ 'carbon offset',
125
+ 'zero waste',
85
126
  ];
86
127
 
87
128
  const CONFUSABLES: Record<string, string> = {
@@ -117,7 +158,6 @@ const CONFUSABLES: Record<string, string> = {
117
158
  ѵ: 'v',
118
159
  };
119
160
 
120
- // Capitals that lowercasing reads wrong: Ν becomes ν, which reads as v.
121
161
  const CAPITALS: Record<string, string> = {
122
162
  Β: 'b',
123
163
  Ζ: 'z',
@@ -134,23 +174,28 @@ const CAPITALS: Record<string, string> = {
134
174
  Ԍ: 'g',
135
175
  };
136
176
 
177
+ const BEFORE_NFKC: Record<string, string> = {
178
+ ϲ: 'c',
179
+ Ϲ: 'c',
180
+ };
181
+
137
182
  const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
138
183
 
139
- export function normalisePhrasing(text: string, byLook = false): string {
140
- // NFKC would turn the lunate sigmas into ς and Σ, which the fold can't read as c.
141
- const compatible = stripInvisible(text).replace(/[ϲϹ]/g, 'c').normalize('NFKC');
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
+ }
142
189
 
143
- let cased = compatible;
144
- if (byLook) {
145
- cased = '';
146
- for (const char of compatible) cased += CAPITALS[char] ?? char;
147
- }
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;
148
193
  const lowered = stripMarks(cased.toLowerCase().normalize('NFC'));
194
+ return foldChars(lowered, CONFUSABLES);
195
+ }
149
196
 
150
- let folded = '';
151
- for (const char of lowered) folded += CONFUSABLES[char] ?? char;
152
-
153
- return folded
197
+ export function normalisePhrasing(text: string, byLook = false): string {
198
+ return foldLookalikes(text, byLook)
154
199
  .replace(/[\u2018\u2019]/g, "'")
155
200
  .replace(/[-_\u2010-\u2015]+/g, ' ')
156
201
  .replace(/\s+/g, (run) => (LINE_BREAK.test(run) ? '\n' : ' '))
@@ -165,37 +210,34 @@ function isGlued(char: string | undefined): boolean {
165
210
 
166
211
  export interface Span {
167
212
  start: number;
168
-
169
213
  end: number;
170
214
  }
171
215
 
172
216
  export interface Indexed {
173
217
  text: string;
174
-
175
- haystack: string;
176
-
177
- spots: number[];
218
+ squeezed: string;
219
+ offsets: number[];
178
220
  }
179
221
 
180
222
  export function indexPhrasing(text: string): Indexed {
181
- const tight: string[] = [];
182
- const spots: number[] = [];
223
+ const kept: string[] = [];
224
+ const offsets: number[] = [];
183
225
  for (let index = 0; index < text.length; index += 1) {
184
226
  const char = text[index] ?? '';
185
227
 
186
228
  if (char === ' ' || char === '\n') continue;
187
- tight.push(char);
188
- spots.push(index);
229
+ kept.push(char);
230
+ offsets.push(index);
189
231
  }
190
232
 
191
- return { text, haystack: tight.join(''), spots };
233
+ return { text, squeezed: kept.join(''), offsets };
192
234
  }
193
235
 
194
236
  export function spansIn(indexed: Indexed, phrase: string): Span[] {
195
- const { text, haystack, spots } = indexed;
237
+ const { text, squeezed, offsets } = indexed;
196
238
 
197
239
  const spans: Span[] = [];
198
- const needle = phrase.replaceAll(' ', '').replaceAll('\n', '');
240
+ const needle = indexPhrasing(phrase).squeezed;
199
241
  if (needle.length === 0) return spans;
200
242
 
201
243
  const guardStart = ASCII_WORD.test(needle[0] ?? '');
@@ -203,18 +245,18 @@ export function spansIn(indexed: Indexed, phrase: string): Span[] {
203
245
 
204
246
  let from = 0;
205
247
  for (;;) {
206
- const at = haystack.indexOf(needle, from);
248
+ const at = squeezed.indexOf(needle, from);
207
249
  if (at === -1) return spans;
208
250
 
209
- const start = spots[at] ?? 0;
210
- const end = spots[at + needle.length - 1] ?? 0;
251
+ const start = offsets[at] ?? 0;
252
+ const end = offsets[at + needle.length - 1] ?? 0;
211
253
  const next = text[end + 1];
212
254
 
213
255
  const plural = next === 's' && !isGlued(text[end + 2]);
214
256
 
215
- const before = guardStart && isGlued(text[start - 1]);
216
- const after = guardEnd && isGlued(next) && !plural;
217
- if (!before && !after) spans.push({ start, end });
257
+ const gluedBefore = guardStart && isGlued(text[start - 1]);
258
+ const gluedAfter = guardEnd && isGlued(next) && !plural;
259
+ if (!gluedBefore && !gluedAfter) spans.push({ start, end });
218
260
 
219
261
  from = at + 1;
220
262
  }