@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
JavaScript
"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