UNPKG

cspell-trie-lib

Version:
1,966 lines (1,945 loc) 231 kB
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