@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
891 lines (890 loc) • 34.9 kB
JavaScript
"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;