Files
Foundry-VTT-Docker/resources/app/common/utils/string-tree.mjs

133 lines
4.5 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
/**
* A string tree node consists of zero-or-more string keys, and a leaves property that contains any objects that
* terminate at the current node.
* @typedef {object} StringTreeNode
*/
/**
* @callback StringTreeEntryFilter
* @param {any} entry The entry to filter.
* @returns {boolean} Whether the entry should be included in the result set.
*/
/**
* A data structure representing a tree of string nodes with arbitrary object leaves.
*/
export default class StringTree {
/**
* The key symbol that stores the leaves of any given node.
* @type {symbol}
*/
static get leaves() {
return StringTree.#leaves;
}
static #leaves = Symbol();
/* -------------------------------------------- */
/**
* The tree's root.
* @type {StringTreeNode}
*/
#root = this.#createNode();
/* -------------------------------------------- */
/**
* Create a new node.
* @returns {StringTreeNode}
*/
#createNode() {
return { [StringTree.leaves]: [] };
}
/* -------------------------------------------- */
/**
* Insert an entry into the tree.
* @param {string[]} strings The string parents for the entry.
* @param {any} entry The entry to store.
* @returns {StringTreeNode} The node the entry was added to.
*/
addLeaf(strings, entry) {
let node = this.#root;
for ( const string of strings ) {
node[string] ??= this.#createNode();
node = node[string];
}
// Once we've traversed the tree, we add our entry.
node[StringTree.leaves].push(entry);
return node;
}
/* -------------------------------------------- */
/**
* Traverse the tree along the given string path and return any entries reachable from the node.
* @param {string[]} strings The string path to the desired node.
* @param {object} [options]
* @param {number} [options.limit] The maximum number of items to retrieve.
* @param {StringTreeEntryFilter} [options.filterEntries] A filter function to apply to each candidate entry.
* @returns {any[]}
*/
lookup(strings, { limit, filterEntries }={}) {
const entries = [];
const node = this.nodeAtPrefix(strings);
if ( !node ) return []; // No matching entries.
const queue = [node];
while ( queue.length ) {
if ( limit && (entries.length >= limit) ) break;
this._breadthFirstSearch(queue.shift(), entries, queue, { limit, filterEntries });
}
return entries;
}
/* -------------------------------------------- */
/**
* Returns the node at the given path through the tree.
* @param {string[]} strings The string path to the desired node.
* @param {object} [options]
* @param {boolean} [options.hasLeaves=false] Only return the most recently visited node that has leaves, otherwise
* return the exact node at the prefix, if it exists.
* @returns {StringTreeNode|void}
*/
nodeAtPrefix(strings, { hasLeaves=false }={}) {
let node = this.#root;
let withLeaves = node;
for ( const string of strings ) {
if ( !(string in node) ) return hasLeaves ? withLeaves : undefined;
node = node[string];
if ( node[StringTree.leaves].length ) withLeaves = node;
}
return hasLeaves ? withLeaves : node;
}
/* -------------------------------------------- */
/**
* Perform a breadth-first search starting from the given node and retrieving any entries reachable from that node,
* until we reach the limit.
* @param {StringTreeNode} node The starting node.
* @param {any[]} entries The accumulated entries.
* @param {StringTreeNode[]} queue The working queue of nodes to search.
* @param {object} [options]
* @param {number} [options.limit] The maximum number of entries to retrieve before stopping.
* @param {StringTreeEntryFilter} [options.filterEntries] A filter function to apply to each candidate entry.
* @protected
*/
_breadthFirstSearch(node, entries, queue, { limit, filterEntries }={}) {
// Retrieve the entries at this node.
let leaves = node[StringTree.leaves];
if ( filterEntries instanceof Function ) leaves = leaves.filter(filterEntries);
entries.push(...leaves);
if ( limit && (entries.length >= limit) ) return;
// Push this node's children onto the end of the queue.
for ( const key of Object.keys(node) ) {
if ( typeof key === "string" ) queue.push(node[key]);
}
}
}