1
0
silverbullet/plug-api/lib/tree.ts

211 lines
4.9 KiB
TypeScript
Raw Normal View History

2022-04-11 18:34:09 +00:00
export type ParseTree = {
2022-04-04 09:51:41 +00:00
type?: string; // undefined === text node
2022-04-04 13:25:07 +00:00
from?: number;
to?: number;
2022-04-04 09:51:41 +00:00
text?: string;
2022-04-11 18:34:09 +00:00
children?: ParseTree[];
// Only present after running addParentPointers
parent?: ParseTree;
2022-04-03 16:12:16 +00:00
};
2022-04-11 18:34:09 +00:00
export function addParentPointers(tree: ParseTree) {
if (!tree.children) {
return;
}
2022-10-15 17:02:56 +00:00
for (const child of tree.children) {
if (child.parent) {
// Already added parent pointers before
return;
}
2022-04-11 18:34:09 +00:00
child.parent = tree;
addParentPointers(child);
}
}
export function removeParentPointers(tree: ParseTree) {
delete tree.parent;
if (!tree.children) {
return;
}
2022-10-15 17:02:56 +00:00
for (const child of tree.children) {
2022-04-11 18:34:09 +00:00
removeParentPointers(child);
}
}
export function findParentMatching(
tree: ParseTree,
2022-10-12 09:47:13 +00:00
matchFn: (tree: ParseTree) => boolean,
2022-04-11 18:34:09 +00:00
): ParseTree | null {
let node = tree.parent;
while (node) {
if (matchFn(node)) {
return node;
}
node = node.parent;
}
return null;
}
export function collectNodesOfType(
tree: ParseTree,
2022-10-12 09:47:13 +00:00
nodeType: string,
2022-04-11 18:34:09 +00:00
): ParseTree[] {
return collectNodesMatching(tree, (n) => n.type === nodeType);
}
export function collectNodesMatching(
tree: ParseTree,
2022-10-12 09:47:13 +00:00
matchFn: (tree: ParseTree) => boolean,
2022-04-11 18:34:09 +00:00
): ParseTree[] {
if (matchFn(tree)) {
return [tree];
}
let results: ParseTree[] = [];
if (tree.children) {
2022-10-15 17:02:56 +00:00
for (const child of tree.children) {
2022-04-11 18:34:09 +00:00
results = [...results, ...collectNodesMatching(child, matchFn)];
}
2022-04-04 09:51:41 +00:00
}
2022-04-11 18:34:09 +00:00
return results;
}
2022-04-03 16:12:16 +00:00
export async function collectNodesMatchingAsync(
tree: ParseTree,
matchFn: (tree: ParseTree) => Promise<boolean>,
): Promise<ParseTree[]> {
if (await matchFn(tree)) {
return [tree];
}
let results: ParseTree[] = [];
if (tree.children) {
for (const child of tree.children) {
results = [
...results,
...await collectNodesMatchingAsync(child, matchFn),
];
}
}
return results;
}
2022-04-11 18:34:09 +00:00
// return value: returning undefined = not matched, continue, null = delete, new node = replace
export function replaceNodesMatching(
tree: ParseTree,
2022-10-12 09:47:13 +00:00
substituteFn: (tree: ParseTree) => ParseTree | null | undefined,
2022-04-11 18:34:09 +00:00
) {
if (tree.children) {
2022-10-15 17:02:56 +00:00
const children = tree.children.slice();
for (const child of children) {
const subst = substituteFn(child);
2022-04-11 18:34:09 +00:00
if (subst !== undefined) {
2022-10-15 17:02:56 +00:00
const pos = tree.children.indexOf(child);
2022-04-11 18:34:09 +00:00
if (subst) {
tree.children.splice(pos, 1, subst);
} else {
// null = delete
tree.children.splice(pos, 1);
}
} else {
replaceNodesMatching(child, substituteFn);
2022-04-04 09:51:41 +00:00
}
2022-04-03 16:12:16 +00:00
}
2022-04-04 09:51:41 +00:00
}
2022-04-11 18:34:09 +00:00
}
export async function replaceNodesMatchingAsync(
tree: ParseTree,
substituteFn: (tree: ParseTree) => Promise<ParseTree | null | undefined>,
) {
if (tree.children) {
const children = tree.children.slice();
for (const child of children) {
const subst = await substituteFn(child);
if (subst !== undefined) {
const pos = tree.children.indexOf(child);
if (subst) {
tree.children.splice(pos, 1, subst);
} else {
// null = delete
tree.children.splice(pos, 1);
}
} else {
2023-07-26 15:12:56 +00:00
await replaceNodesMatchingAsync(child, substituteFn);
}
}
}
}
2022-04-11 18:34:09 +00:00
export function findNodeMatching(
tree: ParseTree,
2022-10-12 09:47:13 +00:00
matchFn: (tree: ParseTree) => boolean,
2022-04-11 18:34:09 +00:00
): ParseTree | null {
return collectNodesMatching(tree, matchFn)[0];
}
export function findNodeOfType(
tree: ParseTree,
2022-10-12 09:47:13 +00:00
nodeType: string,
2022-04-11 18:34:09 +00:00
): ParseTree | null {
return collectNodesMatching(tree, (n) => n.type === nodeType)[0];
}
2022-04-03 16:12:16 +00:00
export function traverseTree(
tree: ParseTree,
// Return value = should stop traversal?
2022-10-12 09:47:13 +00:00
matchFn: (tree: ParseTree) => boolean,
): void {
// Do a collect, but ignore the result
collectNodesMatching(tree, matchFn);
}
export async function traverseTreeAsync(
tree: ParseTree,
// Return value = should stop traversal?
matchFn: (tree: ParseTree) => Promise<boolean>,
): Promise<void> {
// Do a collect, but ignore the result
await collectNodesMatchingAsync(tree, matchFn);
}
2022-04-11 18:34:09 +00:00
// Finds non-text node at position
export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null {
if (pos < tree.from! || pos >= tree.to!) {
2022-04-11 18:34:09 +00:00
return null;
2022-04-04 09:51:41 +00:00
}
2022-04-11 18:34:09 +00:00
if (!tree.children) {
return tree;
2022-04-04 09:51:41 +00:00
}
2022-10-15 17:02:56 +00:00
for (const child of tree.children) {
const n = nodeAtPos(child, pos);
2022-04-11 18:34:09 +00:00
if (n && n.text !== undefined) {
// Got a text node, let's return its parent
return tree;
} else if (n) {
// Got it
return n;
}
}
return null;
2022-04-03 16:12:16 +00:00
}
2022-04-11 18:34:09 +00:00
// Turn ParseTree back into text
export function renderToText(tree: ParseTree): string {
2022-10-15 17:02:56 +00:00
const pieces: string[] = [];
2022-04-11 18:34:09 +00:00
if (tree.text !== undefined) {
return tree.text;
}
2022-10-15 17:02:56 +00:00
for (const child of tree.children!) {
2022-04-11 18:34:09 +00:00
pieces.push(renderToText(child));
}
return pieces.join("");
2022-04-03 16:12:16 +00:00
}
export function cloneTree(tree: ParseTree): ParseTree {
const newTree = { ...tree };
if (tree.children) {
newTree.children = tree.children.map(cloneTree);
}
delete newTree.parent;
return newTree;
}