morphex 0.0.0-stage → 1.0.0
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/BENCHMARKS.md +95 -0
- package/LICENSE +25 -0
- package/NOTICE +23 -0
- package/README.md +197 -2
- package/dist/full.cjs +1798 -0
- package/dist/full.d.cts +12 -0
- package/dist/full.d.ts +12 -0
- package/dist/full.js +1780 -0
- package/dist/index.cjs +1784 -0
- package/dist/index.d.cts +257 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +1767 -0
- package/dist/rules.cjs +1784 -0
- package/dist/rules.js +1767 -0
- package/package.json +101 -4
package/dist/rules.cjs
ADDED
|
@@ -0,0 +1,1784 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*! morphex — MIT code; bundled lexicon data is CC BY-SA 3.0 (Wiktionary contributors via Kaikki.org/Wiktextract). See NOTICE. */
|
|
4
|
+
|
|
5
|
+
// src/compound-table.ts
|
|
6
|
+
var COMPOUND_EXACT = {
|
|
7
|
+
"passer-by": "passers-by",
|
|
8
|
+
"hanger-on": "hangers-on",
|
|
9
|
+
"looker-on": "lookers-on",
|
|
10
|
+
"runner-up": "runners-up",
|
|
11
|
+
"stand-in": "stand-ins",
|
|
12
|
+
"drive-by": "drive-bys",
|
|
13
|
+
"tie-in": "tie-ins",
|
|
14
|
+
"close-up": "close-ups",
|
|
15
|
+
"grown-up": "grown-ups",
|
|
16
|
+
"go-between": "go-betweens",
|
|
17
|
+
"forget-me-not": "forget-me-nots",
|
|
18
|
+
"jack-in-the-box": "jack-in-the-boxes",
|
|
19
|
+
"man-of-war": "men-of-war",
|
|
20
|
+
"major general": "major generals"
|
|
21
|
+
};
|
|
22
|
+
var FREE_TAILS = [
|
|
23
|
+
"in-law",
|
|
24
|
+
"in-arms",
|
|
25
|
+
"in-aid",
|
|
26
|
+
"in-kind",
|
|
27
|
+
"in-chief",
|
|
28
|
+
"of-war",
|
|
29
|
+
"of-arms",
|
|
30
|
+
"of-spring",
|
|
31
|
+
"of-all-trades",
|
|
32
|
+
"out-law",
|
|
33
|
+
"at-arms",
|
|
34
|
+
"at-law",
|
|
35
|
+
"in-crime"
|
|
36
|
+
];
|
|
37
|
+
var POSTPOSED = [
|
|
38
|
+
"general",
|
|
39
|
+
"elect",
|
|
40
|
+
"apparent",
|
|
41
|
+
"designate",
|
|
42
|
+
"martial",
|
|
43
|
+
"laureate",
|
|
44
|
+
"presumptive",
|
|
45
|
+
"public",
|
|
46
|
+
"errant"
|
|
47
|
+
];
|
|
48
|
+
var CURATED_HEADS = [
|
|
49
|
+
"attorney",
|
|
50
|
+
"court",
|
|
51
|
+
"heir",
|
|
52
|
+
"notary",
|
|
53
|
+
"poet",
|
|
54
|
+
"governor",
|
|
55
|
+
"ambassador",
|
|
56
|
+
"president",
|
|
57
|
+
"mayor",
|
|
58
|
+
"minister",
|
|
59
|
+
"secretary",
|
|
60
|
+
"treasurer",
|
|
61
|
+
"surgeon",
|
|
62
|
+
"solicitor",
|
|
63
|
+
"lord",
|
|
64
|
+
"postmaster",
|
|
65
|
+
"editor",
|
|
66
|
+
"commander"
|
|
67
|
+
];
|
|
68
|
+
var PREPOSITIONS = [
|
|
69
|
+
"in",
|
|
70
|
+
"of",
|
|
71
|
+
"at",
|
|
72
|
+
"on",
|
|
73
|
+
"by",
|
|
74
|
+
"to",
|
|
75
|
+
"with",
|
|
76
|
+
"for",
|
|
77
|
+
"from",
|
|
78
|
+
"upon",
|
|
79
|
+
"under",
|
|
80
|
+
"over",
|
|
81
|
+
"into"
|
|
82
|
+
];
|
|
83
|
+
var NON_NOUN_HEADS = [
|
|
84
|
+
"known",
|
|
85
|
+
"born",
|
|
86
|
+
"bred",
|
|
87
|
+
"built",
|
|
88
|
+
"done",
|
|
89
|
+
"gone",
|
|
90
|
+
"held",
|
|
91
|
+
"left",
|
|
92
|
+
"lost",
|
|
93
|
+
"made",
|
|
94
|
+
"paid",
|
|
95
|
+
"sold",
|
|
96
|
+
"told",
|
|
97
|
+
"worn",
|
|
98
|
+
"written",
|
|
99
|
+
"spoken",
|
|
100
|
+
"taken",
|
|
101
|
+
"given",
|
|
102
|
+
"seen",
|
|
103
|
+
"heard",
|
|
104
|
+
"led",
|
|
105
|
+
"won",
|
|
106
|
+
"set",
|
|
107
|
+
"put",
|
|
108
|
+
"cut",
|
|
109
|
+
"read",
|
|
110
|
+
"run",
|
|
111
|
+
"come",
|
|
112
|
+
"become",
|
|
113
|
+
"beaten",
|
|
114
|
+
"broken",
|
|
115
|
+
"chosen",
|
|
116
|
+
"driven",
|
|
117
|
+
"eaten",
|
|
118
|
+
"fallen",
|
|
119
|
+
"forgotten",
|
|
120
|
+
"frozen",
|
|
121
|
+
"hidden",
|
|
122
|
+
"risen",
|
|
123
|
+
"shaken",
|
|
124
|
+
"shown",
|
|
125
|
+
"stolen",
|
|
126
|
+
"sworn",
|
|
127
|
+
"torn",
|
|
128
|
+
"woken",
|
|
129
|
+
"wrong",
|
|
130
|
+
"aware",
|
|
131
|
+
"alike",
|
|
132
|
+
"alone",
|
|
133
|
+
"asleep",
|
|
134
|
+
"awake"
|
|
135
|
+
];
|
|
136
|
+
var PARTICLES = [
|
|
137
|
+
"off",
|
|
138
|
+
"in",
|
|
139
|
+
"on",
|
|
140
|
+
"up",
|
|
141
|
+
"out",
|
|
142
|
+
"over",
|
|
143
|
+
"back",
|
|
144
|
+
"down",
|
|
145
|
+
"through",
|
|
146
|
+
"around",
|
|
147
|
+
"about",
|
|
148
|
+
"away"
|
|
149
|
+
];
|
|
150
|
+
var COORDINATORS = ["and", "or", "nor", "but", "the"];
|
|
151
|
+
var COMPOUND_PREFIXES = ["on"];
|
|
152
|
+
var MAN_EXCEPTIONS = [
|
|
153
|
+
"human",
|
|
154
|
+
"german",
|
|
155
|
+
"roman",
|
|
156
|
+
"shaman",
|
|
157
|
+
"talisman",
|
|
158
|
+
"caiman",
|
|
159
|
+
"ottoman",
|
|
160
|
+
"brahman",
|
|
161
|
+
"atman",
|
|
162
|
+
"liman",
|
|
163
|
+
"chroman",
|
|
164
|
+
"hooman",
|
|
165
|
+
"kundiman",
|
|
166
|
+
"laraman",
|
|
167
|
+
"oralman",
|
|
168
|
+
"abhuman",
|
|
169
|
+
"metahuman",
|
|
170
|
+
"paleohuman",
|
|
171
|
+
"parahuman",
|
|
172
|
+
"dragoman",
|
|
173
|
+
"firman",
|
|
174
|
+
"harman",
|
|
175
|
+
"tarman",
|
|
176
|
+
"turkman",
|
|
177
|
+
"touman",
|
|
178
|
+
"tramman",
|
|
179
|
+
"vataman",
|
|
180
|
+
"velociman",
|
|
181
|
+
"zygentoman"
|
|
182
|
+
];
|
|
183
|
+
var freeTailSet = new Set(FREE_TAILS);
|
|
184
|
+
var postposedSet = new Set(POSTPOSED);
|
|
185
|
+
var curatedHeadSet = new Set(CURATED_HEADS);
|
|
186
|
+
var prepositionSet = new Set(PREPOSITIONS);
|
|
187
|
+
var nonNounHeadSet = new Set(NON_NOUN_HEADS);
|
|
188
|
+
var particleSet = new Set(PARTICLES);
|
|
189
|
+
var coordinatorSet = new Set(COORDINATORS);
|
|
190
|
+
var compoundPrefixSet = new Set(COMPOUND_PREFIXES);
|
|
191
|
+
var manExceptionSet = new Set(MAN_EXCEPTIONS);
|
|
192
|
+
function validateCompoundTable() {
|
|
193
|
+
for (const [key, plural] of Object.entries(COMPOUND_EXACT)) {
|
|
194
|
+
if (key !== key.toLowerCase() || plural !== plural.toLowerCase()) {
|
|
195
|
+
throw new Error(`compound table: exact entry must be lower case: ${key} -> ${plural}`);
|
|
196
|
+
}
|
|
197
|
+
if (!key.includes("-") && !key.includes(" ")) {
|
|
198
|
+
throw new Error(`compound table: exact entry must be a compound: ${key}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const tail of FREE_TAILS) {
|
|
202
|
+
if (!tail.includes("-") || tail.startsWith("-") || tail.endsWith("-") || tail !== tail.toLowerCase()) {
|
|
203
|
+
throw new Error(`compound table: malformed free tail: ${tail}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const token of [
|
|
207
|
+
...POSTPOSED,
|
|
208
|
+
...CURATED_HEADS,
|
|
209
|
+
...PREPOSITIONS,
|
|
210
|
+
...NON_NOUN_HEADS,
|
|
211
|
+
...PARTICLES,
|
|
212
|
+
...COORDINATORS,
|
|
213
|
+
...COMPOUND_PREFIXES,
|
|
214
|
+
...MAN_EXCEPTIONS
|
|
215
|
+
]) {
|
|
216
|
+
if (token !== token.toLowerCase() || token.length === 0) {
|
|
217
|
+
throw new Error(`compound table: malformed token: ${token}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const token of [...POSTPOSED, ...CURATED_HEADS, ...PREPOSITIONS, ...COMPOUND_PREFIXES]) {
|
|
221
|
+
if (token.includes("-") || token.includes(" ")) {
|
|
222
|
+
throw new Error(`compound table: multi-token word in a word set: ${token}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
validateCompoundTable();
|
|
227
|
+
function exactPlural(lowerCase) {
|
|
228
|
+
return Object.prototype.hasOwnProperty.call(COMPOUND_EXACT, lowerCase) ? COMPOUND_EXACT[lowerCase] : void 0;
|
|
229
|
+
}
|
|
230
|
+
function isFreeTail(tailLower) {
|
|
231
|
+
return freeTailSet.has(tailLower);
|
|
232
|
+
}
|
|
233
|
+
function freeTailSuffix(lowerCase) {
|
|
234
|
+
let best = -1;
|
|
235
|
+
for (const tail of FREE_TAILS) {
|
|
236
|
+
const index = lowerCase.length - tail.length - 1;
|
|
237
|
+
if (index > 0 && index > best && lowerCase.charAt(index) === "-" && lowerCase.endsWith(tail)) {
|
|
238
|
+
best = index;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return best;
|
|
242
|
+
}
|
|
243
|
+
function isPostposed(tailLower) {
|
|
244
|
+
return postposedSet.has(tailLower);
|
|
245
|
+
}
|
|
246
|
+
function isCuratedHead(headLower) {
|
|
247
|
+
return curatedHeadSet.has(headLower);
|
|
248
|
+
}
|
|
249
|
+
function isPreposition(tokenLower) {
|
|
250
|
+
return prepositionSet.has(tokenLower);
|
|
251
|
+
}
|
|
252
|
+
function isNonNounHead(tokenLower) {
|
|
253
|
+
return nonNounHeadSet.has(tokenLower);
|
|
254
|
+
}
|
|
255
|
+
function isParticle(tokenLower) {
|
|
256
|
+
return particleSet.has(tokenLower);
|
|
257
|
+
}
|
|
258
|
+
function isManException(tokenLower) {
|
|
259
|
+
return manExceptionSet.has(tokenLower) || tokenLower.endsWith("human");
|
|
260
|
+
}
|
|
261
|
+
function isCoordinator(tokenLower) {
|
|
262
|
+
return coordinatorSet.has(tokenLower);
|
|
263
|
+
}
|
|
264
|
+
function isCompoundPrefix(tokenLower) {
|
|
265
|
+
return compoundPrefixSet.has(tokenLower);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// morphex-rules-only:empty-lexicon
|
|
269
|
+
var BUNDLED_DELTA_LEXICON = "morphex-lexicon-v1 delta 0\n";
|
|
270
|
+
|
|
271
|
+
// src/types.ts
|
|
272
|
+
var INFLECTION_SOURCES = [
|
|
273
|
+
"OVERRIDE",
|
|
274
|
+
"LEXICON",
|
|
275
|
+
"INFERRED",
|
|
276
|
+
"COMPOUND",
|
|
277
|
+
"RULE",
|
|
278
|
+
"FALLBACK"
|
|
279
|
+
];
|
|
280
|
+
var COUNTABILITIES = ["COUNTABLE", "MASS", "MIXED", "PLURAL_ONLY", "UNKNOWN"];
|
|
281
|
+
|
|
282
|
+
// src/word-shape.ts
|
|
283
|
+
var LETTER = new RegExp("^\\p{L}$", "u");
|
|
284
|
+
var LATIN_SCRIPT = new RegExp("^\\p{Script=Latin}$", "u");
|
|
285
|
+
var NON_SPACING_MARK = new RegExp("^\\p{Mn}$", "u");
|
|
286
|
+
function isAsciiLetterCodePoint(codePoint) {
|
|
287
|
+
return codePoint >= 65 && codePoint <= 90 || codePoint >= 97 && codePoint <= 122;
|
|
288
|
+
}
|
|
289
|
+
function isUpperCaseCodePoint(character) {
|
|
290
|
+
const upper = character.toUpperCase();
|
|
291
|
+
return upper === character && character.toLowerCase() !== character;
|
|
292
|
+
}
|
|
293
|
+
function lower(token) {
|
|
294
|
+
return hasUpper(token) ? token.toLowerCase() : token;
|
|
295
|
+
}
|
|
296
|
+
function hasUpper(token) {
|
|
297
|
+
for (const character of token) {
|
|
298
|
+
if (character.length === 1) {
|
|
299
|
+
const codePoint = character.charCodeAt(0);
|
|
300
|
+
if (codePoint >= 65 && codePoint <= 90) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (codePoint >= 128 && isUpperCaseCodePoint(character)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
} else if (isUpperCaseCodePoint(character)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
function casingOf(token) {
|
|
313
|
+
let anyUpper = false;
|
|
314
|
+
let anyLower = false;
|
|
315
|
+
let firstLetterIndex = -1;
|
|
316
|
+
let firstCodePoint = "";
|
|
317
|
+
let index = 0;
|
|
318
|
+
for (const character of token) {
|
|
319
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
320
|
+
if (codePoint < 128) {
|
|
321
|
+
if (codePoint >= 65 && codePoint <= 90) {
|
|
322
|
+
if (firstLetterIndex < 0) {
|
|
323
|
+
firstLetterIndex = index;
|
|
324
|
+
firstCodePoint = character;
|
|
325
|
+
}
|
|
326
|
+
anyUpper = true;
|
|
327
|
+
} else if (codePoint >= 97 && codePoint <= 122) {
|
|
328
|
+
if (firstLetterIndex < 0) {
|
|
329
|
+
firstLetterIndex = index;
|
|
330
|
+
firstCodePoint = character;
|
|
331
|
+
}
|
|
332
|
+
anyLower = true;
|
|
333
|
+
}
|
|
334
|
+
} else if (LETTER.test(character)) {
|
|
335
|
+
if (firstLetterIndex < 0) {
|
|
336
|
+
firstLetterIndex = index;
|
|
337
|
+
firstCodePoint = character;
|
|
338
|
+
}
|
|
339
|
+
if (isUpperCaseCodePoint(character)) {
|
|
340
|
+
anyUpper = true;
|
|
341
|
+
} else {
|
|
342
|
+
anyLower = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
index += character.length;
|
|
346
|
+
}
|
|
347
|
+
if (!anyUpper) {
|
|
348
|
+
return "LOWER";
|
|
349
|
+
}
|
|
350
|
+
if (!anyLower) {
|
|
351
|
+
return "UPPER";
|
|
352
|
+
}
|
|
353
|
+
if (firstLetterIndex === 0 && isUpperCaseCodePoint(firstCodePoint)) {
|
|
354
|
+
return "CAPITALIZED";
|
|
355
|
+
}
|
|
356
|
+
return "MIXED";
|
|
357
|
+
}
|
|
358
|
+
function isPlainLatinToken(token) {
|
|
359
|
+
if (token.length === 0) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
let sawLetter = false;
|
|
363
|
+
for (const character of token) {
|
|
364
|
+
if (character.length === 1) {
|
|
365
|
+
const codePoint = character.charCodeAt(0);
|
|
366
|
+
if (codePoint < 128) {
|
|
367
|
+
if (!isAsciiLetterCodePoint(codePoint)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
sawLetter = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (LETTER.test(character) && LATIN_SCRIPT.test(character)) {
|
|
375
|
+
sawLetter = true;
|
|
376
|
+
} else if (!NON_SPACING_MARK.test(character)) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return sawLetter;
|
|
381
|
+
}
|
|
382
|
+
function isAllUpperLatinToken(token) {
|
|
383
|
+
let letters = 0;
|
|
384
|
+
for (const character of token) {
|
|
385
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
386
|
+
if (codePoint < 128) {
|
|
387
|
+
if (codePoint < 65 || codePoint > 90) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
} else if (!LETTER.test(character) || !LATIN_SCRIPT.test(character)) {
|
|
391
|
+
return false;
|
|
392
|
+
} else if (!isUpperCaseCodePoint(character)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
letters += 1;
|
|
396
|
+
}
|
|
397
|
+
return letters >= 2;
|
|
398
|
+
}
|
|
399
|
+
function codePointLength(token) {
|
|
400
|
+
let length = 0;
|
|
401
|
+
for (let index = 0; index < token.length; index += 1) {
|
|
402
|
+
if ((token.codePointAt(index) ?? 0) > 65535) {
|
|
403
|
+
index += 1;
|
|
404
|
+
}
|
|
405
|
+
length += 1;
|
|
406
|
+
}
|
|
407
|
+
return length;
|
|
408
|
+
}
|
|
409
|
+
function endsSibilant(lowerCase) {
|
|
410
|
+
const length = lowerCase.length;
|
|
411
|
+
if (length === 0) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const last = lowerCase.charAt(length - 1);
|
|
415
|
+
if (last === "s" || last === "x" || last === "z") {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (length >= 2) {
|
|
419
|
+
const previous = lowerCase.charAt(length - 2);
|
|
420
|
+
if ((previous === "c" || previous === "s") && last === "h") {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
function endsConsonantY(lowerCase) {
|
|
427
|
+
const length = lowerCase.length;
|
|
428
|
+
if (length < 2 || lowerCase.charAt(length - 1) !== "y") {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const before = lowerCase.charAt(length - 2);
|
|
432
|
+
if (isAsciiVowel(before)) {
|
|
433
|
+
return before === "u" && length >= 3 && lowerCase.charAt(length - 3) === "q";
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
function isAsciiVowel(character) {
|
|
438
|
+
return character === "a" || character === "e" || character === "i" || character === "o" || character === "u";
|
|
439
|
+
}
|
|
440
|
+
function capitalize(token) {
|
|
441
|
+
if (token.length === 0) {
|
|
442
|
+
return token;
|
|
443
|
+
}
|
|
444
|
+
const first = token.codePointAt(0);
|
|
445
|
+
if (first === void 0) {
|
|
446
|
+
return token;
|
|
447
|
+
}
|
|
448
|
+
const character = String.fromCodePoint(first);
|
|
449
|
+
const upper = character.toUpperCase();
|
|
450
|
+
if (upper === character) {
|
|
451
|
+
return token;
|
|
452
|
+
}
|
|
453
|
+
return upper + token.slice(character.length);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/lexicon.ts
|
|
457
|
+
var FULL_HEADER = "morphex-lexicon-v1";
|
|
458
|
+
var DELTA_HEADER = "morphex-delta-v1";
|
|
459
|
+
var RECORD_FIELDS = 8;
|
|
460
|
+
var COUNTABILITY_CODES = {
|
|
461
|
+
COUNTABLE: "C",
|
|
462
|
+
MASS: "M",
|
|
463
|
+
MIXED: "X",
|
|
464
|
+
PLURAL_ONLY: "P",
|
|
465
|
+
UNKNOWN: "U"
|
|
466
|
+
};
|
|
467
|
+
var COUNTABILITY_BY_CODE = new Map(
|
|
468
|
+
COUNTABILITIES.map((countability) => [COUNTABILITY_CODES[countability], countability])
|
|
469
|
+
);
|
|
470
|
+
var FLAG_INVARIANT = 1;
|
|
471
|
+
var FLAG_INITIALISM = 2;
|
|
472
|
+
var FLAG_CASE_SENSITIVE = 4;
|
|
473
|
+
var FLAG_AMBIGUOUS = 8;
|
|
474
|
+
var NO_ALTERNATIVES = Object.freeze([]);
|
|
475
|
+
function deriveEdit(lemma, plural) {
|
|
476
|
+
const max = Math.min(lemma.length, plural.length);
|
|
477
|
+
let prefix = 0;
|
|
478
|
+
while (prefix < max && lemma.charAt(prefix) === plural.charAt(prefix)) {
|
|
479
|
+
prefix += 1;
|
|
480
|
+
}
|
|
481
|
+
return [lemma.length - prefix, plural.slice(prefix)];
|
|
482
|
+
}
|
|
483
|
+
function reconstructPlural(lemma, cut, append) {
|
|
484
|
+
const end = Math.max(0, lemma.length - cut);
|
|
485
|
+
return lemma.slice(0, end) + append;
|
|
486
|
+
}
|
|
487
|
+
function binarySearch(keys, key) {
|
|
488
|
+
let low = 0;
|
|
489
|
+
let high = keys.length - 1;
|
|
490
|
+
while (low <= high) {
|
|
491
|
+
const middle = low + high >>> 1;
|
|
492
|
+
const candidate = keys[middle];
|
|
493
|
+
if (candidate === void 0) {
|
|
494
|
+
return -1;
|
|
495
|
+
}
|
|
496
|
+
if (candidate === key) {
|
|
497
|
+
return middle;
|
|
498
|
+
}
|
|
499
|
+
if (candidate < key) {
|
|
500
|
+
low = middle + 1;
|
|
501
|
+
} else {
|
|
502
|
+
high = middle - 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return -1;
|
|
506
|
+
}
|
|
507
|
+
var TableLexicon = class {
|
|
508
|
+
kind;
|
|
509
|
+
size;
|
|
510
|
+
diagnostics = true;
|
|
511
|
+
#keys;
|
|
512
|
+
#entries;
|
|
513
|
+
#membership;
|
|
514
|
+
constructor(kind, keys, entries, membership) {
|
|
515
|
+
this.kind = kind;
|
|
516
|
+
this.size = entries.length;
|
|
517
|
+
this.#keys = keys;
|
|
518
|
+
this.#entries = entries;
|
|
519
|
+
this.#membership = membership;
|
|
520
|
+
}
|
|
521
|
+
lookup(key) {
|
|
522
|
+
const index = binarySearch(this.#keys, key);
|
|
523
|
+
return index < 0 ? void 0 : this.#entries[index];
|
|
524
|
+
}
|
|
525
|
+
isOrdinaryWord(key) {
|
|
526
|
+
return this.#membership?.contains(key);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
var DELTA_KIND_MASS = -1;
|
|
530
|
+
var DELTA_KIND_PLURAL_ONLY = -2;
|
|
531
|
+
var DELTA_KIND_INVARIANT = -3;
|
|
532
|
+
function hashDeltaKey(key) {
|
|
533
|
+
let hash = 2166136261;
|
|
534
|
+
for (let index = 0; index < key.length; index += 1) {
|
|
535
|
+
hash = Math.imul(hash ^ key.charCodeAt(index), 16777619);
|
|
536
|
+
}
|
|
537
|
+
return hash >>> 0;
|
|
538
|
+
}
|
|
539
|
+
function buildDeltaIndex(edits, massKeys, pluralOnlyKeys, invariantKeys) {
|
|
540
|
+
const total = edits.length + massKeys.length + pluralOnlyKeys.length + invariantKeys.length;
|
|
541
|
+
const keys = new Array(total);
|
|
542
|
+
const kinds = new Int32Array(total);
|
|
543
|
+
let capacity = 1;
|
|
544
|
+
while (capacity < total * 2) {
|
|
545
|
+
capacity *= 2;
|
|
546
|
+
}
|
|
547
|
+
const slots = new Uint32Array(capacity);
|
|
548
|
+
const mask = capacity - 1;
|
|
549
|
+
let position = 0;
|
|
550
|
+
const insert = (key, kind) => {
|
|
551
|
+
let slot = hashDeltaKey(key) & mask;
|
|
552
|
+
for (; ; ) {
|
|
553
|
+
const stored = slots[slot];
|
|
554
|
+
if (stored === void 0 || stored === 0) {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
if (keys[stored - 1] === key) {
|
|
558
|
+
throw new Error(`duplicate identity key '${key}'`);
|
|
559
|
+
}
|
|
560
|
+
slot = slot + 1 & mask;
|
|
561
|
+
}
|
|
562
|
+
keys[position] = key;
|
|
563
|
+
kinds[position] = kind;
|
|
564
|
+
slots[slot] = position + 1;
|
|
565
|
+
position += 1;
|
|
566
|
+
};
|
|
567
|
+
for (let index = 0; index < edits.length; index += 1) {
|
|
568
|
+
const entry = edits[index];
|
|
569
|
+
if (entry === void 0) {
|
|
570
|
+
throw new Error("delta artifact is missing an edit record");
|
|
571
|
+
}
|
|
572
|
+
insert(entry.key, index);
|
|
573
|
+
}
|
|
574
|
+
for (const key of massKeys) {
|
|
575
|
+
insert(key, DELTA_KIND_MASS);
|
|
576
|
+
}
|
|
577
|
+
for (const key of pluralOnlyKeys) {
|
|
578
|
+
insert(key, DELTA_KIND_PLURAL_ONLY);
|
|
579
|
+
}
|
|
580
|
+
for (const key of invariantKeys) {
|
|
581
|
+
insert(key, DELTA_KIND_INVARIANT);
|
|
582
|
+
}
|
|
583
|
+
if (position !== total) {
|
|
584
|
+
throw new Error("delta artifact index is shorter than its sections");
|
|
585
|
+
}
|
|
586
|
+
return { keys, kinds, slots, mask };
|
|
587
|
+
}
|
|
588
|
+
var DeltaLexicon = class {
|
|
589
|
+
kind = "delta";
|
|
590
|
+
size;
|
|
591
|
+
diagnostics = false;
|
|
592
|
+
#keys;
|
|
593
|
+
#kinds;
|
|
594
|
+
#slots;
|
|
595
|
+
#mask;
|
|
596
|
+
#edits;
|
|
597
|
+
#membership;
|
|
598
|
+
constructor(edits, massKeys, pluralOnlyKeys, invariantKeys, membership) {
|
|
599
|
+
this.#edits = edits;
|
|
600
|
+
const index = buildDeltaIndex(edits, massKeys, pluralOnlyKeys, invariantKeys);
|
|
601
|
+
this.#keys = index.keys;
|
|
602
|
+
this.#kinds = index.kinds;
|
|
603
|
+
this.#slots = index.slots;
|
|
604
|
+
this.#mask = index.mask;
|
|
605
|
+
this.size = edits.length + massKeys.length + pluralOnlyKeys.length + invariantKeys.length;
|
|
606
|
+
this.#membership = membership;
|
|
607
|
+
}
|
|
608
|
+
lookup(key) {
|
|
609
|
+
let slot = hashDeltaKey(key) & this.#mask;
|
|
610
|
+
for (; ; ) {
|
|
611
|
+
const stored = this.#slots[slot];
|
|
612
|
+
if (stored === void 0 || stored === 0) {
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
const position = stored - 1;
|
|
616
|
+
if (this.#keys[position] === key) {
|
|
617
|
+
const kind = this.#kinds[position];
|
|
618
|
+
if (kind === void 0) {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
if (kind >= 0) {
|
|
622
|
+
return this.#edits[kind];
|
|
623
|
+
}
|
|
624
|
+
if (kind === DELTA_KIND_MASS) {
|
|
625
|
+
return identityEntry(key, "MASS", false);
|
|
626
|
+
}
|
|
627
|
+
if (kind === DELTA_KIND_PLURAL_ONLY) {
|
|
628
|
+
return identityEntry(key, "PLURAL_ONLY", false);
|
|
629
|
+
}
|
|
630
|
+
return identityEntry(key, "UNKNOWN", true);
|
|
631
|
+
}
|
|
632
|
+
slot = slot + 1 & this.#mask;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
isOrdinaryWord(key) {
|
|
636
|
+
return this.#membership?.contains(key);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
function identityEntry(key, countability, invariant) {
|
|
640
|
+
return Object.freeze({
|
|
641
|
+
lemma: key,
|
|
642
|
+
key,
|
|
643
|
+
plural: key,
|
|
644
|
+
classical: null,
|
|
645
|
+
alternatives: NO_ALTERNATIVES,
|
|
646
|
+
countability,
|
|
647
|
+
invariant,
|
|
648
|
+
initialism: false,
|
|
649
|
+
caseSensitive: false,
|
|
650
|
+
ambiguous: false
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
var RULES_ONLY_LEXICON = Object.freeze({
|
|
654
|
+
kind: "rules-only",
|
|
655
|
+
size: 0,
|
|
656
|
+
diagnostics: false,
|
|
657
|
+
lookup: () => void 0,
|
|
658
|
+
isOrdinaryWord: () => void 0
|
|
659
|
+
});
|
|
660
|
+
function parseRecord(record2, lineNumber) {
|
|
661
|
+
const fields = record2.split(" ");
|
|
662
|
+
if (fields.length !== RECORD_FIELDS) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
`lexicon line ${lineNumber}: expected ${RECORD_FIELDS} fields, found ${fields.length}`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const [lemmaField, cutField, appendField, classicalCutField, classicalAppendField] = fields;
|
|
668
|
+
const alternativesField = fields[5];
|
|
669
|
+
const countabilityField = fields[6];
|
|
670
|
+
const flagsField = fields[7];
|
|
671
|
+
if (lemmaField === void 0 || cutField === void 0 || appendField === void 0 || classicalCutField === void 0 || classicalAppendField === void 0 || alternativesField === void 0 || countabilityField === void 0 || flagsField === void 0) {
|
|
672
|
+
throw new Error(`lexicon line ${lineNumber}: malformed record`);
|
|
673
|
+
}
|
|
674
|
+
if (lemmaField.length === 0) {
|
|
675
|
+
throw new Error(`lexicon line ${lineNumber}: empty lemma`);
|
|
676
|
+
}
|
|
677
|
+
if (!/^\d+$/.test(cutField)) {
|
|
678
|
+
throw new Error(`lexicon line ${lineNumber}: bad cut '${cutField}'`);
|
|
679
|
+
}
|
|
680
|
+
const cut = Number.parseInt(cutField, 10);
|
|
681
|
+
if (!Number.isSafeInteger(cut) || cut > lemmaField.length) {
|
|
682
|
+
throw new Error(`lexicon line ${lineNumber}: cut ${cut} exceeds lemma length`);
|
|
683
|
+
}
|
|
684
|
+
if (!/^\d+$/.test(flagsField)) {
|
|
685
|
+
throw new Error(`lexicon line ${lineNumber}: bad flags '${flagsField}'`);
|
|
686
|
+
}
|
|
687
|
+
const flags = Number.parseInt(flagsField, 10);
|
|
688
|
+
if (flags < 0 || (flags & -16) !== 0) {
|
|
689
|
+
throw new Error(`lexicon line ${lineNumber}: unknown flags ${flags}`);
|
|
690
|
+
}
|
|
691
|
+
const countability = COUNTABILITY_BY_CODE.get(countabilityField);
|
|
692
|
+
if (countability === void 0) {
|
|
693
|
+
throw new Error(`lexicon line ${lineNumber}: bad countability '${countabilityField}'`);
|
|
694
|
+
}
|
|
695
|
+
let classical = null;
|
|
696
|
+
if (classicalCutField !== "-" || classicalAppendField !== "-") {
|
|
697
|
+
if (!/^\d+$/.test(classicalCutField)) {
|
|
698
|
+
throw new Error(`lexicon line ${lineNumber}: bad classical cut '${classicalCutField}'`);
|
|
699
|
+
}
|
|
700
|
+
const classicalCut = Number.parseInt(classicalCutField, 10);
|
|
701
|
+
if (classicalCut > lemmaField.length) {
|
|
702
|
+
throw new Error(`lexicon line ${lineNumber}: classical cut exceeds lemma length`);
|
|
703
|
+
}
|
|
704
|
+
classical = reconstructPlural(lemmaField, classicalCut, classicalAppendField);
|
|
705
|
+
if (classical.length === 0) {
|
|
706
|
+
throw new Error(`lexicon line ${lineNumber}: empty classical plural`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
let alternatives = NO_ALTERNATIVES;
|
|
710
|
+
if (alternativesField !== "-") {
|
|
711
|
+
const parsed = alternativesField.split(",");
|
|
712
|
+
if (parsed.some((alternative) => alternative.length === 0)) {
|
|
713
|
+
throw new Error(`lexicon line ${lineNumber}: empty alternative`);
|
|
714
|
+
}
|
|
715
|
+
alternatives = Object.freeze(parsed);
|
|
716
|
+
}
|
|
717
|
+
const plural = reconstructPlural(lemmaField, cut, appendField);
|
|
718
|
+
if (plural.length === 0) {
|
|
719
|
+
throw new Error(`lexicon line ${lineNumber}: empty plural`);
|
|
720
|
+
}
|
|
721
|
+
return Object.freeze({
|
|
722
|
+
lemma: lemmaField,
|
|
723
|
+
key: lower(lemmaField),
|
|
724
|
+
plural,
|
|
725
|
+
classical,
|
|
726
|
+
alternatives,
|
|
727
|
+
countability,
|
|
728
|
+
invariant: (flags & FLAG_INVARIANT) !== 0,
|
|
729
|
+
initialism: (flags & FLAG_INITIALISM) !== 0,
|
|
730
|
+
caseSensitive: (flags & FLAG_CASE_SENSITIVE) !== 0,
|
|
731
|
+
ambiguous: (flags & FLAG_AMBIGUOUS) !== 0
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function parseEditRecords(lines, start, count, startLine) {
|
|
735
|
+
const entries = new Array(count);
|
|
736
|
+
let previousKey = null;
|
|
737
|
+
for (let offset = 0; offset < count; offset += 1) {
|
|
738
|
+
const record2 = lines[start + offset];
|
|
739
|
+
if (record2 === void 0 || record2.length === 0) {
|
|
740
|
+
throw new Error(`lexicon line ${startLine + offset}: missing record`);
|
|
741
|
+
}
|
|
742
|
+
const entry = parseRecord(record2, startLine + offset);
|
|
743
|
+
if (previousKey !== null && entry.key <= previousKey) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`lexicon line ${startLine + offset}: keys are not strictly ascending ('${entry.key}')`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
previousKey = entry.key;
|
|
749
|
+
entries[offset] = entry;
|
|
750
|
+
}
|
|
751
|
+
return entries;
|
|
752
|
+
}
|
|
753
|
+
function parseKeyList(lines, start, count, startLine) {
|
|
754
|
+
const keys = new Array(count);
|
|
755
|
+
let previousKey = null;
|
|
756
|
+
for (let offset = 0; offset < count; offset += 1) {
|
|
757
|
+
const key = lines[start + offset];
|
|
758
|
+
if (key === void 0 || key.length === 0) {
|
|
759
|
+
throw new Error(`lexicon line ${startLine + offset}: empty identity key`);
|
|
760
|
+
}
|
|
761
|
+
if (key !== lower(key)) {
|
|
762
|
+
throw new Error(`lexicon line ${startLine + offset}: identity key is not lower case`);
|
|
763
|
+
}
|
|
764
|
+
if (previousKey !== null && key <= previousKey) {
|
|
765
|
+
throw new Error(`lexicon line ${startLine + offset}: identity keys are not ascending`);
|
|
766
|
+
}
|
|
767
|
+
previousKey = key;
|
|
768
|
+
keys[offset] = key;
|
|
769
|
+
}
|
|
770
|
+
return keys;
|
|
771
|
+
}
|
|
772
|
+
function parseFullLexicon(packed) {
|
|
773
|
+
const lines = packed.split("\n");
|
|
774
|
+
const headerLine = lines[0];
|
|
775
|
+
if (headerLine === void 0) {
|
|
776
|
+
throw new Error("lexicon artifact is empty");
|
|
777
|
+
}
|
|
778
|
+
const header = headerLine.split(" ");
|
|
779
|
+
if (header.length !== 3 || header[0] !== FULL_HEADER || header[1] !== "full") {
|
|
780
|
+
throw new Error("lexicon artifact has an unknown header");
|
|
781
|
+
}
|
|
782
|
+
const reportedCount = Number.parseInt(header[2] ?? "", 10);
|
|
783
|
+
if (!Number.isSafeInteger(reportedCount) || reportedCount < 0) {
|
|
784
|
+
throw new Error("lexicon artifact has a bad entry count");
|
|
785
|
+
}
|
|
786
|
+
const records = [];
|
|
787
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
788
|
+
const record2 = lines[index];
|
|
789
|
+
if (record2 === void 0 || record2.length === 0) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
records.push(record2);
|
|
793
|
+
}
|
|
794
|
+
const entries = parseEditRecords(records, 0, records.length, 2);
|
|
795
|
+
if (entries.length !== reportedCount) {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`lexicon artifact reports ${reportedCount} entries but contains ${entries.length}`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return entries;
|
|
801
|
+
}
|
|
802
|
+
function createFullLexicon(packed, membership) {
|
|
803
|
+
const entries = parseFullLexicon(packed);
|
|
804
|
+
return new TableLexicon(
|
|
805
|
+
"full",
|
|
806
|
+
entries.map((entry) => entry.key),
|
|
807
|
+
entries,
|
|
808
|
+
membership
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
function parseDeltaLexicon(packed) {
|
|
812
|
+
const lines = packed.split("\n");
|
|
813
|
+
const headerLine = lines[0];
|
|
814
|
+
if (headerLine === void 0) {
|
|
815
|
+
throw new Error("delta artifact is empty");
|
|
816
|
+
}
|
|
817
|
+
const header = headerLine.split(" ");
|
|
818
|
+
if (header.length !== 5 || header[0] !== DELTA_HEADER) {
|
|
819
|
+
throw new Error("delta artifact has an unknown header");
|
|
820
|
+
}
|
|
821
|
+
const counts = header.slice(1).map((field) => Number.parseInt(field, 10));
|
|
822
|
+
if (counts.length !== 4 || counts.some((count) => !Number.isSafeInteger(count) || count < 0)) {
|
|
823
|
+
throw new Error("delta artifact has a bad section count");
|
|
824
|
+
}
|
|
825
|
+
const [editCount = -1, massCount = -1, pluralOnlyCount = -1, invariantCount = -1] = counts;
|
|
826
|
+
const expected = editCount + massCount + pluralOnlyCount + invariantCount;
|
|
827
|
+
const records = lines.length - 1;
|
|
828
|
+
const trailingEmptyLine = records > 0 && lines[lines.length - 1] === "";
|
|
829
|
+
if (records - (trailingEmptyLine ? 1 : 0) !== expected) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`delta artifact reports ${expected} records but contains ${records - (trailingEmptyLine ? 1 : 0)}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
const edits = parseEditRecords(lines, 1, editCount, 2);
|
|
835
|
+
let offset = 1 + editCount;
|
|
836
|
+
const massKeys = parseKeyList(lines, offset, massCount, 1 + offset);
|
|
837
|
+
offset += massCount;
|
|
838
|
+
const pluralOnlyKeys = parseKeyList(lines, offset, pluralOnlyCount, 1 + offset);
|
|
839
|
+
offset += pluralOnlyCount;
|
|
840
|
+
const invariantKeys = parseKeyList(lines, offset, invariantCount, 1 + offset);
|
|
841
|
+
return { edits, massKeys, pluralOnlyKeys, invariantKeys };
|
|
842
|
+
}
|
|
843
|
+
function createDeltaLexicon(packed, membership) {
|
|
844
|
+
const { edits, massKeys, pluralOnlyKeys, invariantKeys } = parseDeltaLexicon(packed);
|
|
845
|
+
return new DeltaLexicon(edits, massKeys, pluralOnlyKeys, invariantKeys, membership);
|
|
846
|
+
}
|
|
847
|
+
function normalizeInput(input, index) {
|
|
848
|
+
if (input.lemma.length === 0) {
|
|
849
|
+
throw new Error(`custom lexicon entry ${index}: lemma must be a non-empty string`);
|
|
850
|
+
}
|
|
851
|
+
if (input.plural.length === 0) {
|
|
852
|
+
throw new Error(`custom lexicon entry ${index}: plural must be a non-empty string`);
|
|
853
|
+
}
|
|
854
|
+
const alternatives = input.alternatives ?? [];
|
|
855
|
+
if (alternatives.some((alternative) => alternative.length === 0)) {
|
|
856
|
+
throw new Error(`custom lexicon entry ${index}: alternatives must be non-empty strings`);
|
|
857
|
+
}
|
|
858
|
+
return Object.freeze({
|
|
859
|
+
lemma: input.lemma,
|
|
860
|
+
key: lower(input.lemma),
|
|
861
|
+
plural: input.plural,
|
|
862
|
+
classical: input.classical ?? null,
|
|
863
|
+
alternatives: Object.freeze([...alternatives]),
|
|
864
|
+
countability: input.countability ?? "COUNTABLE",
|
|
865
|
+
invariant: input.invariant ?? false,
|
|
866
|
+
initialism: input.initialism ?? false,
|
|
867
|
+
caseSensitive: input.caseSensitive ?? false,
|
|
868
|
+
ambiguous: input.ambiguous ?? false
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function createLexicon(entries, membership) {
|
|
872
|
+
const normalized = [];
|
|
873
|
+
let index = 0;
|
|
874
|
+
for (const input of entries) {
|
|
875
|
+
normalized.push(normalizeInput(input, index));
|
|
876
|
+
index += 1;
|
|
877
|
+
}
|
|
878
|
+
normalized.sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
879
|
+
for (let position = 1; position < normalized.length; position += 1) {
|
|
880
|
+
const previous = normalized[position - 1];
|
|
881
|
+
const current = normalized[position];
|
|
882
|
+
if (previous !== void 0 && current !== void 0 && previous.key === current.key) {
|
|
883
|
+
throw new Error(`custom lexicon: duplicate lemma key '${current.key}'`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return new TableLexicon(
|
|
887
|
+
"custom",
|
|
888
|
+
normalized.map((entry) => entry.key),
|
|
889
|
+
normalized,
|
|
890
|
+
membership
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
function fnv1aLow32(value) {
|
|
894
|
+
let hash = 2216829733;
|
|
895
|
+
const bytes = new TextEncoder().encode(value);
|
|
896
|
+
for (const byte of bytes) {
|
|
897
|
+
hash = Math.imul(hash ^ byte, 435) >>> 0;
|
|
898
|
+
}
|
|
899
|
+
return hash >>> 0;
|
|
900
|
+
}
|
|
901
|
+
function decodeBase64(value) {
|
|
902
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
903
|
+
const clean = value.endsWith("==") ? value.slice(0, -2) : value.endsWith("=") ? value.slice(0, -1) : value;
|
|
904
|
+
const output = new Uint8Array(Math.floor(clean.length * 3 / 4));
|
|
905
|
+
let bits = 0;
|
|
906
|
+
let bitCount = 0;
|
|
907
|
+
let written = 0;
|
|
908
|
+
for (const character of clean) {
|
|
909
|
+
const digit = alphabet.indexOf(character);
|
|
910
|
+
if (digit < 0) {
|
|
911
|
+
throw new Error("invalid base64 lexicon data");
|
|
912
|
+
}
|
|
913
|
+
bits = bits << 6 | digit;
|
|
914
|
+
bitCount += 6;
|
|
915
|
+
if (bitCount >= 8) {
|
|
916
|
+
bitCount -= 8;
|
|
917
|
+
output[written] = bits >> bitCount & 255;
|
|
918
|
+
written += 1;
|
|
919
|
+
bits &= (1 << bitCount) - 1;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (written !== output.length) {
|
|
923
|
+
throw new Error("truncated base64 lexicon data");
|
|
924
|
+
}
|
|
925
|
+
return output;
|
|
926
|
+
}
|
|
927
|
+
function createMembershipTable(base64) {
|
|
928
|
+
const bytes = decodeBase64(base64);
|
|
929
|
+
if (bytes.length % 4 !== 0) {
|
|
930
|
+
throw new Error("membership table length is not a multiple of four");
|
|
931
|
+
}
|
|
932
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
933
|
+
const hashes = new Uint32Array(bytes.length / 4);
|
|
934
|
+
for (let index = 0; index < hashes.length; index += 1) {
|
|
935
|
+
hashes[index] = view.getUint32(index * 4, true);
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
size: hashes.length,
|
|
939
|
+
contains(value) {
|
|
940
|
+
const target = fnv1aLow32(value);
|
|
941
|
+
let low = 0;
|
|
942
|
+
let high = hashes.length - 1;
|
|
943
|
+
while (low <= high) {
|
|
944
|
+
const middle = low + high >>> 1;
|
|
945
|
+
const candidate = hashes[middle];
|
|
946
|
+
if (candidate === void 0) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
if (candidate === target) {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
if (candidate < target) {
|
|
953
|
+
low = middle + 1;
|
|
954
|
+
} else {
|
|
955
|
+
high = middle - 1;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/defaults.ts
|
|
964
|
+
var bundled;
|
|
965
|
+
function defaultLexiconProvider() {
|
|
966
|
+
bundled ??= createDeltaLexicon(BUNDLED_DELTA_LEXICON);
|
|
967
|
+
return bundled;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/render.ts
|
|
971
|
+
function edit(token, cut, append) {
|
|
972
|
+
const end = Math.max(0, token.length - cut);
|
|
973
|
+
if (append.length === 0) {
|
|
974
|
+
return end === token.length ? token : token.slice(0, end);
|
|
975
|
+
}
|
|
976
|
+
if (end === 0) {
|
|
977
|
+
return append;
|
|
978
|
+
}
|
|
979
|
+
return token.slice(0, end) + append;
|
|
980
|
+
}
|
|
981
|
+
function render(source, lowerCase, cut, append, policy) {
|
|
982
|
+
if (policy === "LOWERCASE") {
|
|
983
|
+
return edit(lowerCase, cut, append);
|
|
984
|
+
}
|
|
985
|
+
if (policy === "UPPERCASE") {
|
|
986
|
+
return edit(source, cut, append).toUpperCase();
|
|
987
|
+
}
|
|
988
|
+
const casing = casingOf(source);
|
|
989
|
+
if (casing === "LOWER") {
|
|
990
|
+
return edit(lowerCase, cut, append);
|
|
991
|
+
}
|
|
992
|
+
if (source.length !== lowerCase.length) {
|
|
993
|
+
const base = edit(lowerCase, cut, append);
|
|
994
|
+
if (casing === "UPPER") {
|
|
995
|
+
return base.toUpperCase();
|
|
996
|
+
}
|
|
997
|
+
if (casing === "CAPITALIZED") {
|
|
998
|
+
return capitalize(base);
|
|
999
|
+
}
|
|
1000
|
+
return base;
|
|
1001
|
+
}
|
|
1002
|
+
const edited = edit(source, cut, append);
|
|
1003
|
+
return casing === "UPPER" ? edited.toUpperCase() : edited;
|
|
1004
|
+
}
|
|
1005
|
+
function renderFull(source, lowerCase, fullLower, policy) {
|
|
1006
|
+
if (policy === "LOWERCASE") {
|
|
1007
|
+
return fullLower;
|
|
1008
|
+
}
|
|
1009
|
+
if (policy === "UPPERCASE") {
|
|
1010
|
+
return fullLower.toUpperCase();
|
|
1011
|
+
}
|
|
1012
|
+
switch (casingOf(source)) {
|
|
1013
|
+
case "LOWER":
|
|
1014
|
+
return fullLower;
|
|
1015
|
+
case "UPPER":
|
|
1016
|
+
return fullLower.toUpperCase();
|
|
1017
|
+
case "CAPITALIZED":
|
|
1018
|
+
return capitalize(fullLower);
|
|
1019
|
+
case "MIXED":
|
|
1020
|
+
return fullLower.startsWith(lowerCase) ? source + fullLower.slice(lowerCase.length) : fullLower;
|
|
1021
|
+
default:
|
|
1022
|
+
return fullLower;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function renderInitialism(source, lowerCase, suffix, policy) {
|
|
1026
|
+
if (policy === "LOWERCASE") {
|
|
1027
|
+
return lowerCase + suffix;
|
|
1028
|
+
}
|
|
1029
|
+
if (policy === "UPPERCASE") {
|
|
1030
|
+
return source.toUpperCase() + suffix.toUpperCase();
|
|
1031
|
+
}
|
|
1032
|
+
return source + suffix;
|
|
1033
|
+
}
|
|
1034
|
+
function applyCasePolicy(value, policy) {
|
|
1035
|
+
if (policy === "LOWERCASE") {
|
|
1036
|
+
return value.toLowerCase();
|
|
1037
|
+
}
|
|
1038
|
+
if (policy === "UPPERCASE") {
|
|
1039
|
+
return value.toUpperCase();
|
|
1040
|
+
}
|
|
1041
|
+
return value;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/rules.ts
|
|
1045
|
+
function applyProductiveRule(noun, lowerCase) {
|
|
1046
|
+
if (lowerCase.endsWith("woman")) {
|
|
1047
|
+
return {
|
|
1048
|
+
id: "backoff:woman->women",
|
|
1049
|
+
cut: 5,
|
|
1050
|
+
append: "women",
|
|
1051
|
+
description: "documented -woman -> -women backoff"
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
if (lowerCase.endsWith("wife")) {
|
|
1055
|
+
return {
|
|
1056
|
+
id: "backoff:wife->wives",
|
|
1057
|
+
cut: 4,
|
|
1058
|
+
append: "wives",
|
|
1059
|
+
description: "documented -wife -> -wives backoff"
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
if (lowerCase.length >= 6 && lowerCase.endsWith("man") && !lowerCase.endsWith("woman") && noun === lowerCase && !isManException(lowerCase)) {
|
|
1063
|
+
return {
|
|
1064
|
+
id: "backoff:man->men",
|
|
1065
|
+
cut: 3,
|
|
1066
|
+
append: "men",
|
|
1067
|
+
description: "documented noun + man -> men backoff"
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
if (lowerCase.length >= 5 && lowerCase.endsWith("sis")) {
|
|
1071
|
+
return {
|
|
1072
|
+
id: "backoff:sis->ses",
|
|
1073
|
+
cut: 2,
|
|
1074
|
+
append: "es",
|
|
1075
|
+
description: "documented Greek -sis -> -ses backoff"
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
if (endsSibilant(lowerCase)) {
|
|
1079
|
+
return {
|
|
1080
|
+
id: "rule:sibilant-es",
|
|
1081
|
+
cut: 0,
|
|
1082
|
+
append: "es",
|
|
1083
|
+
description: "suffix s/x/z/ch/sh appends es"
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (endsConsonantY(lowerCase)) {
|
|
1087
|
+
return {
|
|
1088
|
+
id: "rule:consonant-y-ies",
|
|
1089
|
+
cut: 1,
|
|
1090
|
+
append: "ies",
|
|
1091
|
+
description: "consonant before y becomes ies"
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
id: "rule:default-s",
|
|
1096
|
+
cut: 0,
|
|
1097
|
+
append: "s",
|
|
1098
|
+
description: "default appends s"
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// src/english-inflector.ts
|
|
1103
|
+
var MAX_COMPOUND_DEPTH = 4;
|
|
1104
|
+
var FALLBACK_ID = "fallback:unsupported";
|
|
1105
|
+
var F_TO_VES = [
|
|
1106
|
+
["life", "lives"],
|
|
1107
|
+
["shelf", "shelves"],
|
|
1108
|
+
["half", "halves"],
|
|
1109
|
+
["staff", "staves"],
|
|
1110
|
+
["wolf", "wolves"],
|
|
1111
|
+
["leaf", "leaves"],
|
|
1112
|
+
["loaf", "loaves"],
|
|
1113
|
+
["calf", "calves"],
|
|
1114
|
+
["elf", "elves"],
|
|
1115
|
+
["self", "selves"],
|
|
1116
|
+
["thief", "thieves"],
|
|
1117
|
+
["sheaf", "sheaves"],
|
|
1118
|
+
["hoof", "hooves"]
|
|
1119
|
+
];
|
|
1120
|
+
function createSink() {
|
|
1121
|
+
return {
|
|
1122
|
+
stage: "FALLBACK",
|
|
1123
|
+
id: FALLBACK_ID,
|
|
1124
|
+
countability: "UNKNOWN",
|
|
1125
|
+
invariant: false,
|
|
1126
|
+
ambiguous: false,
|
|
1127
|
+
alternatives: [],
|
|
1128
|
+
note: ""
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function record(sink, stage, id, countability, invariant, ambiguous, alternatives, note) {
|
|
1132
|
+
sink.stage = stage;
|
|
1133
|
+
sink.id = id;
|
|
1134
|
+
sink.countability = countability;
|
|
1135
|
+
sink.invariant = invariant;
|
|
1136
|
+
sink.ambiguous = ambiguous;
|
|
1137
|
+
sink.alternatives = alternatives;
|
|
1138
|
+
sink.note = note;
|
|
1139
|
+
}
|
|
1140
|
+
var DiagnosticInflection = class {
|
|
1141
|
+
input;
|
|
1142
|
+
plural;
|
|
1143
|
+
alternatives;
|
|
1144
|
+
ambiguous;
|
|
1145
|
+
stage;
|
|
1146
|
+
source;
|
|
1147
|
+
countability;
|
|
1148
|
+
invariant;
|
|
1149
|
+
note;
|
|
1150
|
+
diagnostics;
|
|
1151
|
+
constructor(fields) {
|
|
1152
|
+
this.input = fields.input;
|
|
1153
|
+
this.plural = fields.plural;
|
|
1154
|
+
this.alternatives = Object.freeze([...fields.alternatives]);
|
|
1155
|
+
this.ambiguous = fields.ambiguous;
|
|
1156
|
+
this.stage = fields.stage;
|
|
1157
|
+
this.source = fields.source;
|
|
1158
|
+
this.countability = fields.countability;
|
|
1159
|
+
this.invariant = fields.invariant;
|
|
1160
|
+
this.note = fields.note;
|
|
1161
|
+
this.diagnostics = fields.diagnostics;
|
|
1162
|
+
}
|
|
1163
|
+
isAccepted(form) {
|
|
1164
|
+
return this.plural === form || this.alternatives.includes(form);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
function entryNote(entry) {
|
|
1168
|
+
switch (entry.countability) {
|
|
1169
|
+
case "MASS":
|
|
1170
|
+
return "mass noun: returned unchanged by policy";
|
|
1171
|
+
case "PLURAL_ONLY":
|
|
1172
|
+
return "plural-only lemma: returned unchanged by policy";
|
|
1173
|
+
case "MIXED":
|
|
1174
|
+
return "mixed mass/countable noun: pluralized in its countable sense";
|
|
1175
|
+
default:
|
|
1176
|
+
return entry.invariant ? "invariant plural: singular and plural are identical" : "";
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function initialismSuffix(lowerCase) {
|
|
1180
|
+
return lowerCase.charAt(lowerCase.length - 1) === "s" ? "es" : "s";
|
|
1181
|
+
}
|
|
1182
|
+
var standardInstance;
|
|
1183
|
+
var EnglishInflector = class _EnglishInflector {
|
|
1184
|
+
#mode;
|
|
1185
|
+
#casePolicy;
|
|
1186
|
+
#overrides;
|
|
1187
|
+
#initialisms;
|
|
1188
|
+
#uppercasePolicy;
|
|
1189
|
+
#lexicon;
|
|
1190
|
+
/** @internal Instances are created through `standard()` or `builder()`. */
|
|
1191
|
+
constructor(config) {
|
|
1192
|
+
this.#mode = config.mode;
|
|
1193
|
+
this.#casePolicy = config.casePolicy;
|
|
1194
|
+
this.#overrides = config.overrides;
|
|
1195
|
+
this.#initialisms = config.initialisms;
|
|
1196
|
+
this.#uppercasePolicy = config.uppercasePolicy;
|
|
1197
|
+
this.#lexicon = config.lexicon;
|
|
1198
|
+
}
|
|
1199
|
+
/** Shared default instance: anglicized mode, `PRESERVE` casing, bundled delta lexicon. */
|
|
1200
|
+
static standard() {
|
|
1201
|
+
standardInstance ??= _EnglishInflector.builder().build();
|
|
1202
|
+
return standardInstance;
|
|
1203
|
+
}
|
|
1204
|
+
static builder() {
|
|
1205
|
+
return new Builder();
|
|
1206
|
+
}
|
|
1207
|
+
get mode() {
|
|
1208
|
+
return this.#mode;
|
|
1209
|
+
}
|
|
1210
|
+
get casePolicy() {
|
|
1211
|
+
return this.#casePolicy;
|
|
1212
|
+
}
|
|
1213
|
+
get uppercasePolicy() {
|
|
1214
|
+
return this.#uppercasePolicy;
|
|
1215
|
+
}
|
|
1216
|
+
/** Number of lexical entries visible to this instance (0 for a rules-only build). */
|
|
1217
|
+
get lexiconSize() {
|
|
1218
|
+
return this.#lexicon.size;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Returns the plural of a singular noun. With a `count`, returns `noun` unchanged when
|
|
1222
|
+
* `count === 1` and the plural otherwise. This is deliberately a plain count test, not an
|
|
1223
|
+
* absolute-value or locale rule.
|
|
1224
|
+
*/
|
|
1225
|
+
pluralize(noun, count) {
|
|
1226
|
+
if (count === 1) {
|
|
1227
|
+
return noun;
|
|
1228
|
+
}
|
|
1229
|
+
return this.#resolve(noun, void 0, 0, this.#casePolicy);
|
|
1230
|
+
}
|
|
1231
|
+
/** Returns a full diagnostic result. The chosen form is identical to `pluralize(noun)`. */
|
|
1232
|
+
analyze(noun) {
|
|
1233
|
+
const sink = createSink();
|
|
1234
|
+
const plural = this.#resolve(noun, sink, 0, this.#casePolicy);
|
|
1235
|
+
return new DiagnosticInflection({
|
|
1236
|
+
input: noun,
|
|
1237
|
+
plural,
|
|
1238
|
+
alternatives: sink.alternatives,
|
|
1239
|
+
ambiguous: sink.ambiguous,
|
|
1240
|
+
stage: sink.stage,
|
|
1241
|
+
source: sink.id,
|
|
1242
|
+
countability: sink.countability,
|
|
1243
|
+
invariant: sink.invariant,
|
|
1244
|
+
note: sink.note,
|
|
1245
|
+
diagnostics: this.#lexicon.diagnostics
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
// ------------------------------------------------------------------ resolution
|
|
1249
|
+
#resolve(noun, sink, depth, policy) {
|
|
1250
|
+
if (noun.length === 0) {
|
|
1251
|
+
if (sink !== void 0) {
|
|
1252
|
+
record(
|
|
1253
|
+
sink,
|
|
1254
|
+
"FALLBACK",
|
|
1255
|
+
"fallback:empty",
|
|
1256
|
+
"UNKNOWN",
|
|
1257
|
+
false,
|
|
1258
|
+
false,
|
|
1259
|
+
[],
|
|
1260
|
+
"empty input returns empty output"
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
return noun;
|
|
1264
|
+
}
|
|
1265
|
+
const override = this.#overrides.get(noun);
|
|
1266
|
+
if (override !== void 0) {
|
|
1267
|
+
if (sink !== void 0) {
|
|
1268
|
+
record(
|
|
1269
|
+
sink,
|
|
1270
|
+
"OVERRIDE",
|
|
1271
|
+
"override",
|
|
1272
|
+
"UNKNOWN",
|
|
1273
|
+
false,
|
|
1274
|
+
false,
|
|
1275
|
+
[],
|
|
1276
|
+
"caller override matched the exact input spelling"
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return override;
|
|
1280
|
+
}
|
|
1281
|
+
const lowerCase = lower(noun);
|
|
1282
|
+
const entry = this.#lexicon.lookup(lowerCase);
|
|
1283
|
+
if (entry !== void 0 && (!entry.caseSensitive || entry.lemma === noun)) {
|
|
1284
|
+
return this.#fromEntry(noun, lowerCase, entry, sink, policy);
|
|
1285
|
+
}
|
|
1286
|
+
if (this.#initialisms.has(lowerCase)) {
|
|
1287
|
+
if (sink !== void 0) {
|
|
1288
|
+
record(
|
|
1289
|
+
sink,
|
|
1290
|
+
"LEXICON",
|
|
1291
|
+
"config:initialism",
|
|
1292
|
+
"UNKNOWN",
|
|
1293
|
+
false,
|
|
1294
|
+
false,
|
|
1295
|
+
[],
|
|
1296
|
+
"token declared an initialism by configuration"
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
return renderInitialism(noun, lowerCase, initialismSuffix(lowerCase), policy);
|
|
1300
|
+
}
|
|
1301
|
+
const ordinaryWord = this.#lexicon.isOrdinaryWord(lowerCase);
|
|
1302
|
+
if (this.#uppercasePolicy === "INFER_INITIALISM" && this.#lexicon.size > 0 && ordinaryWord !== void 0 && !ordinaryWord && isAllUpperLatinToken(noun) && codePointLength(noun) >= 2 && codePointLength(noun) <= 6) {
|
|
1303
|
+
if (sink !== void 0) {
|
|
1304
|
+
record(
|
|
1305
|
+
sink,
|
|
1306
|
+
"INFERRED",
|
|
1307
|
+
"inferred:uppercase-initialism",
|
|
1308
|
+
"UNKNOWN",
|
|
1309
|
+
false,
|
|
1310
|
+
false,
|
|
1311
|
+
[],
|
|
1312
|
+
"unknown all-upper-case token of 2-6 Latin letters: appended lower-case s; lexical membership was checked first"
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
return renderInitialism(noun, lowerCase, "s", policy);
|
|
1316
|
+
}
|
|
1317
|
+
if (depth < MAX_COMPOUND_DEPTH && (noun.includes("-") || noun.includes(" "))) {
|
|
1318
|
+
const compound = this.#resolveCompound(noun, lowerCase, depth, policy, sink);
|
|
1319
|
+
if (compound !== null) {
|
|
1320
|
+
return compound === noun ? noun : compound;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (isPlainLatinToken(noun)) {
|
|
1324
|
+
const rule = applyProductiveRule(noun, lowerCase);
|
|
1325
|
+
if (sink !== void 0) {
|
|
1326
|
+
record(sink, "RULE", rule.id, "UNKNOWN", false, false, [], rule.description);
|
|
1327
|
+
}
|
|
1328
|
+
return render(noun, lowerCase, rule.cut, rule.append, policy);
|
|
1329
|
+
}
|
|
1330
|
+
if (sink !== void 0) {
|
|
1331
|
+
record(
|
|
1332
|
+
sink,
|
|
1333
|
+
"FALLBACK",
|
|
1334
|
+
FALLBACK_ID,
|
|
1335
|
+
"UNKNOWN",
|
|
1336
|
+
false,
|
|
1337
|
+
false,
|
|
1338
|
+
[],
|
|
1339
|
+
"unsupported token shape (identifier, punctuation, possessive or non-Latin letter); returned unchanged"
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
return noun;
|
|
1343
|
+
}
|
|
1344
|
+
#fromEntry(noun, lowerCase, entry, sink, policy) {
|
|
1345
|
+
const [cut, append] = deriveEdit(entry.lemma, entry.plural);
|
|
1346
|
+
if (entry.initialism) {
|
|
1347
|
+
if (sink !== void 0) {
|
|
1348
|
+
record(
|
|
1349
|
+
sink,
|
|
1350
|
+
"LEXICON",
|
|
1351
|
+
`lex:${entry.caseSensitive ? entry.lemma : entry.key}#initialism`,
|
|
1352
|
+
entry.countability,
|
|
1353
|
+
false,
|
|
1354
|
+
entry.ambiguous,
|
|
1355
|
+
[],
|
|
1356
|
+
"initialism metadata: append the entry's s/es suffix without whole-token re-casing"
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return renderInitialism(noun, lowerCase, append, policy);
|
|
1360
|
+
}
|
|
1361
|
+
if (entry.countability === "MASS" || entry.countability === "PLURAL_ONLY" || entry.invariant) {
|
|
1362
|
+
if (sink !== void 0) {
|
|
1363
|
+
record(
|
|
1364
|
+
sink,
|
|
1365
|
+
"LEXICON",
|
|
1366
|
+
`lex:${entry.caseSensitive ? entry.lemma : entry.key}`,
|
|
1367
|
+
entry.countability,
|
|
1368
|
+
entry.invariant,
|
|
1369
|
+
entry.ambiguous,
|
|
1370
|
+
this.#renderAlternatives(noun, lowerCase, entry, false, policy),
|
|
1371
|
+
entryNote(entry)
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
return noun;
|
|
1375
|
+
}
|
|
1376
|
+
const classicalForm = this.#mode === "CLASSICAL" ? entry.classical : null;
|
|
1377
|
+
const classicalUsed = classicalForm !== null;
|
|
1378
|
+
let pluralCut = cut;
|
|
1379
|
+
let pluralAppend = append;
|
|
1380
|
+
if (classicalForm !== null) {
|
|
1381
|
+
[pluralCut, pluralAppend] = deriveEdit(entry.lemma, classicalForm);
|
|
1382
|
+
}
|
|
1383
|
+
const plural = render(noun, lowerCase, pluralCut, pluralAppend, policy);
|
|
1384
|
+
if (sink !== void 0) {
|
|
1385
|
+
record(
|
|
1386
|
+
sink,
|
|
1387
|
+
"LEXICON",
|
|
1388
|
+
`lex:${entry.caseSensitive ? entry.lemma : entry.key}`,
|
|
1389
|
+
entry.countability,
|
|
1390
|
+
false,
|
|
1391
|
+
entry.ambiguous,
|
|
1392
|
+
this.#renderAlternatives(noun, lowerCase, entry, classicalUsed, policy),
|
|
1393
|
+
classicalUsed ? "classical preference selected by mode" : entryNote(entry)
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
return plural;
|
|
1397
|
+
}
|
|
1398
|
+
#renderAlternatives(noun, lowerCase, entry, classicalUsed, policy) {
|
|
1399
|
+
const stored = entry.alternatives;
|
|
1400
|
+
const classical = entry.classical;
|
|
1401
|
+
if (stored.length === 0 && classical === null) {
|
|
1402
|
+
return [];
|
|
1403
|
+
}
|
|
1404
|
+
const alternatives = [];
|
|
1405
|
+
if (classicalUsed) {
|
|
1406
|
+
alternatives.push(renderFull(noun, lowerCase, entry.plural, policy));
|
|
1407
|
+
} else if (classical !== null) {
|
|
1408
|
+
alternatives.push(renderFull(noun, lowerCase, classical, policy));
|
|
1409
|
+
}
|
|
1410
|
+
for (const alternative of stored) {
|
|
1411
|
+
alternatives.push(renderFull(noun, lowerCase, alternative, policy));
|
|
1412
|
+
}
|
|
1413
|
+
return alternatives;
|
|
1414
|
+
}
|
|
1415
|
+
// ------------------------------------------------------------------ compounds
|
|
1416
|
+
#resolveCompound(noun, lowerCase, depth, policy, sink) {
|
|
1417
|
+
const exact = exactPlural(lowerCase);
|
|
1418
|
+
if (exact !== void 0) {
|
|
1419
|
+
if (sink !== void 0) {
|
|
1420
|
+
record(
|
|
1421
|
+
sink,
|
|
1422
|
+
"COMPOUND",
|
|
1423
|
+
`compound:exact:${lowerCase}`,
|
|
1424
|
+
"UNKNOWN",
|
|
1425
|
+
false,
|
|
1426
|
+
false,
|
|
1427
|
+
[],
|
|
1428
|
+
"exact compound entry"
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
return renderFull(noun, lowerCase, exact, policy);
|
|
1432
|
+
}
|
|
1433
|
+
const freeSuffix = freeTailSuffix(lowerCase);
|
|
1434
|
+
if (freeSuffix > 0 && freeSuffix < noun.length) {
|
|
1435
|
+
const headPlural = this.#resolveHead(noun.slice(0, freeSuffix), depth, true);
|
|
1436
|
+
if (headPlural !== null) {
|
|
1437
|
+
if (sink !== void 0) {
|
|
1438
|
+
record(
|
|
1439
|
+
sink,
|
|
1440
|
+
"COMPOUND",
|
|
1441
|
+
"compound:hyphen:free-tail-suffix",
|
|
1442
|
+
"UNKNOWN",
|
|
1443
|
+
false,
|
|
1444
|
+
false,
|
|
1445
|
+
[],
|
|
1446
|
+
"documented trailing free tail: head before the suffix is inflected"
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
return applyCasePolicy(headPlural.text + noun.slice(freeSuffix), policy);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const hyphen = noun.indexOf("-");
|
|
1453
|
+
if (hyphen > 0 && hyphen < noun.length - 1) {
|
|
1454
|
+
const head = noun.slice(0, hyphen);
|
|
1455
|
+
const tail = lowerCase.slice(hyphen + 1);
|
|
1456
|
+
const recognised = isFreeTail(tail) || isPostposed(tail) && isCuratedHead(lower(head));
|
|
1457
|
+
if (recognised) {
|
|
1458
|
+
const headPlural = this.#resolveHead(head, depth);
|
|
1459
|
+
if (headPlural !== null) {
|
|
1460
|
+
if (sink !== void 0) {
|
|
1461
|
+
record(
|
|
1462
|
+
sink,
|
|
1463
|
+
"COMPOUND",
|
|
1464
|
+
`compound:hyphen:${tail}`,
|
|
1465
|
+
"UNKNOWN",
|
|
1466
|
+
false,
|
|
1467
|
+
false,
|
|
1468
|
+
[],
|
|
1469
|
+
`documented hyphenated compound pattern '${tail}'`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
return applyCasePolicy(headPlural.text + noun.slice(hyphen), policy);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (this.#isParticleCompound(noun, hyphen, tail)) {
|
|
1476
|
+
if (sink !== void 0) {
|
|
1477
|
+
record(
|
|
1478
|
+
sink,
|
|
1479
|
+
"COMPOUND",
|
|
1480
|
+
"compound:hyphen:particle",
|
|
1481
|
+
"UNKNOWN",
|
|
1482
|
+
false,
|
|
1483
|
+
false,
|
|
1484
|
+
[],
|
|
1485
|
+
"verb-particle hyphenated noun: whole expression takes s"
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
return render(noun, lowerCase, 0, "s", policy);
|
|
1489
|
+
}
|
|
1490
|
+
if (this.#isHeadFinalCompound(noun, hyphen, tail)) {
|
|
1491
|
+
const tailPlural = this.#pluralizeCompoundHead(noun.slice(hyphen + 1), depth, true);
|
|
1492
|
+
if (tailPlural !== null) {
|
|
1493
|
+
if (sink !== void 0) {
|
|
1494
|
+
record(
|
|
1495
|
+
sink,
|
|
1496
|
+
"COMPOUND",
|
|
1497
|
+
"compound:hyphen:head-final",
|
|
1498
|
+
"UNKNOWN",
|
|
1499
|
+
false,
|
|
1500
|
+
false,
|
|
1501
|
+
[],
|
|
1502
|
+
"head-final hyphenated compound: final noun head inflected"
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
return applyCasePolicy(noun.slice(0, hyphen + 1) + tailPlural, policy);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const lastHyphen = noun.lastIndexOf("-");
|
|
1509
|
+
if (lastHyphen > hyphen && this.#isMultiHeadFinalCompound(noun)) {
|
|
1510
|
+
const multiTail = noun.slice(lastHyphen + 1);
|
|
1511
|
+
const tailPlural = this.#pluralizeCompoundHead(multiTail, depth, true);
|
|
1512
|
+
if (tailPlural !== null) {
|
|
1513
|
+
if (sink !== void 0) {
|
|
1514
|
+
record(
|
|
1515
|
+
sink,
|
|
1516
|
+
"COMPOUND",
|
|
1517
|
+
"compound:hyphen:multi-head-final",
|
|
1518
|
+
"UNKNOWN",
|
|
1519
|
+
false,
|
|
1520
|
+
false,
|
|
1521
|
+
[],
|
|
1522
|
+
"multi-hyphen head-final noun without a function word: final head inflected"
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
return applyCasePolicy(noun.slice(0, lastHyphen + 1) + tailPlural, policy);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
const space = noun.indexOf(" ");
|
|
1531
|
+
if (space < 0 || space === 0 || space === noun.length - 1 || noun.includes(" ")) {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
const tokens = noun.split(" ");
|
|
1535
|
+
if (tokens.length < 2 || tokens.some((token) => token.length === 0)) {
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
const lastToken = tokens[tokens.length - 1];
|
|
1539
|
+
if (lastToken === void 0) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
const lastLower = lower(lastToken);
|
|
1543
|
+
if (isPostposed(lastLower)) {
|
|
1544
|
+
const headLower = tokens.slice(0, -1).map((token) => lower(token)).join(" ");
|
|
1545
|
+
if (isCuratedHead(headLower)) {
|
|
1546
|
+
const headPlural = this.#resolveHead(noun.slice(0, space), depth);
|
|
1547
|
+
if (headPlural !== null) {
|
|
1548
|
+
if (sink !== void 0) {
|
|
1549
|
+
record(
|
|
1550
|
+
sink,
|
|
1551
|
+
"COMPOUND",
|
|
1552
|
+
`compound:space:${lastLower}`,
|
|
1553
|
+
"UNKNOWN",
|
|
1554
|
+
false,
|
|
1555
|
+
false,
|
|
1556
|
+
[],
|
|
1557
|
+
`postposed modifier '${lastLower}' after a curated head`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return applyCasePolicy(headPlural.text + noun.slice(space), policy);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (tokens.some((token) => isPreposition(lower(token)))) {
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
if (!isPlainLatinToken(lastToken)) {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
const lastPlural = this.#pluralizeCompoundHead(lastToken, depth, false);
|
|
1571
|
+
if (lastPlural === null) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
if (sink !== void 0) {
|
|
1575
|
+
record(
|
|
1576
|
+
sink,
|
|
1577
|
+
"COMPOUND",
|
|
1578
|
+
"compound:space:final",
|
|
1579
|
+
"UNKNOWN",
|
|
1580
|
+
false,
|
|
1581
|
+
false,
|
|
1582
|
+
[],
|
|
1583
|
+
"space-separated compound: final token inflected"
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
const lastStart = noun.length - lastToken.length;
|
|
1587
|
+
return applyCasePolicy(noun.slice(0, lastStart) + lastPlural, policy);
|
|
1588
|
+
}
|
|
1589
|
+
#resolveHead(head, depth, allowCompoundShape = false) {
|
|
1590
|
+
if (depth >= MAX_COMPOUND_DEPTH || head.length === 0) {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
if (!allowCompoundShape && !isPlainLatinToken(head)) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
const headSink = createSink();
|
|
1597
|
+
const text = this.#resolve(head, headSink, depth + 1, "PRESERVE");
|
|
1598
|
+
if (headSink.stage === "FALLBACK") {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
text,
|
|
1603
|
+
stage: headSink.stage,
|
|
1604
|
+
id: headSink.id,
|
|
1605
|
+
invariant: headSink.invariant
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
#pluralizeCompoundHead(head, depth, allowRespell) {
|
|
1609
|
+
const resolved = this.#resolveHead(head, depth);
|
|
1610
|
+
if (resolved === null) {
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
if (allowRespell && this.#shouldRespell(resolved, head)) {
|
|
1614
|
+
return this.#spellHead(head, "PRESERVE");
|
|
1615
|
+
}
|
|
1616
|
+
return resolved.text;
|
|
1617
|
+
}
|
|
1618
|
+
#shouldRespell(resolved, head) {
|
|
1619
|
+
if (resolved.stage !== "RULE") {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
const lowerCase = lower(head);
|
|
1623
|
+
return this.#lexicon.isOrdinaryWord(lowerCase) !== true;
|
|
1624
|
+
}
|
|
1625
|
+
#spellHead(head, policy) {
|
|
1626
|
+
if (!isPlainLatinToken(head)) {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
const lowerCase = lower(head);
|
|
1630
|
+
if (lowerCase.endsWith("child")) {
|
|
1631
|
+
return render(head, lowerCase, 5, "children", policy);
|
|
1632
|
+
}
|
|
1633
|
+
for (const [singular, plural] of F_TO_VES) {
|
|
1634
|
+
if (lowerCase.endsWith(singular)) {
|
|
1635
|
+
return render(head, lowerCase, singular.length, plural, policy);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const rule = applyProductiveRule(head, lowerCase);
|
|
1639
|
+
return render(head, lowerCase, rule.cut, rule.append, policy);
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Admission policy for the generic head-final hyphenated compound path. Exactly one separator,
|
|
1643
|
+
* both tokens plain Latin words, a multi-letter final token, no preposition, and a documented
|
|
1644
|
+
* non-noun head exclusion.
|
|
1645
|
+
*/
|
|
1646
|
+
#isHeadFinalCompound(noun, hyphen, tailLower) {
|
|
1647
|
+
if (hyphen !== noun.lastIndexOf("-")) {
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
const head = noun.slice(0, hyphen);
|
|
1651
|
+
const tail = noun.slice(hyphen + 1);
|
|
1652
|
+
if (!isPlainLatinToken(head) || !isPlainLatinToken(tail)) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
if (codePointLength(head) < 1 || codePointLength(tail) < 2) {
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
return !(isNonNounHead(tailLower) || isPreposition(lower(head)) && !isCompoundPrefix(lower(head)) || isPreposition(tailLower) || isFreeTail(tailLower));
|
|
1659
|
+
}
|
|
1660
|
+
/** Multi-hyphen modifier chains ending in a noun head, with no preposition/coordinator. */
|
|
1661
|
+
#isMultiHeadFinalCompound(noun) {
|
|
1662
|
+
const tokens = noun.split("-");
|
|
1663
|
+
if (tokens.length < 3) {
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
for (const token of tokens) {
|
|
1667
|
+
if (!isPlainLatinToken(token) || codePointLength(token) < 1) {
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
1672
|
+
const token = tokens[index];
|
|
1673
|
+
if (token === void 0) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
const tokenLower = lower(token);
|
|
1677
|
+
if (isPreposition(tokenLower) || isCoordinator(tokenLower)) {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const last = tokens[tokens.length - 1];
|
|
1682
|
+
if (last === void 0) {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
const tailLower = lower(last);
|
|
1686
|
+
return !isPreposition(tailLower) && !isNonNounHead(tailLower) && !isCoordinator(tailLower);
|
|
1687
|
+
}
|
|
1688
|
+
/** A single-hyphen verb-particle noun such as `bake-off` or `blow-in`. */
|
|
1689
|
+
#isParticleCompound(noun, hyphen, tailLower) {
|
|
1690
|
+
if (hyphen !== noun.lastIndexOf("-") || !isParticle(tailLower)) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
const head = noun.slice(0, hyphen);
|
|
1694
|
+
const tail = noun.slice(hyphen + 1);
|
|
1695
|
+
return isPlainLatinToken(head) && isPlainLatinToken(tail) && codePointLength(head) >= 1 && codePointLength(tail) >= 2;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
var Builder = class {
|
|
1699
|
+
#mode = "ANGLICIZED";
|
|
1700
|
+
#casePolicy = "PRESERVE";
|
|
1701
|
+
#overrides = /* @__PURE__ */ new Map();
|
|
1702
|
+
#initialisms = /* @__PURE__ */ new Set();
|
|
1703
|
+
#uppercasePolicy = "INFER_INITIALISM";
|
|
1704
|
+
#lexicon;
|
|
1705
|
+
withMode(mode) {
|
|
1706
|
+
this.#mode = mode;
|
|
1707
|
+
return this;
|
|
1708
|
+
}
|
|
1709
|
+
withCasePolicy(casePolicy) {
|
|
1710
|
+
this.#casePolicy = casePolicy;
|
|
1711
|
+
return this;
|
|
1712
|
+
}
|
|
1713
|
+
withUppercasePolicy(uppercasePolicy) {
|
|
1714
|
+
this.#uppercasePolicy = uppercasePolicy;
|
|
1715
|
+
return this;
|
|
1716
|
+
}
|
|
1717
|
+
/** Adds an exact, case-sensitive replacement for the whole input spelling. */
|
|
1718
|
+
withOverride(noun, plural) {
|
|
1719
|
+
if (noun.length === 0) {
|
|
1720
|
+
throw new Error("override key must not be empty");
|
|
1721
|
+
}
|
|
1722
|
+
this.#overrides.set(noun, plural);
|
|
1723
|
+
return this;
|
|
1724
|
+
}
|
|
1725
|
+
withOverrides(replacements) {
|
|
1726
|
+
for (const [noun, plural] of Object.entries(replacements)) {
|
|
1727
|
+
this.withOverride(noun, plural);
|
|
1728
|
+
}
|
|
1729
|
+
return this;
|
|
1730
|
+
}
|
|
1731
|
+
/** Declares a token an initialism: it appends `s`, or `es` after a final s. */
|
|
1732
|
+
withInitialism(token) {
|
|
1733
|
+
if (token.length === 0) {
|
|
1734
|
+
throw new Error("initialism token must not be empty");
|
|
1735
|
+
}
|
|
1736
|
+
this.#initialisms.add(lower(token));
|
|
1737
|
+
return this;
|
|
1738
|
+
}
|
|
1739
|
+
withInitialisms(tokens) {
|
|
1740
|
+
for (const token of tokens) {
|
|
1741
|
+
this.withInitialism(token);
|
|
1742
|
+
}
|
|
1743
|
+
return this;
|
|
1744
|
+
}
|
|
1745
|
+
/** Replaces the bundled lexicon with caller-supplied or custom data. */
|
|
1746
|
+
withLexicon(lexicon) {
|
|
1747
|
+
this.#lexicon = lexicon;
|
|
1748
|
+
return this;
|
|
1749
|
+
}
|
|
1750
|
+
/** Builds a rules-only instance (no bundled lexicon). */
|
|
1751
|
+
withoutBundledLexicon() {
|
|
1752
|
+
this.#lexicon = RULES_ONLY_LEXICON;
|
|
1753
|
+
return this;
|
|
1754
|
+
}
|
|
1755
|
+
build() {
|
|
1756
|
+
return new EnglishInflector({
|
|
1757
|
+
mode: this.#mode,
|
|
1758
|
+
casePolicy: this.#casePolicy,
|
|
1759
|
+
overrides: new Map(this.#overrides),
|
|
1760
|
+
initialisms: new Set(this.#initialisms),
|
|
1761
|
+
uppercasePolicy: this.#uppercasePolicy,
|
|
1762
|
+
lexicon: this.#lexicon ?? defaultLexiconProvider()
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
exports.Builder = Builder;
|
|
1768
|
+
exports.COMPOUND_EXACT = COMPOUND_EXACT;
|
|
1769
|
+
exports.COUNTABILITIES = COUNTABILITIES;
|
|
1770
|
+
exports.EnglishInflector = EnglishInflector;
|
|
1771
|
+
exports.INFLECTION_SOURCES = INFLECTION_SOURCES;
|
|
1772
|
+
exports.RULES_ONLY_LEXICON = RULES_ONLY_LEXICON;
|
|
1773
|
+
exports.createDeltaLexicon = createDeltaLexicon;
|
|
1774
|
+
exports.createFullLexicon = createFullLexicon;
|
|
1775
|
+
exports.createLexicon = createLexicon;
|
|
1776
|
+
exports.createMembershipTable = createMembershipTable;
|
|
1777
|
+
exports.decodeBase64 = decodeBase64;
|
|
1778
|
+
exports.defaultLexiconProvider = defaultLexiconProvider;
|
|
1779
|
+
exports.deriveEdit = deriveEdit;
|
|
1780
|
+
exports.fnv1aLow32 = fnv1aLow32;
|
|
1781
|
+
exports.parseFullLexicon = parseFullLexicon;
|
|
1782
|
+
exports.validateCompoundTable = validateCompoundTable;
|
|
1783
|
+
//# sourceMappingURL=rules.cjs.map
|
|
1784
|
+
//# sourceMappingURL=rules.cjs.map
|