silverbullet/plugs/index/item.ts

166 lines
3.8 KiB
TypeScript

import type { IndexTreeEvent, ObjectValue } from "../../plug-api/types.ts";
import {
findParentMatching,
type ParseTree,
renderToText,
traverseTreeAsync,
} from "@silverbulletmd/silverbullet/lib/tree";
import {
cleanAttributes,
extractAttributes,
} from "@silverbulletmd/silverbullet/lib/attribute";
import { rewritePageRefs } from "@silverbulletmd/silverbullet/lib/resolve";
import { indexObjects } from "./api.ts";
import {
cleanHashTags,
extractHashTags,
updateITags,
} from "@silverbulletmd/silverbullet/lib/tags";
import {
extractFrontmatter,
type FrontMatter,
} from "@silverbulletmd/silverbullet/lib/frontmatter";
import { deepClone } from "@silverbulletmd/silverbullet/lib/json";
export type ItemObject = ObjectValue<
{
page: string;
name: string;
text: string;
pos: number;
} & Record<string, any>
>;
export async function indexItems({ name, tree }: IndexTreeEvent) {
const items = await extractItems(name, tree);
// console.log("Found", items, "item(s)");
await indexObjects(name, items);
}
export async function extractItems(name: string, tree: ParseTree) {
const items: ObjectValue<ItemObject>[] = [];
const frontmatter = await extractFrontmatter(tree);
await traverseTreeAsync(tree, async (n) => {
if (n.type !== "ListItem") {
return false;
}
if (!n.children) {
// Weird, let's jump out
return true;
}
// Is this a task?
if (n.children.find((n) => n.type === "Task")) {
// Skip tasks
return true;
}
const item: ItemObject = await extractItemFromNode(
name,
n,
frontmatter,
);
items.push(item);
return false;
});
return items;
}
export async function extractItemFromNode(
name: string,
itemNode: ParseTree,
frontmatter: FrontMatter,
) {
const item: ItemObject = {
ref: `${name}@${itemNode.from}`,
tag: "item",
name: "",
text: "",
page: name,
pos: itemNode.from!,
};
// Now let's extract tags and attributes
const tags = extractHashTags(itemNode);
const extractedAttributes = await extractAttributes(
["item", ...tags],
itemNode,
);
const clonedTextNodes: ParseTree[] = [];
for (const child of itemNode.children!.slice(1)) {
rewritePageRefs(child, name);
if (child.type === "OrderedList" || child.type === "BulletList") {
break;
}
clonedTextNodes.push(deepClone(child, ["parent"]));
}
// Original text
item.text = clonedTextNodes.map(renderToText).join("").trim();
// Clean out attribtus and tags and render a clean item name
for (const clonedTextNode of clonedTextNodes) {
cleanHashTags(clonedTextNode);
cleanAttributes(clonedTextNode);
}
item.name = clonedTextNodes.map(renderToText).join("").trim();
if (tags.length > 0) {
item.tags = tags;
}
for (const [key, value] of Object.entries(extractedAttributes)) {
item[key] = value;
}
updateITags(item, frontmatter);
await enrichItemFromParents(itemNode, item, name, frontmatter);
return item;
}
export async function enrichItemFromParents(
n: ParseTree,
item: ObjectValue<any>,
pageName: string,
frontmatter: FrontMatter,
) {
let directParent = true;
let parentItemNode = findParentMatching(n, (n) => n.type === "ListItem");
while (parentItemNode) {
// console.log("Got parent", parentItemNode);
const parentItem = await extractItemFromNode(
pageName,
parentItemNode,
frontmatter,
);
if (directParent) {
item.parent = parentItem.ref;
directParent = false;
}
// Merge tags
item.itags = [
...new Set([
...item.itags || [],
...(parentItem.itags!.filter((t) => !["item", "task"].includes(t))),
]),
];
parentItemNode = findParentMatching(
parentItemNode,
(n) => n.type === "ListItem",
);
}
}