cspell-trie-lib
Version:
Trie Data Structure to support cspell.
1,966 lines (1,945 loc) • 231 kB
JavaScript
import { opAppend, opCombine, opConcatMap, opFilter, opFlatten, opMap, opUnique, pipe, pipeSync, reduce } from "@cspell/cspell-pipe/sync";
import assert from "node:assert";
import { endianness } from "node:os";
import { genSequence } from "gensequence";
//#region src/lib/utils/memorizeLastCall.ts
const SymEmpty = Symbol("memorizeLastCall");
function memorizeLastCall(fn) {
let lastP = void 0;
let lastR = SymEmpty;
function calc(p) {
if (lastR !== SymEmpty && lastP === p) return lastR;
lastP = p;
lastR = fn(p);
return lastR;
}
return calc;
}
//#endregion
//#region src/lib/ITrieNode/find.ts
const defaultLegacyMinCompoundLength$3 = 3;
const _defaultFindOptions$1 = {
matchCase: false,
compoundMode: "compound",
legacyMinCompoundLength: defaultLegacyMinCompoundLength$3
};
Object.freeze(_defaultFindOptions$1);
const arrayCompoundModes$1 = [
"none",
"compound",
"legacy"
];
const knownCompoundModes$1 = new Map(arrayCompoundModes$1.map((a) => [a, a]));
const notFound = {
found: false,
compoundUsed: false,
caseMatched: false,
forbidden: void 0
};
Object.freeze(notFound);
/**
*
* @param root Trie root node. root.c contains the compound root and forbidden root.
* @param word A pre normalized word use `normalizeWord` or `normalizeWordToLowercase`
* @param options
*/
function findWordNode$1(root, word, options) {
return _findWordNode$1(root, word, options);
}
/**
*
* @param root Trie root node. root.c contains the compound root and forbidden root.
* @param word A pre normalized word use `normalizeWord` or `normalizeWordToLowercase`
* @param options
*/
function findWord$1(root, word, options) {
if (root.find) {
const found$1 = root.find(word, options?.matchCase || false);
if (found$1) {
if (options?.checkForbidden && found$1.forbidden === void 0) found$1.forbidden = isForbiddenWord$1(root, word, root.forbidPrefix);
return found$1;
}
if (!root.hasCompoundWords) return notFound;
}
const { found, compoundUsed, caseMatched, forbidden } = _findWordNode$1(root, word, options);
const result = {
found,
compoundUsed,
caseMatched,
forbidden
};
if (options?.checkForbidden && forbidden === void 0) result.forbidden = isForbiddenWord$1(root, word, root.forbidPrefix);
return result;
}
/**
*
* @param root Trie root node. root.c contains the compound root and forbidden root.
* @param word A pre normalized word use `normalizeWord` or `normalizeWordToLowercase`
* @param options
*/
function _findWordNode$1(root, word, options) {
const trieInfo = root.info;
const matchCase = options?.matchCase || false;
const compoundMode = knownCompoundModes$1.get(options?.compoundMode) || _defaultFindOptions$1.compoundMode;
const compoundPrefix = compoundMode === "compound" ? trieInfo.compoundCharacter ?? root.compoundFix : "";
const ignoreCasePrefix = matchCase ? "" : trieInfo.stripCaseAndAccentsPrefix ?? root.caseInsensitivePrefix;
const mustCheckForbidden = options?.checkForbidden === true;
const checkForbidden = options?.checkForbidden ?? true;
function __findCompound() {
const f = findCompoundWord$1(root, word, compoundPrefix, ignoreCasePrefix);
if (f.found !== false && (mustCheckForbidden || f.compoundUsed && checkForbidden)) {
const r = !f.caseMatched ? walk$2(root, root.caseInsensitivePrefix) : root;
f.forbidden = isForbiddenWord$1(r, word, root.forbidPrefix);
}
return f;
}
function __findExact() {
const n = root.getNode ? root.getNode(word) : walk$2(root, word);
const isFound = isEndOfWordNode$1(n);
const result = {
found: isFound && word,
compoundUsed: false,
forbidden: checkForbidden ? isForbiddenWord$1(root, word, root.forbidPrefix) : void 0,
node: n,
caseMatched: true
};
return result;
}
switch (compoundMode) {
case "none": return matchCase ? __findExact() : __findCompound();
case "compound": return __findCompound();
case "legacy": return findLegacyCompound$1(root, word, options);
}
}
function findLegacyCompound$1(root, word, options) {
const roots = [root];
if (!options?.matchCase) roots.push(walk$2(root, root.caseInsensitivePrefix));
return findLegacyCompoundNode$1(roots, word, options?.legacyMinCompoundLength || defaultLegacyMinCompoundLength$3);
}
function findCompoundNode$1(root, word, compoundCharacter, ignoreCasePrefix) {
const stack = [{
n: root,
compoundPrefix: ignoreCasePrefix,
cr: void 0,
caseMatched: true
}];
const compoundPrefix = compoundCharacter || ignoreCasePrefix;
const possibleCompoundPrefix = ignoreCasePrefix && compoundCharacter ? ignoreCasePrefix + compoundCharacter : "";
const nw = word.normalize();
const w = [...nw];
function determineRoot(s) {
const prefix = s.compoundPrefix;
let r = root;
let i$1;
for (i$1 = 0; i$1 < prefix.length && r; ++i$1) r = r.get(prefix[i$1]);
const caseMatched$1 = s.caseMatched && prefix[0] !== ignoreCasePrefix;
return {
n: s.n,
compoundPrefix: prefix === compoundPrefix ? possibleCompoundPrefix : "",
cr: r,
caseMatched: caseMatched$1
};
}
let compoundUsed = false;
let caseMatched = true;
let i = 0;
let node;
while (true) {
const s = stack[i];
const h = w[i++];
const n = s.cr || s.n;
const c = h && n?.get(h) || void 0;
if (c && i < word.length) {
caseMatched = s.caseMatched;
stack[i] = {
n: c,
compoundPrefix,
cr: void 0,
caseMatched
};
} else if (!c || !c.eow) {
node = node || c;
while (--i > 0) {
const s$1 = stack[i];
if (!s$1.compoundPrefix || !s$1.n?.hasChildren()) continue;
if (s$1.n.get(compoundCharacter)) break;
}
if (i >= 0 && stack[i].compoundPrefix) {
compoundUsed = i > 0;
const r = determineRoot(stack[i]);
stack[i] = r;
if (!r.cr) break;
if (!i && !r.caseMatched && nw !== nw.toLowerCase()) break;
} else break;
} else {
node = c;
caseMatched = s.caseMatched;
break;
}
}
const found = i === word.length && word || false;
const result = {
found,
compoundUsed,
node,
forbidden: void 0,
caseMatched
};
return result;
}
function findCompoundWord$1(root, word, compoundCharacter, ignoreCasePrefix) {
const { found, compoundUsed, node, caseMatched } = findCompoundNode$1(root, word, compoundCharacter, ignoreCasePrefix);
if (!node || !node.eow) return {
found: false,
compoundUsed,
node,
forbidden: void 0,
caseMatched
};
return {
found,
compoundUsed,
node,
forbidden: void 0,
caseMatched
};
}
function findWordExact$1(root, word) {
const r = root;
if (r?.findExact) return r.findExact(word);
return isEndOfWordNode$1(walk$2(root, word));
}
function isEndOfWordNode$1(n) {
return !!n?.eow;
}
function walk$2(root, word) {
const w = [...word];
let n = root;
let i = 0;
while (n && i < w.length) {
const h = w[i++];
n = n.get(h);
}
return n;
}
function findLegacyCompoundNode$1(roots, word, minCompoundLength) {
const root = roots[0];
const numRoots = roots.length;
const stack = [{
n: root,
usedRoots: 1,
subLength: 0,
isCompound: false,
cr: void 0,
caseMatched: true
}];
const w = word;
const wLen = w.length;
let compoundUsed = false;
let caseMatched = true;
let i = 0;
let node;
while (true) {
const s = stack[i];
const h = w[i++];
const n = s.cr || s.n;
const c = n?.get(h);
if (c && i < wLen) stack[i] = {
n: c,
usedRoots: 0,
subLength: s.subLength + 1,
isCompound: s.isCompound,
cr: void 0,
caseMatched: s.caseMatched
};
else if (!c || !c.eow || c.eow && s.subLength < minCompoundLength - 1) {
while (--i > 0) {
const s$1 = stack[i];
if (s$1.usedRoots < numRoots && s$1.n?.eow && (s$1.subLength >= minCompoundLength || !s$1.subLength) && wLen - i >= minCompoundLength) break;
}
if (i > 0 || stack[i].usedRoots < numRoots) {
compoundUsed = i > 0;
const s$1 = stack[i];
s$1.cr = roots[s$1.usedRoots++];
s$1.subLength = 0;
s$1.isCompound = compoundUsed;
s$1.caseMatched = s$1.caseMatched && s$1.usedRoots <= 1;
} else break;
} else {
node = c;
caseMatched = s.caseMatched;
break;
}
}
function extractWord() {
if (!word || i < word.length) return false;
const letters = [];
let subLen = 0;
for (let j = 0; j < i; ++j) {
const { subLength } = stack[j];
if (subLength < subLen) letters.push("+");
letters.push(word[j]);
subLen = subLength;
}
return letters.join("");
}
const found = extractWord();
const result = {
found,
compoundUsed,
node,
forbidden: void 0,
caseMatched
};
return result;
}
function isForbiddenWord$1(root, word, forbiddenPrefix) {
const r = root;
if (r?.isForbidden) return r.isForbidden(word);
return findWordExact$1(root?.get(forbiddenPrefix), word);
}
const createFindOptions$1 = memorizeLastCall(_createFindOptions$1);
function _createFindOptions$1(options) {
if (!options) return _defaultFindOptions$1;
const d = _defaultFindOptions$1;
return {
matchCase: options.matchCase ?? d.matchCase,
compoundMode: options.compoundMode ?? d.compoundMode,
legacyMinCompoundLength: options.legacyMinCompoundLength ?? d.legacyMinCompoundLength,
checkForbidden: options.checkForbidden ?? d.checkForbidden
};
}
//#endregion
//#region src/lib/walker/walkerTypes.ts
const JOIN_SEPARATOR = "+";
const WORD_SEPARATOR = " ";
let CompoundWordsMethod = /* @__PURE__ */ function(CompoundWordsMethod$1) {
/**
* Do not compound words.
*/
CompoundWordsMethod$1[CompoundWordsMethod$1["NONE"] = 0] = "NONE";
/**
* Create word compounds separated by spaces.
*/
CompoundWordsMethod$1[CompoundWordsMethod$1["SEPARATE_WORDS"] = 1] = "SEPARATE_WORDS";
/**
* Create word compounds without separation.
*/
CompoundWordsMethod$1[CompoundWordsMethod$1["JOIN_WORDS"] = 2] = "JOIN_WORDS";
return CompoundWordsMethod$1;
}({});
//#endregion
//#region src/lib/ITrieNode/walker/walker.ts
/**
* Walks the Trie and yields a value at each node.
* next(goDeeper: boolean):
*/
function* compoundWalker$1(root, compoundingMethod) {
const empty = Object.freeze([]);
const roots = {
[CompoundWordsMethod.NONE]: empty,
[CompoundWordsMethod.JOIN_WORDS]: [[JOIN_SEPARATOR, root]],
[CompoundWordsMethod.SEPARATE_WORDS]: [[WORD_SEPARATOR, root]]
};
const rc = roots[compoundingMethod].length ? roots[compoundingMethod] : void 0;
function children(n) {
if (n.hasChildren()) {
const entries = n.entries();
const c = Array.isArray(entries) ? entries : [...entries];
return n.eow && rc ? [...c, ...rc] : c;
}
if (n.eow) return roots[compoundingMethod];
return empty;
}
let depth = 0;
const stack = [];
stack[depth] = {
t: "",
c: children(root),
ci: 0
};
while (depth >= 0) {
let s = stack[depth];
let baseText = s.t;
while (s.ci < s.c.length) {
const [char, node] = s.c[s.ci++];
const text = baseText + char;
const goDeeper = yield {
text,
node,
depth
};
if (goDeeper ?? true) {
depth++;
baseText = text;
stack[depth] = {
t: text,
c: children(node),
ci: 0
};
}
s = stack[depth];
}
depth -= 1;
}
}
/**
* Walks the Trie and yields a value at each node.
* next(goDeeper: boolean):
*/
function* nodeWalker$1(root) {
let depth = 0;
const stack = [];
const entries = root.entries();
stack[depth] = {
t: "",
n: root,
c: Array.isArray(entries) ? entries : [...entries],
ci: 0
};
while (depth >= 0) {
let s = stack[depth];
let baseText = s.t;
while (s.ci < s.c.length && s.n) {
const idx$1 = s.ci++;
const [char, node] = s.c[idx$1];
const text = baseText + char;
const goDeeper = yield {
text,
node,
depth
};
if (goDeeper !== false) {
depth++;
baseText = text;
const s$1 = stack[depth];
const entries$1 = node.entries();
const c = Array.isArray(entries$1) ? entries$1 : [...entries$1];
if (s$1) {
s$1.t = text;
s$1.n = node;
s$1.c = c;
s$1.ci = 0;
} else stack[depth] = {
t: text,
n: node,
c,
ci: 0
};
}
s = stack[depth];
}
depth -= 1;
}
}
function walker$1(root, compoundingMethod = CompoundWordsMethod.NONE) {
return compoundingMethod === CompoundWordsMethod.NONE ? nodeWalker$1(root) : compoundWalker$1(root, compoundingMethod);
}
function walkerWords$1(root) {
return walkerWordsITrie(root);
}
/**
* Walks the Trie and yields each word.
*/
function* walkerWordsITrie(root) {
let depth = 0;
const stack = [];
const entries = root.entries();
const c = Array.isArray(entries) ? entries : [...entries];
stack[depth] = {
t: "",
n: root,
c,
ci: 0
};
while (depth >= 0) {
let s = stack[depth];
let baseText = s.t;
while (s.ci < s.c.length && s.n) {
const [char, node] = s.c[s.ci++];
if (!node) continue;
const text = baseText + char;
if (node.eow) yield text;
depth++;
baseText = text;
const entries$1 = node.entries();
const c$1 = Array.isArray(entries$1) ? entries$1 : [...entries$1];
if (stack[depth]) {
s = stack[depth];
s.t = text;
s.n = node;
s.c = c$1;
s.ci = 0;
} else stack[depth] = {
t: text,
n: node,
c: c$1,
ci: 0
};
s = stack[depth];
}
depth -= 1;
}
}
//#endregion
//#region src/lib/ITrieNode/trie-util.ts
/**
* Generate a Iterator that can walk a Trie and yield the words.
*/
function iteratorTrieWords$1(node) {
return walkerWords$1(node);
}
function findNode$1(node, word) {
for (let i = 0; i < word.length; ++i) {
const n = node.get(word[i]);
if (!n) return void 0;
node = n;
}
return node;
}
function countWords$1(root) {
const visited = /* @__PURE__ */ new Map();
function walk$3(n) {
const nestedCount = visited.get(n.id);
if (nestedCount !== void 0) return nestedCount;
let cnt = n.eow ? 1 : 0;
visited.set(n, cnt);
for (const c of n.values()) cnt += walk$3(c);
visited.set(n, cnt);
return cnt;
}
return walk$3(root);
}
//#endregion
//#region src/lib/utils/isDefined.ts
function isDefined(t) {
return t !== void 0;
}
//#endregion
//#region src/lib/walker/hintedWalker.ts
function hintedWalker(root, ignoreCase, hint, compoundingMethod, emitWordSeparator) {
return hintedWalkerNext(root, ignoreCase, hint, compoundingMethod, emitWordSeparator);
}
/**
* Walks the Trie and yields a value at each node.
* next(goDeeper: boolean):
*/
function* hintedWalkerNext(root, ignoreCase, hint, compoundingMethod, emitWordSeparator = "") {
const _compoundingMethod = compoundingMethod ?? CompoundWordsMethod.NONE;
const compoundCharacter = root.compoundCharacter;
const noCaseCharacter = root.stripCaseAndAccentsPrefix;
const rawRoots = [root, ignoreCase ? root.c[noCaseCharacter] : void 0].filter(isDefined);
const specialRootsPrefix = existMap([
compoundCharacter,
noCaseCharacter,
root.forbiddenWordPrefix
]);
function filterRoot(root$1) {
const children$1 = root$1.c && Object.entries(root$1.c);
const c = children$1?.filter(([v]) => !(v in specialRootsPrefix));
return { c: c && Object.fromEntries(c) };
}
const roots = rawRoots.map(filterRoot);
const compoundRoots = rawRoots.map((r) => r.c?.[compoundCharacter]).filter(isDefined);
const setOfCompoundRoots = new Set(compoundRoots);
const rootsForCompoundMethods = [...roots, ...compoundRoots];
const compoundMethodRoots = {
[CompoundWordsMethod.NONE]: [],
[CompoundWordsMethod.JOIN_WORDS]: rootsForCompoundMethods.map((r) => [JOIN_SEPARATOR, r]),
[CompoundWordsMethod.SEPARATE_WORDS]: rootsForCompoundMethods.map((r) => [WORD_SEPARATOR, r])
};
function* children(n, hintOffset) {
if (n.c) {
const h = hint.slice(hintOffset, hintOffset + 3) + hint.slice(Math.max(0, hintOffset - 2), hintOffset);
const hints = new Set(h);
const c = n.c;
yield* [...hints].filter((a) => a in c).map((letter) => ({
letter,
node: c[letter],
hintOffset: hintOffset + 1
}));
hints.add(compoundCharacter);
yield* Object.entries(c).filter((a) => !hints.has(a[0])).map(([letter, node]) => ({
letter,
node,
hintOffset: hintOffset + 1
}));
if (compoundCharacter in c && !setOfCompoundRoots.has(n)) for (const compoundRoot of compoundRoots) for (const child of children(compoundRoot, hintOffset)) {
const { letter, node, hintOffset: hintOffset$1 } = child;
yield {
letter: emitWordSeparator + letter,
node,
hintOffset: hintOffset$1
};
}
}
if (n.f) yield* [...compoundMethodRoots[_compoundingMethod]].map(([letter, node]) => ({
letter,
node,
hintOffset
}));
}
for (const root$1 of roots) {
let depth = 0;
const stack = [];
const stackText = [""];
stack[depth] = children(root$1, depth);
let ir;
while (depth >= 0) {
while (!(ir = stack[depth].next()).done) {
const { letter: char, node, hintOffset } = ir.value;
const text = stackText[depth] + char;
const hinting = yield {
text,
node,
depth
};
if (hinting && hinting.goDeeper) {
depth++;
stackText[depth] = text;
stack[depth] = children(node, hintOffset);
}
}
depth -= 1;
}
}
}
function existMap(values) {
const m = Object.create(null);
for (const v of values) m[v] = true;
return m;
}
//#endregion
//#region src/lib/TrieNode/trie.ts
function trieRootToITrieRoot(root) {
return ImplITrieRoot.toITrieNode(root);
}
const EmptyKeys$2 = Object.freeze([]);
const EmptyValues = Object.freeze([]);
const EmptyEntries$2 = Object.freeze([]);
var ImplITrieNode = class ImplITrieNode {
id;
_keys;
constructor(node) {
this.node = node;
this.id = node;
}
/** flag End of Word */
get eow() {
return !!this.node.f;
}
/** number of children */
get size() {
if (!this.node.c) return 0;
return this.keys().length;
}
/** get keys to children */
keys() {
if (this._keys) return this._keys;
const keys = this.node.c ? Object.keys(this.node.c) : EmptyKeys$2;
this._keys = keys;
return keys;
}
/** get the child nodes */
values() {
return !this.node.c ? EmptyValues : Object.values(this.node.c).map((n) => ImplITrieNode.toITrieNode(n));
}
entries() {
return !this.node.c ? EmptyEntries$2 : Object.entries(this.node.c).map(([k, n]) => [k, ImplITrieNode.toITrieNode(n)]);
}
/** get child ITrieNode */
get(char) {
const n = this.node.c?.[char];
if (!n) return void 0;
return ImplITrieNode.toITrieNode(n);
}
getNode(chars) {
return this.findNode(chars);
}
has(char) {
const c = this.node.c;
return c && char in c || false;
}
child(keyIdx) {
const char = this.keys()[keyIdx];
const n = char && this.get(char);
if (!n) throw new Error("Index out of range.");
return n;
}
hasChildren() {
return !!this.node.c;
}
#findTrieNode(word) {
let node = this.node;
for (const char of word) {
if (!node) return void 0;
node = node.c?.[char];
}
return node;
}
findNode(word) {
const node = this.#findTrieNode(word);
return node && ImplITrieNode.toITrieNode(node);
}
findExact(word) {
const node = this.#findTrieNode(word);
return !!node && !!node.f;
}
static toITrieNode(node) {
return new this(node);
}
};
var ImplITrieRoot = class extends ImplITrieNode {
info;
hasForbiddenWords;
hasCompoundWords;
hasNonStrictWords;
constructor(root) {
super(root);
this.root = root;
const { stripCaseAndAccentsPrefix, compoundCharacter, forbiddenWordPrefix, isCaseAware } = root;
this.info = {
stripCaseAndAccentsPrefix,
compoundCharacter,
forbiddenWordPrefix,
isCaseAware
};
this.hasForbiddenWords = !!root.c[forbiddenWordPrefix];
this.hasCompoundWords = !!root.c[compoundCharacter];
this.hasNonStrictWords = !!root.c[stripCaseAndAccentsPrefix];
}
get eow() {
return false;
}
resolveId(id) {
const n = id;
return new ImplITrieNode(n);
}
get forbidPrefix() {
return this.root.forbiddenWordPrefix;
}
get compoundFix() {
return this.root.compoundCharacter;
}
get caseInsensitivePrefix() {
return this.root.stripCaseAndAccentsPrefix;
}
static toITrieNode(node) {
return new this(node);
}
};
//#endregion
//#region src/lib/walker/walker.ts
/**
* Walks the Trie and yields a value at each node.
* next(goDeeper: boolean):
*/
function* compoundWalker(root, compoundingMethod) {
const roots = {
[CompoundWordsMethod.NONE]: [],
[CompoundWordsMethod.JOIN_WORDS]: [[JOIN_SEPARATOR, root]],
[CompoundWordsMethod.SEPARATE_WORDS]: [[WORD_SEPARATOR, root]]
};
const rc = roots[compoundingMethod].length ? roots[compoundingMethod] : void 0;
const empty = [];
function children(n) {
if (n.c && n.f && rc) return [...Object.entries(n.c), ...rc];
if (n.c) return Object.entries(n.c);
if (n.f && rc) return rc;
return empty;
}
let depth = 0;
const stack = [];
stack[depth] = {
t: "",
c: children(root),
ci: 0
};
while (depth >= 0) {
let s = stack[depth];
let baseText = s.t;
while (s.ci < s.c.length) {
const [char, node] = s.c[s.ci++];
const text = baseText + char;
const goDeeper = yield {
text,
node,
depth
};
if (goDeeper ?? true) {
depth++;
baseText = text;
stack[depth] = {
t: text,
c: children(node),
ci: 0
};
}
s = stack[depth];
}
depth -= 1;
}
}
/**
* Walks the Trie and yields a value at each node.
* next(goDeeper: boolean):
*/
function* nodeWalker(root) {
const empty = [];
function children(n) {
if (n.c) return Object.keys(n.c);
return empty;
}
let depth = 0;
const stack = [];
stack[depth] = {
t: "",
n: root.c,
c: children(root),
ci: 0
};
while (depth >= 0) {
let s = stack[depth];
let baseText = s.t;
while (s.ci < s.c.length && s.n) {
const char = s.c[s.ci++];
const node = s.n[char];
const text = baseText + char;
const goDeeper = yield {
text,
node,
depth
};
if (goDeeper !== false) {
depth++;
baseText = text;
const s$1 = stack[depth];
const c = children(node);
if (s$1) {
s$1.t = text;
s$1.n = node.c;
s$1.c = c;
s$1.ci = 0;
} else stack[depth] = {
t: text,
n: node.c,
c,
ci: 0
};
}
s = stack[depth];
}
depth -= 1;
}
}
const walkerWords = _walkerWords;
/**
* Walks the Trie and yields each word.
*/
function* _walkerWords(root) {
const empty = [];
function children(n) {
if (n.c) return Object.keys(n.c);
return empty;
}
let depth = 0;
const stack = [];
stack[depth] = {
t: "",
n: root.c,
c: children(root),
ci: 0
};
while (depth >= 0) {
let s = stack[depth];
let baseText = s.t;
while (s.ci < s.c.length && s.n) {
const char = s.c[s.ci++];
const node = s.n[char];
const text = baseText + char;
if (node.f) yield text;
depth++;
baseText = text;
const c = children(node);
if (stack[depth]) {
s = stack[depth];
s.t = text;
s.n = node.c;
s.c = c;
s.ci = 0;
} else stack[depth] = {
t: text,
n: node.c,
c,
ci: 0
};
s = stack[depth];
}
depth -= 1;
}
}
function walker(root, compoundingMethod = CompoundWordsMethod.NONE) {
return compoundingMethod === CompoundWordsMethod.NONE ? nodeWalker(root) : compoundWalker(root, compoundingMethod);
}
//#endregion
//#region src/lib/suggestions/genSuggestionsOptions.ts
const defaultGenSuggestionOptions = {
compoundMethod: CompoundWordsMethod.NONE,
ignoreCase: true,
changeLimit: 5
};
const defaultSuggestionOptions = {
...defaultGenSuggestionOptions,
numSuggestions: 8,
includeTies: true,
timeout: 5e3
};
const keyMapOfGenSuggestionOptionsStrict = {
changeLimit: "changeLimit",
compoundMethod: "compoundMethod",
ignoreCase: "ignoreCase",
compoundSeparator: "compoundSeparator"
};
const keyMapOfSuggestionOptionsStrict = {
...keyMapOfGenSuggestionOptionsStrict,
filter: "filter",
includeTies: "includeTies",
numSuggestions: "numSuggestions",
timeout: "timeout",
weightMap: "weightMap"
};
/**
* Create suggestion options using composition.
* @param opts - partial options.
* @returns Options - with defaults.
*/
function createSuggestionOptions(...opts) {
const options = { ...defaultSuggestionOptions };
const keys = Object.keys(keyMapOfSuggestionOptionsStrict);
for (const opt of opts) for (const key of keys) assign(options, opt, key);
return options;
}
function assign(dest, src, k) {
dest[k] = src[k] ?? dest[k];
}
//#endregion
//#region src/lib/utils/PairingHeap.ts
var PairingHeap = class {
_heap;
_size = 0;
constructor(compare$3) {
this.compare = compare$3;
}
/** Add an item to the heap. */
add(v) {
this._heap = insert$1(this.compare, this._heap, v);
++this._size;
return this;
}
/** take an item from the heap. */
dequeue() {
const n = this.next();
if (n.done) return void 0;
return n.value;
}
/** Add items to the heap */
append(i) {
for (const v of i) this.add(v);
return this;
}
/** get the next value */
next() {
if (!this._heap) return {
value: void 0,
done: true
};
const value = this._heap.v;
--this._size;
this._heap = removeHead(this.compare, this._heap);
return { value };
}
/** peek at the next value without removing it. */
peek() {
return this._heap?.v;
}
[Symbol.iterator]() {
return this;
}
/** alias of `size` */
get length() {
return this._size;
}
/** number of entries in the heap. */
get size() {
return this._size;
}
};
function removeHead(compare$3, heap) {
if (!heap || !heap.c) return void 0;
return mergeSiblings(compare$3, heap.c);
}
function insert$1(compare$3, heap, v) {
const n = {
v,
s: void 0,
c: void 0
};
if (!heap || compare$3(v, heap.v) <= 0) {
n.c = heap;
return n;
}
n.s = heap.c;
heap.c = n;
return heap;
}
function merge(compare$3, a, b) {
if (compare$3(a.v, b.v) <= 0) {
a.s = void 0;
b.s = a.c;
a.c = b;
return a;
}
b.s = void 0;
a.s = b.c;
b.c = a;
return b;
}
function mergeSiblings(compare$3, n) {
if (!n.s) return n;
const s = n.s;
const ss = s.s;
const m = merge(compare$3, n, s);
return ss ? merge(compare$3, m, mergeSiblings(compare$3, ss)) : m;
}
//#endregion
//#region src/lib/suggestions/constants.ts
const DEFAULT_COMPOUNDED_WORD_SEPARATOR = "∙";
const opCosts = {
baseCost: 100,
swapCost: 75,
duplicateLetterCost: 80,
compound: 1,
visuallySimilar: 1,
firstLetterBias: 5,
wordBreak: 99,
wordLengthCostFactor: .5
};
//#endregion
//#region src/lib/suggestions/orthography.ts
const intl = new Intl.Collator("en", { sensitivity: "base" });
const compare$2 = intl.compare;
/**
* This a set of letters that look like each other.
* There can be a maximum of 30 groups.
* It is possible for a letter to appear in more than 1 group, but not encouraged.
*/
const visualLetterGroups = [
forms("ǎàåÄÀAãâáǟặắấĀāăąaäæɐɑαаᾳ") + "ᾳ",
forms("Bbḃвъь"),
forms("ċČčcĉçCÇćĊСсς"),
forms("ḎḋḏḑďđḍDd"),
forms("ēëÈÊËềéèếệĕeEĒėęěêəɛёЁеʒ"),
forms("fḟFff"),
forms("ġĠĞǧĝģGgɣ"),
forms("ħĦĥḥHhḤȟн"),
forms("IįïİÎÍīiÌìíîıɪɨїΊΙ"),
forms("jJĵ"),
forms("ķKkκкќ"),
forms("ḷłľļLlĺḶίι"),
forms("Mṃṁm"),
forms("nņÑNṇňŇñńŋѝий"),
forms("ÒOøȭŌōőỏoÖòȱȯóôõöơɔόδо"),
forms("PṗpрРρ"),
forms("Qq"),
forms("řRṛrŕŗѓгя"),
forms("ṣšȘṢsSŠṡŞŝśșʃΣ"),
forms("tțȚťTṭṬṫ"),
forms("ÜüûŪưůūűúÛŭÙùuųU"),
forms("Vvν"),
forms("ŵwWẃẅẁωш"),
forms("xXх"),
forms("ÿýYŷyÝỳУўу"),
forms("ZẓžŽżŻźz")
];
function forms(letters) {
const n = letters.normalize("NFC").replaceAll(/\p{M}/gu, "");
const na = n.normalize("NFD").replaceAll(/\p{M}/gu, "");
const s = new Set(n + n.toLowerCase() + n.toUpperCase() + na + na.toLowerCase() + na.toUpperCase());
return [...s].join("");
}
/**
* This is a map of letters to groups mask values.
* If two letters are part of the same group then `visualLetterMaskMap[a] & visualLetterMaskMap[b] !== 0`
*/
const visualLetterMaskMap = calcVisualLetterMasks(visualLetterGroups);
/**
*
* @param groups
* @returns
*/
function calcVisualLetterMasks(groups) {
const map = Object.create(null);
for (let i = 0; i < groups.length; ++i) {
const m = 1 << i;
const g = groups[i];
for (const c of g) map[c] = (map[c] || 0) | m;
}
return map;
}
//#endregion
//#region src/lib/distance/weightedMaps.ts
const matchPossibleWordSeparators = /[+∙•・●]/g;
function createWeightMap(...defs) {
const map = _createWeightMap();
addDefsToWeightMap(map, defs);
return map;
}
function addDefToWeightMap(map, ...defs) {
return addDefsToWeightMap(map, defs);
}
function addAdjustment(map, ...adjustments) {
for (const adj of adjustments) map.adjustments.set(adj.id, adj);
return map;
}
function addDefsToWeightMap(map, defs) {
function addSet(set, def) {
addSetToTrieCost(map.insDel, set, def.insDel, def.penalty);
addSetToTrieTrieCost(map.replace, set, def.replace, def.penalty);
addSetToTrieTrieCost(map.swap, set, def.swap, def.penalty);
}
for (const _def of defs) {
const def = normalizeDef(_def);
const mapSets = splitMap$1(def);
mapSets.forEach((s) => addSet(s, def));
}
return map;
}
function _createWeightMap() {
return {
insDel: {},
replace: {},
swap: {},
adjustments: /* @__PURE__ */ new Map()
};
}
function lowest(a, b) {
if (a === void 0) return b;
if (b === void 0) return a;
return a <= b ? a : b;
}
function highest(a, b) {
if (a === void 0) return b;
if (b === void 0) return a;
return a >= b ? a : b;
}
function normalize(s) {
const f = new Set([s]);
f.add(s.normalize("NFC"));
f.add(s.normalize("NFD"));
return f;
}
function* splitMapSubstringsIterable(map) {
let seq = "";
let mode = 0;
for (const char of map) {
if (mode && char === ")") {
yield* normalize(seq);
mode = 0;
continue;
}
if (mode) {
seq += char;
continue;
}
if (char === "(") {
mode = 1;
seq = "";
continue;
}
yield* normalize(char);
}
}
function splitMapSubstrings(map) {
return [...splitMapSubstringsIterable(map)];
}
/**
* Splits a WeightedMapDef.map
* @param map
*/
function splitMap$1(def) {
const { map } = def;
const sets = map.split("|");
return sets.map(splitMapSubstrings).filter((s) => s.length > 0);
}
function addToTrieCost(trie, str, cost, penalties) {
if (!str) return;
let t = trie;
for (const c of str) {
const n = t.n = t.n || Object.create(null);
t = n[c] = n[c] || Object.create(null);
}
t.c = lowest(t.c, cost);
t.p = highest(t.p, penalties);
}
function addToTrieTrieCost(trie, left, right, cost, penalties) {
let t = trie;
for (const c of left) {
const n = t.n = t.n || Object.create(null);
t = n[c] = n[c] || Object.create(null);
}
const trieCost = t.t = t.t || Object.create(null);
addToTrieCost(trieCost, right, cost, penalties);
}
function addSetToTrieCost(trie, set, cost, penalties) {
if (cost === void 0) return;
for (const str of set) addToTrieCost(trie, str, cost, penalties);
}
function addSetToTrieTrieCost(trie, set, cost, penalties) {
if (cost === void 0) return;
for (const left of set) for (const right of set) {
if (left === right) continue;
addToTrieTrieCost(trie, left, right, cost, penalties);
}
}
function* searchTrieNodes(trie, str, i) {
const len = str.length;
for (let n = trie.n; i < len && n;) {
const t = n[str[i]];
if (!t) return;
++i;
yield {
i,
t
};
n = t.n;
}
}
function* findTrieCostPrefixes(trie, str, i) {
for (const n of searchTrieNodes(trie, str, i)) {
const { c, p } = n.t;
if (c !== void 0) yield {
i: n.i,
c,
p: p || 0
};
}
}
function* findTrieTrieCostPrefixes(trie, str, i) {
for (const n of searchTrieNodes(trie, str, i)) {
const t = n.t.t;
if (t !== void 0) yield {
i: n.i,
t
};
}
}
function createWeightCostCalculator(weightMap) {
return new _WeightCostCalculator(weightMap);
}
var _WeightCostCalculator = class {
constructor(weightMap) {
this.weightMap = weightMap;
}
*calcInsDelCosts(pos) {
const { a, ai, b, bi, c, p } = pos;
for (const del of findTrieCostPrefixes(this.weightMap.insDel, a, ai)) yield {
a,
b,
ai: del.i,
bi,
c: c + del.c,
p: p + del.p
};
for (const ins of findTrieCostPrefixes(this.weightMap.insDel, b, bi)) yield {
a,
b,
ai,
bi: ins.i,
c: c + ins.c,
p: p + ins.p
};
}
*calcReplaceCosts(pos) {
const { a, ai, b, bi, c, p } = pos;
for (const del of findTrieTrieCostPrefixes(this.weightMap.replace, a, ai)) for (const ins of findTrieCostPrefixes(del.t, b, bi)) yield {
a,
b,
ai: del.i,
bi: ins.i,
c: c + ins.c,
p: p + ins.p
};
}
*calcSwapCosts(pos) {
const { a, ai, b, bi, c, p } = pos;
const swap = this.weightMap.swap;
for (const left of findTrieTrieCostPrefixes(swap, a, ai)) for (const right of findTrieCostPrefixes(left.t, a, left.i)) {
const sw = a.slice(left.i, right.i) + a.slice(ai, left.i);
if (b.slice(bi).startsWith(sw)) {
const len = sw.length;
yield {
a,
b,
ai: ai + len,
bi: bi + len,
c: c + right.c,
p: p + right.p
};
}
}
}
calcAdjustment(word) {
let penalty = 0;
for (const adj of this.weightMap.adjustments.values()) if (adj.regexp.global) for (const _m of word.matchAll(adj.regexp)) penalty += adj.penalty;
else if (adj.regexp.test(word)) penalty += adj.penalty;
return penalty;
}
};
function normalizeDef(def) {
const { map,...rest } = def;
return {
...rest,
map: normalizeMap(map)
};
}
function normalizeMap(map) {
return map.replaceAll(matchPossibleWordSeparators, DEFAULT_COMPOUNDED_WORD_SEPARATOR);
}
//#endregion
//#region src/lib/distance/distanceAStarWeighted.ts
/**
* Calculate the edit distance between two words using an A* algorithm.
*
* Using basic weights, this algorithm has the same results as the Damerau-Levenshtein algorithm.
*/
function distanceAStarWeighted(wordA, wordB, map, cost = 100) {
const calc = createWeightCostCalculator(map);
const best = _distanceAStarWeightedEx(wordA, wordB, calc, cost);
const penalty = calc.calcAdjustment(wordB);
return best.c + best.p + penalty;
}
function _distanceAStarWeightedEx(wordA, wordB, map, cost = 100) {
const a = "^" + wordA + "$";
const b = "^" + wordB + "$";
const aN = a.length;
const bN = b.length;
const candidates = new CandidatePool(aN, bN);
candidates.add({
ai: 0,
bi: 0,
c: 0,
p: 0,
f: void 0
});
/** Substitute / Replace */
function opSub(n) {
const { ai, bi, c, p } = n;
if (ai < aN && bi < bN) {
const cc = a[ai] === b[bi] ? c : c + cost;
candidates.add({
ai: ai + 1,
bi: bi + 1,
c: cc,
p,
f: n
});
}
}
/** Insert */
function opIns(n) {
const { ai, bi, c, p } = n;
if (bi < bN) candidates.add({
ai,
bi: bi + 1,
c: c + cost,
p,
f: n
});
}
/** Delete */
function opDel(n) {
const { ai, bi, c, p } = n;
if (ai < aN) candidates.add({
ai: ai + 1,
bi,
c: c + cost,
p,
f: n
});
}
/** Swap adjacent letters */
function opSwap(n) {
const { ai, bi, c, p } = n;
if (a[ai] === b[bi + 1] && a[ai + 1] === b[bi]) candidates.add({
ai: ai + 2,
bi: bi + 2,
c: c + cost,
p,
f: n
});
}
function opMap$1(n) {
const { ai, bi, c, p } = n;
const pos = {
a,
b,
ai,
bi,
c,
p
};
const costCalculations = [
map.calcInsDelCosts(pos),
map.calcSwapCosts(pos),
map.calcReplaceCosts(pos)
];
costCalculations.forEach((iter) => {
for (const nn of iter) candidates.add({
...nn,
f: n
});
});
}
let best;
while (best = candidates.next()) {
if (best.ai === aN && best.bi === bN) break;
opSwap(best);
opIns(best);
opDel(best);
opMap$1(best);
opSub(best);
}
assert(best);
return best;
}
var CandidatePool = class {
pool = new PairingHeap(compare$1);
grid = [];
constructor(aN, bN) {
this.aN = aN;
this.bN = bN;
}
next() {
let n;
while (n = this.pool.dequeue()) if (!n.d) return n;
return void 0;
}
add(n) {
const i = idx(n.ai, n.bi, this.bN);
const g = this.grid[i];
if (!g) {
this.grid[i] = n;
this.pool.add(n);
return;
}
if (g.c <= n.c) return;
g.d = true;
this.grid[i] = n;
this.pool.add(n);
}
};
function idx(r, c, cols) {
return r * cols + c;
}
function compare$1(a, b) {
return a.c - b.c || b.ai + b.bi - a.ai - a.bi;
}
//#endregion
//#region src/lib/distance/levenshtein.ts
const initialRow = [...".".repeat(50)].map((_, i) => i);
Object.freeze(initialRow);
/**
* Damerau–Levenshtein distance
* [Damerau–Levenshtein distance - Wikipedia](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance)
* @param a - first word
* @param b - second word
* @returns Distance value
*/
function levenshteinDistance(a, b) {
const aa = " " + a;
const bb = " " + b;
const nA = a.length + 1;
const nB = b.length + 1;
const firstRow = initialRow.slice(0, nA + 1);
for (let i = firstRow.length; i <= nA; ++i) firstRow[i] = i;
const matrix = [
firstRow,
[1, ...firstRow],
[
2,
1,
...firstRow
]
];
let ppRow = matrix[0];
let pRow = matrix[1];
for (let j = 2; j <= nB; ++j) {
const row = matrix[j % 3];
row[0] = pRow[0] + 1;
row[1] = pRow[1] + 1;
const bp = bb[j - 1];
const bc = bb[j];
let ap = aa[0];
for (let i = 2, i1 = 1; i <= nA; i1 = i, ++i) {
const ac = aa[i];
const c = pRow[i1] + (ac == bc ? 0 : 1);
const ct = ac == bp && ap == bc ? ppRow[i1 - 1] + 1 : c;
row[i] = Math.min(c, ct, pRow[i] + 1, row[i1] + 1);
ap = ac;
}
ppRow = pRow;
pRow = row;
}
return pRow[nA];
}
//#endregion
//#region src/lib/distance/distance.ts
const defaultCost = 100;
/**
* Calculate the edit distance between any two words.
* Use the Damerau–Levenshtein distance algorithm.
* @param wordA
* @param wordB
* @param editCost - the cost of each edit (defaults to 100)
* @returns the edit distance.
*/
function editDistance(wordA, wordB, editCost = defaultCost) {
return levenshteinDistance(wordA, wordB) * editCost;
}
/**
* Calculate the weighted edit distance between any two words.
* @param wordA
* @param wordB
* @param weights - the weights to use
* @param editCost - the cost of each edit (defaults to 100)
* @returns the edit distance
*/
function editDistanceWeighted(wordA, wordB, weights, editCost = defaultCost) {
return distanceAStarWeighted(wordA, wordB, weights, editCost);
}
/**
* Collect Map definitions into a single weighted map.
* @param defs - list of definitions
* @returns A Weighted Map to be used with distance calculations.
*/
function createWeightedMap(defs) {
return createWeightMap(...defs);
}
//#endregion
//#region src/lib/utils/timer.ts
function startTimer() {
const start = performance.now();
return () => performance.now() - start;
}
function createPerfTimer() {
const timer = startTimer();
const active = /* @__PURE__ */ new Map();
const events = [{
name: "start",
at: 0
}];
function updateEvent(event, atTime = timer()) {
const elapsed = atTime - event.at;
event.elapsed = (event.elapsed || 0) + elapsed;
return elapsed;
}
function start(name) {
const event = createEvent(name || "start");
events.push(event);
name && active.set(name, event);
return () => updateEvent(event);
}
function stop(name) {
const knownEvent = name && active.get(name);
if (knownEvent) return updateEvent(knownEvent);
return mark(name || "stop");
}
function createEvent(name) {
return {
name,
at: timer()
};
}
function mark(name) {
const event = createEvent(name);
events.push(event);
return event.at;
}
function formatReport() {
const lineElements = [
{
name: "Event Name",
at: "Time",
elapsed: "Elapsed"
},
{
name: "----------",
at: "----",
elapsed: "-------"
},
...mapEvents()
];
function mapEvents() {
const stack = [];
return events.map((e) => {
for (let s = stack.pop(); s; s = stack.pop()) if (s >= e.at + (e.elapsed || 0)) {
stack.push(s);
break;
}
const d = stack.length;
if (e.elapsed) stack.push(e.at + e.elapsed);
return {
name: "| ".repeat(d) + (e.name || "").replaceAll(" ", " "),
at: `${t(e.at)}`,
elapsed: e.elapsed ? `${t(e.elapsed)}` : "--"
};
});
}
function t(ms) {
return ms.toFixed(3) + "ms";
}
function m(v, s) {
return Math.max(v, s.length);
}
const lengths = lineElements.reduce((a, b) => ({
name: m(a.name, b.name),
at: m(a.at, b.at),
elapsed: m(a.elapsed, b.elapsed)
}), {
name: 0,
at: 0,
elapsed: 0
});
const lines = lineElements.map((e) => `${e.at.padStart(lengths.at)} ${e.name.padEnd(lengths.name)} ${e.elapsed.padStart(lengths.elapsed)}`);
return lines.join("\n");
}
function measureFn(name, fn) {
const s = start(name);
const v = fn();
s();
return v;
}
async function measureAsyncFn(name, fn) {
const s = start(name);
const v = await fn();
s();
return v;
}
function report(reporter = console.log) {
reporter(formatReport());
}
return {
start,
stop,
mark,
elapsed: timer,
report,
formatReport,
measureFn,
measureAsyncFn
};
}
let globalPerfTimer = void 0;
function getGlobalPerfTimer() {
const timer = globalPerfTimer || createPerfTimer();
globalPerfTimer = timer;
return timer;
}
//#endregion
//#region src/lib/utils/util.ts
function isDefined$1(a) {
return a !== void 0;
}
/**
* Remove any fields with an `undefined` value.
* @param t - object to clean
* @returns t
*/
function cleanCopy(t) {
const r = { ...t };
return clean$1(r);
}
/**
* Remove any fields with an `undefined` value.
* **MODIFIES THE OBJECT**
* @param t - object to clean
* @returns t
*/
function clean$1(t) {
for (const prop in t) if (t[prop] === void 0) delete t[prop];
return t;
}
function unique(a) {
return [...new Set(a)];
}
/**
*
* @param text verbatim text to be inserted into a regexp
* @returns text that can be used in a regexp.
*/
function regexQuote(text) {
return text.replaceAll(/([[\]\-+(){},|*.\\])/g, "\\$1");
}
/**
* Factory to create a function that will replace all occurrences of `match` with `withText`
* @param match - string to match
* @param replaceWithText - the text to substitute.
*/
function replaceAllFactory(match, replaceWithText) {
const r = RegExp(regexQuote(match), "g");
return (text) => text.replace(r, replaceWithText);
}
//#endregion
//#region src/lib/suggestions/suggestCollector.ts
const defaultMaxNumberSuggestions = 10;
const BASE_COST = 100;
const MAX_NUM_CHANGES = 5;
const MAX_COST_SCALE = .5;
const MAX_ALLOWED_COST_SCALE = 1.03 * MAX_COST_SCALE;
const collator = new Intl.Collator();
const regexSeparator = new RegExp(`[${regexQuote(WORD_SEPARATOR)}]`, "g");
const wordLengthCost = [
0,
50,
25,
5,
0
];
const EXTRA_WORD_COST = 5;
/** time in ms */
const DEFAULT_COLLECTOR_TIMEOUT = 1e3;
const symStopProcessing = Symbol("Collector Stop Processing");
function compSuggestionResults(a, b) {
const aPref = a.isPreferred && -1 || 0;
const bPref = b.isPreferred && -1 || 0;
return aPref - bPref || a.cost - b.cost || a.word.length - b.word.length || collator.compare(a.word, b.word);
}
const defaultSuggestionCollectorOptions = Object.freeze({
numSuggestions: defaultMaxNumberSuggestions,
filter: () => true,
changeLimit: MAX_NUM_CHANGES,
includeTies: false,
ignoreCase: true,
timeout: DEFAULT_COLLECTOR_TIMEOUT,
weightMap: void 0,
compoundSeparator: "",
compoundMethod: void 0
});
function suggestionCollector(wordToMatch, options) {
const { filter = () => true, changeLimit = MAX_NUM_CHANGES, includeTies = false, ignoreCase = true, timeout = DEFAULT_COLLECTOR_TIMEOUT, weightMap, compoundSeparator = defaultSuggestionCollectorOptions.compoundSeparator } = options;
const numSuggestions = Math.max(options.numSuggestions, 0) || 0;
const numSugToHold = weightMap ? numSuggestions * 2 : numSuggestions;
const sugs = /* @__PURE__ */ new Map();
let maxCost = BASE_COST * Math.min(wordToMatch.length * MAX_ALLOWED_COST_SCALE, changeLimit);
const useSeparator = compoundSeparator || (weightMap ? DEFAULT_COMPOUNDED_WORD_SEPARATOR : defaultSuggestionCollectorOptions.compoundSeparator);
const fnCleanWord = !useSeparator || useSeparator === compoundSeparator ? (w) => w : replaceAllFactory(useSeparator, "");
if (useSeparator && weightMap) addDefToWeightMap(weightMap, {
map: useSeparator,
insDel: 50
});
const genSuggestionOptions = clean$1({
changeLimit,
ignoreCase,
compoundMethod: options.compoundMethod,
compoundSeparator: useSeparator
});
let timeRemaining = timeout;
function dropMax() {
if (sugs.size < 2 || !numSuggestions) {
sugs.clear();
return;
}
const sorted = [...sugs.values()].sort(compSuggestionResults);
let i = numSugToHold - 1;
maxCost = sorted[i].cost;
for (; i < sorted.length && sorted[i].cost <= maxCost; ++i);
for (; i < sorted.length; ++i) sugs.delete(sorted[i].word);
}
function adjustCost(sug) {
if (sug.isPreferred) return sug;
const words = sug.word.split(regexSeparator);
const extraCost = words.map((w) => wordLengthCost[w.length] || 0).reduce((a, b) => a + b, 0) + (words.length - 1) * EXTRA_WORD_COST;
return {
word: sug.word,
cost: sug.cost + extraCost
};
}
function collectSuggestion(suggestion) {
const { word, cost, isPreferred } = adjustCost(suggestion);
if (cost <= maxCost && filter(suggestion.word, cost)) {
const known = sugs.get(word);
if (known) {
known.cost = Math.min(known.cost, cost);
known.isPreferred = known.isPreferred || isPreferred;
} else {
sugs.set(word, {
word,
cost,
isPreferred
});
if (cost < maxCost && sugs.size > numSugToHold) dropMax();
}
}
return maxCost;
}
/**
* Collection suggestions from a SuggestionIterator
* @param src - the SuggestionIterator used to generate suggestions.
* @param timeout - the amount of time in milliseconds to allow for suggestions.
*/
function collect(src, timeout$1, filter$1) {
let stop = false;
timeout$1 = timeout$1 ?? timeRemaining;
timeout$1 = Math.min(timeout$1, timeRemaining);
if (timeout$1 < 0) return;
const timer = startTimer();
let ir;
while (!(ir = src.next(stop || maxCost)).done) {
if (timer() > timeout$1) stop = symStopProcessing;
const { value } = ir;
if (!value) continue;
if (isSuggestionResult(value)) {
if (!filter$1 || filter$1(value.word, value.cost)) collectSuggestion(value);
continue;
}
}
timeRemaining -= timer();
}
function cleanCompoundResult(sr) {
const { word, cost } = sr;
const cWord = fnCleanWord(word);
if (cWord !== word) return {
word: cWord,
cost,
compoundWord: word,
isPreferred: void 0
};
return { ...sr };
}
function suggestions() {
if (numSuggestions < 1 || !sugs.size) return [];
const NF = "NFD";
const nWordToMatch = wordToMatch.normalize(NF);
const rawValues = [...sugs.values()];
const values = weightMap ? rawValues.map(({ word, cost, isPreferred }) => ({
word,
cost: isPreferred ? cost : editDistanceWeighted(nWordToMatch, word.normalize(NF), weightMap, 110),
isPreferred
})) : rawValues;
const sorted = values.sort(compSuggestionResults).map(cleanCompoundResult);
let i = Math.min(sorted.length, numSuggestions) - 1;
const limit = includeTies ? sorted.length : Math.min(sorted.length, numSuggestions);
const iCost = sorted[i].cost;
const maxCost$1 = Math.min(iCost, weightMap ? changeLimit * BASE_COST - 1 : iCost);
for (i = 1; i < limit && sorted[i].cost <= maxCost$1; ++i);
sorted.length = i;
return sorted;
}
const collector = {
collect,
add: function(suggestion) {
collectSuggestion(suggestion);
return this;
},
get suggestions() {
return suggestions();
},
get maxCost() {
return maxCost;
},
get word() {
return wordToMatch;
},
get maxNumSuggestions() {
return numSuggestions;
},
get changeLimit() {
return changeLimit;
},
includesTies: includeTies,
ignoreCase,
symbolStopProcessing: symStopProcessing,
genSuggestionOptions
};
return collector;
}
/**
* Impersonating a Collector, allows searching for multiple variants on the same word.
* The collection is still in the original collector.
* @param collector - collector to impersonate
* @param word - word to present instead of `collector.word`.
* @returns a SuggestionCollector
*/
function impersonateCollector(collector, word) {
const r = Object.create(collector);
Object.defineProperty(r, "word", {
value: word,
writable: false
});
return r;
}
function isSuggestionResult(s) {
const r = s;
return !!r && typeof r === "object" && r?.cost !== void 0 && r.word != void 0;
}
//#endregion
//#region src/lib/suggestions/suggestAStar.ts
/**
* Compare Path Nodes.
* Balance the calculation between depth vs cost
*/
function comparePath(a, b) {
return a.c / (a.i + 1) - b.c / (b.i + 1) + (b.i - a.i);
}
function suggestAStar(trie, word, options = {}) {
const opts = createSuggestionOptions(options);
const collector = suggestionCollector(word, opts);
collector.collect(getSuggestionsAStar(trie, word, opts));
return collector.suggestions;
}
function* getSuggestionsAStar(trie, srcWord, options = {}) {
const { compoundMethod, changeLimit, ignoreCase, weightMap } = createSuggestionOptions(options);
const visMap = visualLetterMaskMap;
const root = trie.getRoot();
const rootIgnoreCase = ignoreCase && root.get(root.info.stripCaseAndAccentsPrefix) || void 0;
const pathHeap = new PairingHeap(comparePath);
const resultHeap = new PairingHeap(compareSuggestion);
const rootPNode = {
n: root,
i: 0,
c: 0,
s: "",
p: void 0,
t: createCostTrie()
};
const BC = opCosts.baseCost;
const VC = opCosts.visuallySimilar;
const DL = opCosts.duplicateLetterCost;
const wordSeparator = compoundMethod === CompoundWordsMethod.JOIN_WORDS ? JOIN_SEPARATOR : WORD_SEPARATOR;
const sc = specialChars(trie.info);
const comp = trie.info.compoundCharacter;
const compRoot = root.get(comp);
const compRootIgnoreCase = rootIgnoreCase && rootIgnoreCase.get(comp);
const emitted = Object.create(null);
const srcLetters = [...srcWord];
/** Initial limit is based upon the length of the word. */
let limit = BC * Math.min(srcLetters.length * opCosts.wordLengthCostFactor, changeLimit);
pathHeap.add(rootPNode);
if (rootIgnoreCase) pathHeap.add({
n: rootIgnoreCase,
i: 0,
c: 0,
s: "",
p: void 0,
t: createCostTrie()
});
let best = pathHeap.dequeue();
let maxSize = pathHeap.size;
let suggestionsGenerated = 0;
let nodesProcessed = 0;
let nodesProcessedLimit = 1e3;
let minGen = 1;
while (best) {
if (++nodesProcessed > nodesProcessedLimit) {
nodesProcessedLimit += 1e3;
if (s