UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

891 lines (890 loc) 34.9 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnnotationCategory = void 0; /** * ARCHITECTURE: ContentIndex — Trie-Based Search Index * * ContentIndex provides a compressed trie data structure for fast term lookup * across project content. It is the shared search engine used by: * * 1. **Omni Search (Ctrl+E)** — Bottom status bar in the editor. Uses * `getDescendentStrings()` for autocomplete term suggestions and * `getMatches()` for finding matched project item paths. * * 2. **Search in Files (Ctrl+Shift+F)** — ProjectSearchDialog uses the * index to prioritize candidate files before doing brute-force text scan. * * 3. **Quick Open (Ctrl+P)** — QuickOpenDialog uses simple path includes() * but could be enhanced to use this index in the future. * * **Data Structure:** * - `items[]` — Array of indexed values (file paths like `/bp/entities/pig.json`) * - `trie{}` — Compressed trie mapping search terms → arrays of item indices * - Terminal nodes use `±` or `$` markers * - Annotated entries use `IAnnotatedIndexData { n: number, a: string }` * * **Index Population (ProjectInfoSet.ts):** * - File base names (e.g., "pig" from "pig.json") * - Storage relative paths (e.g., "entities/pig.json") * - Parsed JSON content tokens (words > 2 chars) * - Parsed JS/TS tokens via esprima * - Entity/block/item type IDs from info generators (with annotation categories) * * **Key Methods:** * - `getMatches(searchString)` — Multi-term AND search. Splits on spaces, * intersects results across terms. Each term is matched via both trie * traversal and linear substring scan of items[]. * - `getDescendentStrings(term)` — Returns all trie entries starting with * the given prefix (for autocomplete dropdown). * - `getTermMatch(term)` — Single-term lookup: trie traversal + linear * substring scan of items[] for path matching. * - `insert(key, item, annotation?)` — Inserts a term into the trie. * * **Configuration:** * - `startLength` — Minimum input length before autocomplete triggers (currently 3). * - JSON token threshold — Tokens > 2 chars are indexed from parsed content. * * Last updated: February 2026 */ const ProjectUtilities_1 = __importDefault(require("../app/ProjectUtilities")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const Log_1 = __importDefault(require("./Log")); const Utilities_1 = __importDefault(require("./Utilities")); const esprima_next_1 = __importDefault(require("esprima-next")); var AnnotationCategory; (function (AnnotationCategory) { AnnotationCategory["blockTextureReferenceSource"] = "a"; AnnotationCategory["blockTypeDependent"] = "b"; AnnotationCategory["entityComponentDependent"] = "c"; AnnotationCategory["blockComponentDependent"] = "d"; AnnotationCategory["entityTypeDependent"] = "e"; AnnotationCategory["entityFilter"] = "f"; AnnotationCategory["entityComponentDependentInGroup"] = "g"; AnnotationCategory["blockTextureReferenceDependent"] = "h"; AnnotationCategory["itemTypeDependent"] = "i"; AnnotationCategory["itemComponentDependent"] = "j"; AnnotationCategory["itemTextureReferenceSource"] = "k"; AnnotationCategory["featureSource"] = "l"; AnnotationCategory["featureDependent"] = "m"; AnnotationCategory["featureRuleSource"] = "n"; AnnotationCategory["blockComponentDependentInPermutation"] = "p"; AnnotationCategory["storagePathDependent"] = "s"; AnnotationCategory["textureFile"] = "t"; AnnotationCategory["entityEvent"] = "v"; AnnotationCategory["blockTypeSource"] = "B"; AnnotationCategory["entityTypeSource"] = "E"; AnnotationCategory["itemTypeSource"] = "I"; AnnotationCategory["itemTextureSource"] = "J"; AnnotationCategory["blockSounds"] = "L"; AnnotationCategory["musicDefinitionSource"] = "M"; AnnotationCategory["entitySounds"] = "N"; AnnotationCategory["interactiveSounds"] = "R"; AnnotationCategory["jsSource"] = "S"; AnnotationCategory["terrainTextureSource"] = "T"; AnnotationCategory["soundDefinitionSource"] = "U"; AnnotationCategory["individualEventSoundsSource"] = "V"; AnnotationCategory["worldProperty"] = "W"; AnnotationCategory["experiment"] = "X"; // Cross-reference completion annotation categories AnnotationCategory["geometrySource"] = "G"; AnnotationCategory["animationSource"] = "A"; AnnotationCategory["animationControllerSource"] = "C"; AnnotationCategory["renderControllerSource"] = "D"; AnnotationCategory["particleSource"] = "P"; AnnotationCategory["fogSource"] = "F"; AnnotationCategory["lootTableSource"] = "O"; AnnotationCategory["recipeSource"] = "Q"; AnnotationCategory["biomeSource"] = "Y"; AnnotationCategory["spawnRuleSource"] = "Z"; AnnotationCategory["structureSource"] = "r"; AnnotationCategory["dialogueSource"] = "q"; AnnotationCategory["functionSource"] = "u"; AnnotationCategory["soundEventSource"] = "w"; // Component-level annotation categories for biomes and particles AnnotationCategory["biomeBehaviorComponentDependent"] = "x"; AnnotationCategory["biomeClientComponentDependent"] = "y"; AnnotationCategory["particleEmitterComponentDependent"] = "z"; AnnotationCategory["particleComponentDependent"] = "pc"; })(AnnotationCategory || (exports.AnnotationCategory = AnnotationCategory = {})); class ContentIndex { _processedPathsCache; _hashCatalog = {}; // O(1) lookup map: item string → index in items[]. Replaces O(n) linear scans // in insert() and getTermMatch(). Rebuilt on setItems(). _itemIndexMap = new Map(); #data = { items: [], trie: {}, }; get hashCatalog() { return this._hashCatalog; } #iteration = Math.floor(Math.random() * 1000000); get iteration() { return this.#iteration; } set iteration(newIteration) { this.#iteration = newIteration; } static getAnnotationCategoryKeys() { const keys = []; for (const key in AnnotationCategory) { keys.push(key.toLowerCase()); } return keys; } static getAnnotationCategoryFromLongString(longStr) { longStr = longStr.toLowerCase(); for (const key in AnnotationCategory) { if (key.toLowerCase() === longStr) { return AnnotationCategory[key]; } } return undefined; } get data() { return this.#data; } get startLength() { return 3; } get items() { return this.#data.items; } setItems(items) { this.#data.items = items; this._processedPathsCache = undefined; // reset processed paths cache // Rebuild the O(1) lookup map from the new items array this._itemIndexMap.clear(); for (let i = 0; i < items.length; i++) { this._itemIndexMap.set(items[i], i); } } setTrie(trie) { this.#data.trie = trie; } getAll(withAnnotation) { const results = {}; this._appendToResults("", this.#data.trie, results, withAnnotation); return results; } _appendToResults(prefix, node, results, withAnnotation) { for (const token in node) { const subNode = node[token]; if (subNode) { if (token === "±" || token === "$") { const arr = subNode; if (Array.isArray(arr)) { if (Utilities_1.default.isUsableAsObjectKey(prefix)) { let res = this.getValuesFromIndexArray(arr, withAnnotation); if (res) { results[prefix] = res; } } } } else if (Array.isArray(subNode)) { if (Utilities_1.default.isUsableAsObjectKey(prefix + token)) { let res = this.getValuesFromIndexArray(subNode, withAnnotation); if (res) { results[prefix + token] = res; } } } else { this._appendToResults(prefix + token, subNode, results, withAnnotation); } } } } mergeFrom(index, newItem) { const all = index.getAll(); for (const fullKey in all) { const annVals = all[fullKey]; let annVal; for (const subVal of annVals) { if (subVal.annotation) { if (!annVal) { annVal = subVal.annotation; } else if (annVal.indexOf(subVal.annotation) < 0) { annVal += subVal.annotation; } } } this.insert(fullKey, newItem, annVal); } } static processResultValues(annotatedValues, withAnyAnnotation) { if (!annotatedValues) { return undefined; } if (withAnyAnnotation) { let newAnnotatedValues = []; for (const annV of annotatedValues) { if (annV.annotation && withAnyAnnotation.includes(annV.annotation)) { newAnnotatedValues.push(annV); } } annotatedValues = newAnnotatedValues; } return annotatedValues; } getValuesFromIndexArray(indices, withAnnotation) { let results = []; if (!indices) { return undefined; } if (Utilities_1.default.arrayHasNegativeAndIsNumeric(indices)) { indices = Utilities_1.default.decodeSequentialRunLengthUsingNegative(indices); } for (const index of indices) { if (typeof index === "object") { const indexN = index.n; if (indexN >= 0 && indexN < this.#data.items.length) { const annotate = index.a; if (!withAnnotation || withAnnotation.includes(annotate)) { results.push({ value: this.#data.items[indexN], annotation: index.a }); } } } else if (index >= 0 && index < this.#data.items.length && !withAnnotation) { results.push({ value: this.#data.items[index], annotation: undefined }); } } if (results.length === 0) { return undefined; } return results; } loadFromData(data) { this.#data = data; this._processedPathsCache = undefined; // reset processed paths cache } _getProcessedPaths() { if (this._processedPathsCache) return this._processedPathsCache; this._processedPathsCache = this.data.items .filter((item) => item.startsWith("/")) .map((item) => { const lastPeriod = item.lastIndexOf("."); return lastPeriod >= 0 ? item.substring(0, lastPeriod) : item; }); return this._processedPathsCache; } hasPathMatches(pathEnd) { pathEnd = pathEnd.toLowerCase(); pathEnd = StorageUtilities_1.default.stripExtension(pathEnd); return this._getProcessedPaths().some((path) => path.endsWith(pathEnd)); } getPathMatches(pathEnd) { pathEnd = pathEnd.toLowerCase(); let pathType = ProjectUtilities_1.default.inferJsonProjectItemTypeFromExtension(pathEnd); pathEnd = StorageUtilities_1.default.stripExtension(pathEnd); const results = []; for (const candPath of this.data.items) { const candType = ProjectUtilities_1.default.inferJsonProjectItemTypeFromExtension(candPath); const candPathStripped = StorageUtilities_1.default.stripExtension(candPath.toLowerCase()); if (candPathStripped.endsWith(pathEnd) && pathType === candType) { results.push(StorageUtilities_1.default.stripExtension(candPath)); } } return results; } async getMatches(searchString, wholeTermSearch, withAnyAnnotation) { if (typeof searchString !== "string") { Log_1.default.unexpectedContentState("CIMGMS"); return undefined; } searchString = searchString.trim().toLowerCase(); let terms = [searchString]; if (!wholeTermSearch) { terms = searchString.split(" "); } let termWasSearched = false; let andResults; for (const term of terms) { if (term.length > 1) { const results = this.getTermMatch(term); termWasSearched = true; if (results && results.length) { if (andResults === undefined) { andResults = results; } else { const newArr = []; for (let num of results) { if (andResults.includes(num)) { newArr.push(num); } } andResults = newArr; } } } } if (andResults === undefined || andResults.length === 0) { if (termWasSearched) { return []; } return undefined; } let annotatedValues = ContentIndex.processResultValues(this.getValuesFromIndexArray(andResults), withAnyAnnotation); if (!annotatedValues) { return undefined; } return annotatedValues.sort((a, b) => { let aTermMatches = 0; let bTermMatches = 0; const aVal = a.value.toLowerCase(); const bVal = b.value.toLowerCase(); for (const term of terms) { if (aVal.startsWith(term)) { aTermMatches += 5; } else if (aVal.includes(term)) { aTermMatches++; } if (bVal.startsWith(term)) { bTermMatches += 5; } else if (bVal.includes(term)) { bTermMatches++; } } if (aTermMatches === bTermMatches) { return Utilities_1.default.staticCompare(a.value, b.value); } return bTermMatches - aTermMatches; }); } getTermMatchStrings(term) { const results = this.getTermMatch(term); if (results === undefined) { return results; } return this.getValuesFromIndexArray(results); } async getDescendentStrings(term) { let termIndex = 0; let curNode = this.#data.trie; const results = {}; let hasAdvanced = true; let termSubstr = ""; while (termIndex < term.length && hasAdvanced) { hasAdvanced = false; if (Array.isArray(curNode)) { return undefined; } let nextNode = curNode[term[termIndex]]; if (nextNode) { curNode = nextNode; termIndex++; termSubstr = term.substring(0, termIndex); hasAdvanced = true; } else { let nextStart = term[termIndex]; for (const item in curNode) { // we've found part of our string in this node if (item.startsWith(nextStart) && curNode[item] !== undefined) { let itemIndex = 0; hasAdvanced = true; curNode = curNode[item]; termSubstr = term.substring(0, termIndex) + item; while (termIndex < term.length && itemIndex < item.length && item[itemIndex] === term[termIndex]) { itemIndex++; termIndex++; } break; } } } } if (termIndex < term.length) { const termStub = term.substring(termIndex); for (const childNodeName in curNode) { if (childNodeName.startsWith(termStub) && curNode[childNodeName]) { this._appendToResults(term.substring(0, termIndex) + childNodeName, curNode[childNodeName], results); } } } else { if (Array.isArray(curNode)) { results[termSubstr] = this.getValuesFromIndexArray(curNode); } else if (curNode["±"] !== undefined) { this._appendToResults(termSubstr, curNode, results); } } return results; } getTermMatch(term) { let termIndex = 0; let curNode = this.#data.trie; let hasAdvanced = true; let pathMatches = undefined; let i = 0; for (const item of this.#data.items) { if (item.indexOf(term) >= 0) { if (!pathMatches) { pathMatches = []; } pathMatches.push(i); } i++; } while (termIndex < term.length && hasAdvanced) { hasAdvanced = false; if (Array.isArray(curNode)) { return undefined; } let nextNode = curNode[term[termIndex]]; if (nextNode) { curNode = nextNode; termIndex++; hasAdvanced = true; } else { let nextStart = term[termIndex]; if (termIndex < term.length - 1) { nextStart += term[termIndex + 1]; } for (const item in curNode) { // we've found part of our string in this node if (item.startsWith(nextStart) && curNode[item] !== undefined && !hasAdvanced) { let itemIndex = 0; hasAdvanced = true; curNode = curNode[item]; while (termIndex < term.length && itemIndex < item.length && item[itemIndex] === term[termIndex]) { itemIndex++; termIndex++; } } } } } if (termIndex < term.length) { return undefined; } if (Array.isArray(curNode)) { if (pathMatches) { return ContentIndex.mergeResults(curNode, pathMatches); } return curNode; } else if (curNode["±"] !== undefined) { if (pathMatches) { return ContentIndex.mergeResults(curNode["±"], pathMatches); } return curNode["±"]; } else { const arr = []; this.aggregateIndices(curNode, arr); if (pathMatches) { return ContentIndex.mergeResults(arr, pathMatches); } return arr; } } static mergeResults(resultsArrA, resultsArrB) { const results = []; const seenNumbers = new Set(); const seenObjects = new Set(); // "value|annotation" composite key for (const item of resultsArrA) { if (typeof item === "object") { const key = `${item.value}|${item.annotation}`; if (!seenObjects.has(key)) { seenObjects.add(key); results.push(item); } } else { if (!seenNumbers.has(item)) { seenNumbers.add(item); results.push(item); } } } for (const item of resultsArrB) { if (typeof item === "object") { const key = `${item.value}|${item.annotation}`; if (!seenObjects.has(key)) { seenObjects.add(key); results.push(item); } } else { if (!seenNumbers.has(item)) { seenNumbers.add(item); results.push(item); } } } return results; } aggregateIndices(curNode, arr, seen) { if (!seen) { seen = new Set(arr); } for (const childNodeName in curNode) { const childNode = curNode[childNodeName]; if (childNode) { if (Array.isArray(childNode)) { for (const num of childNode) { const n = typeof num === "object" ? num.n : num; if (!seen.has(n)) { seen.add(n); arr.push(num); } } } else if (childNode["±"] !== undefined) { for (const num of childNode["±"]) { const n = typeof num === "object" ? num.n : num; if (!seen.has(n)) { seen.add(n); arr.push(num); } } } else { this.aggregateIndices(childNode, arr, seen); } } } } insertArray(key, items) { for (const item of items) { this.insert(key, item.value, item.annotation); } } insert(key, item, annotationChar) { if (Utilities_1.default.isNumericIsh(key) || key.length > 70) { return; } // since we treat ± as special, ban usage of ± in strings. key = key.replace(/±/gi, "").toLowerCase().trim(); let keyIndex = 0; let curNode = this.#data.trie; let parentNode = curNode; let curNodeIndex; let dataIndex = -1; // O(1) item lookup via Map (replaces O(n) linear scan) const existingIndex = this._itemIndexMap.get(item); if (existingIndex !== undefined) { dataIndex = existingIndex; } if (dataIndex < 0) { dataIndex = this.#data.items.length; this.#data.items.push(item); this._itemIndexMap.set(item, dataIndex); this._processedPathsCache = undefined; } let hasAdvanced = true; while (keyIndex < key.length && hasAdvanced) { hasAdvanced = false; if (!Array.isArray(curNode)) { for (const item in curNode) { // we've found part of our string in this node if (item.startsWith(key[keyIndex]) && curNode[item] !== undefined) { // && curNode[item].constructor !== Array) { let itemIndex = 0; hasAdvanced = true; curNodeIndex = item; parentNode = curNode; curNode = curNode[item]; while (keyIndex < key.length && itemIndex < item.length && item[itemIndex] === key[keyIndex]) { itemIndex++; keyIndex++; } // if we're in the middle of a string like "subset", and we're trying add the word "subpar", // create a new node called "sub" and place "set" underneath it. // also support the case where we're adding "sub" but "subset" already exists (keyIndex === key.length) if (item[itemIndex] !== key[keyIndex] && itemIndex < item.length && keyIndex <= key.length) { parentNode[curNodeIndex] = undefined; curNodeIndex = item.substring(0, itemIndex); let newNode = {}; parentNode[curNodeIndex] = newNode; const term = item.substring(itemIndex); if (Utilities_1.default.isUsableAsObjectKey(term)) { newNode[term] = curNode; } curNode = newNode; } break; } } } } // we've reached the end of the trie; we need to add a new node if (keyIndex < key.length) { // if parent node was a leaf array, switch to an object if (Array.isArray(curNode) && curNodeIndex) { parentNode[curNodeIndex] = {}; parentNode[curNodeIndex]["±"] = curNode; curNode = parentNode[curNodeIndex]; } const substr = key.substring(keyIndex); if (substr !== "±") { if (Utilities_1.default.isUsableAsObjectKey(substr)) { // create a new leaf array curNode[substr] = this.ensureAnnotatedContentInArray([], dataIndex, annotationChar); } } } else { if (Array.isArray(curNode) && curNodeIndex) { if (Utilities_1.default.isUsableAsObjectKey(curNodeIndex)) { parentNode[curNodeIndex] = this.ensureAnnotatedContentInArray(curNode, dataIndex, annotationChar); } } else { if (curNode["±"] === undefined) { curNode["±"] = []; } curNode["±"] = this.ensureAnnotatedContentInArray(curNode["±"], dataIndex, annotationChar); } } } ensureAnnotatedContentInArray(arr, dataIndex, annotationChar) { try { for (const item of arr) { if (typeof item === "object") { if (item.n === dataIndex) { if (annotationChar) { if (!item.a) { item.a = annotationChar; } else { if (item.a.indexOf(annotationChar) < 0) { item.a += annotationChar; } } } return arr; } } else if (item === dataIndex) { if (!annotationChar) { return arr; } // convert simple number to annotated object const newArr = []; for (const existItem of arr) { if (existItem !== dataIndex) { newArr.push(existItem); } } newArr.push({ n: dataIndex, a: annotationChar }); return newArr; } } if (annotationChar) { arr.push({ n: dataIndex, a: annotationChar }); } else { arr.push(dataIndex); } } catch (e) { Log_1.default.verbose("Error ensuring annotated content: " + e + "|" + arr + "|" + JSON.stringify(arr)); } return arr; } parseJsContent(sourcePath, content) { try { const results = esprima_next_1.default.tokenize(content); if (results) { for (const token of results) { if (token.type === "Identifier" && token.value && token.value.length > 3) { if (token.value !== "from") { this.insert(token.value.toLowerCase(), sourcePath, "S"); } } } } } catch (e) { Log_1.default.debugAlert("JS parsing error:" + e); } } parseTextContent(sourcePath, content) { const dictionaryOfTerms = {}; let curWord = ""; content = content.toLowerCase(); for (let i = 0; i < content.length; i++) { const curChar = content[i]; if (curChar === "{" || curChar === "}" || curChar === " " || curChar === "\r" || curChar === "\n" || curChar === "\t" || curChar === "(" || curChar === ")" || curChar === "[" || curChar === "]" || curChar === ":" || curChar === '"' || curChar === "'") { if (curWord.length > 0) { if (curWord.length > 3 && !Utilities_1.default.isNumericIsh(curWord) && Utilities_1.default.isUsableAsObjectKey(curWord)) { dictionaryOfTerms[curWord] = true; } curWord = ""; } } else { curWord += content[i]; } } for (const term in dictionaryOfTerms) { this.insert(term, sourcePath); } } parseJsonContent(sourcePath, content) { const dictionaryOfTerms = {}; let curWord = ""; content = content.toLowerCase(); for (let i = 0; i < content.length; i++) { const curChar = content[i]; if (curChar === "{" || curChar === "}" || curChar === " " || curChar === "\r" || curChar === "\n" || curChar === "\t" || curChar === "(" || curChar === ")" || curChar === "[" || curChar === "]" || curChar === ":" || curChar === '"' || curChar === "'") { if (curWord.length > 0) { if (curWord.length > 3 && !Utilities_1.default.isNumericIsh(curWord) && Utilities_1.default.isUsableAsObjectKey(curWord)) { dictionaryOfTerms[curWord] = true; } curWord = ""; } } else { curWord += content[i]; } } for (const term in dictionaryOfTerms) { this.insert(term, sourcePath); } } /** * Extracts indexable tokens from a parsed JSON object by recursively walking keys and string values. * Much faster than parseJsonContent which does character-by-character text tokenization. * Only processes keys and string values (the same tokens that parseJsonContent would find). */ indexJsonObject(sourcePath, data) { const terms = new Set(); const depthExceeded = ContentIndex._collectJsonTerms(data, terms, 0); if (depthExceeded) { Log_1.default.debug("ContentIndex: JSON nesting depth exceeded " + ContentIndex.MAX_JSON_DEPTH + " in '" + sourcePath + "'; skipping deeper levels."); } for (const term of terms) { this.insert(term, sourcePath); } } /** * Recursively collects indexable terms (keys and string values) from a parsed JSON object. */ /** Max nesting depth for JSON term collection. Most Minecraft JSON stays under 10 levels, but features can reach ~25. */ static MAX_JSON_DEPTH = 30; static _collectJsonTerms(obj, terms, depth) { if (obj === null || obj === undefined) { return false; } if (depth > ContentIndex.MAX_JSON_DEPTH) { return true; } if (typeof obj === "string") { ContentIndex._addStringTerms(obj, terms); return false; } // Numbers, booleans, and other primitives are not indexable terms — skip them. if (typeof obj !== "object") { return false; } let exceeded = false; if (Array.isArray(obj)) { for (const item of obj) { if (ContentIndex._collectJsonTerms(item, terms, depth + 1)) { exceeded = true; } } return exceeded; } for (const key in obj) { // Index the key itself const lowerKey = key.toLowerCase(); if (lowerKey.length > 3 && !Utilities_1.default.isNumericIsh(lowerKey) && Utilities_1.default.isUsableAsObjectKey(lowerKey)) { terms.add(lowerKey); } // Recurse into values const val = obj[key]; if (val !== null && val !== undefined) { if (ContentIndex._collectJsonTerms(val, terms, depth + 1)) { exceeded = true; } } } return exceeded; } /** * Splits a string on common delimiters and adds qualifying terms. * Matches the same tokenization logic as parseJsonContent's character loop. */ /** Characters that act as word boundaries when tokenizing strings for indexing. */ static TERM_DELIMITERS = new Set([ "{", "}", " ", "\r", "\n", "\t", "(", ")", "[", "]", ":", '"', "'", ]); static _addStringTerms(str, terms) { const lower = str.toLowerCase(); let curWord = ""; for (let i = 0; i < lower.length; i++) { const c = lower[i]; if (ContentIndex.TERM_DELIMITERS.has(c)) { if (curWord.length > 3 && !Utilities_1.default.isNumericIsh(curWord) && Utilities_1.default.isUsableAsObjectKey(curWord)) { terms.add(curWord); } curWord = ""; } else { curWord += c; } } if (curWord.length > 3 && !Utilities_1.default.isNumericIsh(curWord) && Utilities_1.default.isUsableAsObjectKey(curWord)) { terms.add(curWord); } } } exports.default = ContentIndex;