UNPKG

@mdfriday/foundry

Version:

The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.

488 lines 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Tree = void 0; exports.createTree = createTree; exports.createTreeFromMap = createTreeFromMap; // leafNode is used to represent a value class LeafNode { constructor(key, val) { this.key = key; this.val = val; } } // edge is used to represent an edge node class Edge { constructor(label, node) { this.label = label; this.node = node; } } class Node { constructor() { // leaf is used to store possible leaf this.leaf = null; // prefix is the common prefix we ignore this.prefix = ''; // Edges should be stored in-order for iteration. // We avoid a fully materialized slice to save memory, // since in most cases we expect to be sparse this.edges = []; } isLeaf() { return this.leaf !== null; } addEdge(e) { const num = this.edges.length; let idx = 0; // Binary search to find insertion point while (idx < num && this.edges[idx].label < e.label) { idx++; } this.edges.splice(idx, 0, e); } updateEdge(label, node) { const num = this.edges.length; let idx = 0; // Binary search while (idx < num && this.edges[idx].label < label) { idx++; } if (idx < num && this.edges[idx].label === label) { this.edges[idx].node = node; return; } throw new Error("replacing missing edge"); } getEdge(label) { const num = this.edges.length; let idx = 0; // Binary search while (idx < num && this.edges[idx].label < label) { idx++; } if (idx < num && this.edges[idx].label === label) { return this.edges[idx].node; } return null; } delEdge(label) { const num = this.edges.length; let idx = 0; // Binary search while (idx < num && this.edges[idx].label < label) { idx++; } if (idx < num && this.edges[idx].label === label) { this.edges.splice(idx, 1); } } mergeChild() { const e = this.edges[0]; const child = e.node; this.prefix = this.prefix + child.prefix; this.leaf = child.leaf; this.edges = child.edges; } } // longestPrefix finds the length of the shared prefix // of two strings function longestPrefix(k1, k2) { const max = Math.min(k1.length, k2.length); let i = 0; for (i = 0; i < max; i++) { if (k1[i] !== k2[i]) { break; } } return i; } // Tree implements a radix tree. This can be treated as a // Dictionary abstract data type. The main advantage over // a standard hash map is prefix-based lookups and // ordered iteration, class Tree { constructor() { this.root = new Node(); this.size = 0; } // NewFromMap returns a new tree containing the keys // from an existing map static newFromMap(m) { const t = new Tree(); if (m) { if (m instanceof Map) { m.forEach((v, k) => { t.insert(k, v); }); } else { for (const [k, v] of Object.entries(m)) { t.insert(k, v); } } } return t; } // Len is used to return the number of elements in the tree len() { return this.size; } // Insert is used to add a newentry or update // an existing entry. Returns [previousValue, wasUpdated] insert(s, v) { let parent = null; let n = this.root; let search = s; while (true) { // Handle key exhaustion if (search.length === 0) { if (n.isLeaf()) { const old = n.leaf.val; n.leaf.val = v; return [old, true]; } n.leaf = new LeafNode(s, v); this.size++; return [undefined, false]; } // Look for the edge parent = n; n = n.getEdge(search.charCodeAt(0)); // No edge, create one if (n === null) { const e = new Edge(search.charCodeAt(0), new Node()); e.node.leaf = new LeafNode(s, v); e.node.prefix = search; parent.addEdge(e); this.size++; return [undefined, false]; } // Determine longest prefix of the search key on match const commonPrefix = longestPrefix(search, n.prefix); if (commonPrefix === n.prefix.length) { search = search.substring(commonPrefix); continue; } // Split the node this.size++; const child = new Node(); child.prefix = search.substring(0, commonPrefix); parent.updateEdge(search.charCodeAt(0), child); // Restore the existing node child.addEdge(new Edge(n.prefix.charCodeAt(commonPrefix), n)); n.prefix = n.prefix.substring(commonPrefix); // Create a new leaf node const leaf = new LeafNode(s, v); // If the new key is a subset, add to this node search = search.substring(commonPrefix); if (search.length === 0) { child.leaf = leaf; return [undefined, false]; } // Create a new edge for the node const newNode = new Node(); newNode.leaf = leaf; newNode.prefix = search; child.addEdge(new Edge(search.charCodeAt(0), newNode)); return [undefined, false]; } } // Delete is used to delete a key, returning the previous // value and if it was deleted delete(s) { let parent = null; let label = 0; let n = this.root; let search = s; while (true) { // Check for key exhaustion if (search.length === 0) { if (!n.isLeaf()) { break; } // DELETE const leaf = n.leaf; n.leaf = null; this.size--; // Check if we should delete this node from the parent if (parent !== null && n.edges.length === 0) { parent.delEdge(label); } // Check if we should merge this node if (n !== this.root && n.edges.length === 1) { n.mergeChild(); } // Check if we should merge the parent's other child if (parent !== null && parent !== this.root && parent.edges.length === 1 && !parent.isLeaf()) { parent.mergeChild(); } return [leaf.val, true]; } // Look for an edge parent = n; label = search.charCodeAt(0); n = n.getEdge(label); if (n === null) { break; } // Consume the search prefix if (search.startsWith(n.prefix)) { search = search.substring(n.prefix.length); } else { break; } } return [undefined, false]; } // DeletePrefix is used to delete the subtree under a prefix // Returns how many nodes were deleted // Use this to delete large subtrees efficiently async deletePrefix(s) { return await this._deletePrefix(null, this.root, s); } // _deletePrefix does a recursive deletion async _deletePrefix(parent, n, prefix) { // Check for key exhaustion if (prefix.length === 0) { // Remove the leaf node let subTreeSize = 0; // recursively walk from all edges of the node to be deleted await recursiveWalk(n, (s, v) => { subTreeSize++; return Promise.resolve(false); }); if (n.isLeaf()) { n.leaf = null; } n.edges = []; // deletes the entire subtree // Check if we should merge the parent's other child if (parent !== null && parent !== this.root && parent.edges.length === 1 && !parent.isLeaf()) { parent.mergeChild(); } this.size -= subTreeSize; return subTreeSize; } // Look for an edge const label = prefix.charCodeAt(0); const child = n.getEdge(label); if (child === null || (!child.prefix.startsWith(prefix) && !prefix.startsWith(child.prefix))) { return 0; } // Consume the search prefix if (child.prefix.length > prefix.length) { prefix = prefix.substring(prefix.length); } else { prefix = prefix.substring(child.prefix.length); } return this._deletePrefix(n, child, prefix); } // Get is used to lookup a specific key, returning // the value and if it was found get(s) { let n = this.root; let search = s; while (true) { // Check for key exhaustion if (search.length === 0) { if (n.isLeaf()) { return [n.leaf.val, true]; } break; } // Look for an edge n = n.getEdge(search.charCodeAt(0)); if (n === null) { break; } // Consume the search prefix if (search.startsWith(n.prefix)) { search = search.substring(n.prefix.length); } else { break; } } return [undefined, false]; } // LongestPrefix is like Get, but instead of an // exact match, it will return the longest prefix match. longestPrefix(s) { let last = null; let n = this.root; let search = s; while (true) { // Look for a leaf node if (n.isLeaf()) { last = n.leaf; } // Check for key exhaustion if (search.length === 0) { break; } // Look for an edge n = n.getEdge(search.charCodeAt(0)); if (n === null) { break; } // Consume the search prefix if (search.startsWith(n.prefix)) { search = search.substring(n.prefix.length); } else { break; } } if (last !== null) { return [last.key, last.val, true]; } return ['', undefined, false]; } // Minimum is used to return the minimum value in the tree minimum() { let n = this.root; while (true) { if (n.isLeaf()) { return [n.leaf.key, n.leaf.val, true]; } if (n.edges.length > 0) { n = n.edges[0].node; } else { break; } } return ['', undefined, false]; } // Maximum is used to return the maximum value in the tree maximum() { let n = this.root; while (true) { const num = n.edges.length; if (num > 0) { n = n.edges[num - 1].node; continue; } if (n.isLeaf()) { return [n.leaf.key, n.leaf.val, true]; } break; } return ['', undefined, false]; } // Walk is used to walk the tree async walk(fn) { await recursiveWalk(this.root, fn); } // WalkPrefix is used to walk the tree under a prefix async walkPrefix(prefix, fn) { let n = this.root; let search = prefix; while (true) { // Check for key exhaustion if (search.length === 0) { await recursiveWalk(n, fn); return; } // Look for an edge n = n.getEdge(search.charCodeAt(0)); if (n === null) { return; } // Consume the search prefix if (search.startsWith(n.prefix)) { search = search.substring(n.prefix.length); continue; } if (n.prefix.startsWith(search)) { // Child may be under our search prefix await recursiveWalk(n, fn); } return; } } // WalkPath is used to walk the tree, but only visiting nodes // from the root down to a given leaf. Where WalkPrefix walks // all the entries *under* the given prefix, this walks the // entries *above* the given prefix. async walkPath(path, fn) { let n = this.root; let search = path; while (true) { // Visit the leaf values if any if (n.leaf !== null && await fn(n.leaf.key, n.leaf.val)) { return; } // Check for key exhaustion if (search.length === 0) { return; } // Look for an edge n = n.getEdge(search.charCodeAt(0)); if (n === null) { return; } // Consume the search prefix if (search.startsWith(n.prefix)) { search = search.substring(n.prefix.length); } else { break; } } } // ToMap is used to walk the tree and convert it into a map async toMap() { const out = {}; await this.walk((k, v) => { out[k] = v; return Promise.resolve(false); }); return out; } } exports.Tree = Tree; // recursiveWalk is used to do a pre-order walk of a node // recursively. Returns true if the walk should be aborted async function recursiveWalk(n, fn) { // Visit the leaf values if any if (n.leaf !== null && await fn(n.leaf.key, n.leaf.val)) { return true; } // Recurse on the children let i = 0; let k = n.edges.length; // keeps track of number of edges in previous iteration while (i < k) { const e = n.edges[i]; if (await recursiveWalk(e.node, fn)) { return true; } // It is a possibility that the WalkFn modified the node we are // iterating on. If there are no more edges, mergeChild happened, // so the last edge became the current node n, on which we'll // iterate one last time. if (n.edges.length === 0) { return recursiveWalk(n, fn); } // If there are now less edges than in the previous iteration, // then do not increment the loop index, since the current index // points to a new edge. Otherwise, get to the next index. if (n.edges.length >= k) { i++; } k = n.edges.length; } return false; } // Factory functions for convenience function createTree() { return new Tree(); } function createTreeFromMap(m) { return Tree.newFromMap(m); } //# sourceMappingURL=radix.js.map