158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import type { FunctionMap, Query } from "@silverbulletmd/silverbullet/types";
|
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
|
import type { System } from "$lib/plugos/system.ts";
|
|
import { LimitedMap } from "$lib/limited_map.ts";
|
|
import {
|
|
parsePageRef,
|
|
positionOfLine,
|
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
|
import { parse } from "$common/markdown_parser/parse_tree.ts";
|
|
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
|
|
import { traverseTree } from "@silverbulletmd/silverbullet/lib/tree";
|
|
import { renderToText } from "@silverbulletmd/silverbullet/lib/tree";
|
|
import { findNodeOfType } from "@silverbulletmd/silverbullet/lib/tree";
|
|
import {
|
|
isFederationPath,
|
|
rewritePageRefs,
|
|
} from "@silverbulletmd/silverbullet/lib/resolve";
|
|
|
|
const pageCacheTtl = 10 * 1000; // 10s
|
|
|
|
export function buildQueryFunctions(
|
|
allKnownFiles: Set<string>,
|
|
system: System<any>,
|
|
): FunctionMap {
|
|
const pageCache = new LimitedMap<string>(10);
|
|
|
|
return {
|
|
...builtinFunctions,
|
|
pageExists(name: string) {
|
|
if (typeof name !== "string") {
|
|
throw new Error("pageExists(): name is not a string");
|
|
}
|
|
|
|
if (name.startsWith("!") || name.startsWith("{{")) {
|
|
// Let's assume federated pages exist, and ignore template variable ones
|
|
return true;
|
|
}
|
|
|
|
const flattendFiles = new Set(
|
|
[...allKnownFiles].flatMap((file) =>
|
|
(file.endsWith(".md")) ? [file.slice(0, -3)] : []
|
|
),
|
|
);
|
|
|
|
return flattendFiles.has(name);
|
|
},
|
|
async template(template: unknown, obj: unknown) {
|
|
if (typeof template !== "string") {
|
|
throw new Error("template(): template is not a string");
|
|
}
|
|
|
|
return (await system.invokeFunction("template.renderTemplate", [
|
|
template,
|
|
obj,
|
|
])).text;
|
|
},
|
|
// INTERNAL: Used for implementing the { query } syntax in expressions
|
|
$query(query: Query, variables: Record<string, any>) {
|
|
return system.localSyscall("system.invokeFunction", [
|
|
"query.renderQuery",
|
|
query,
|
|
variables,
|
|
]);
|
|
},
|
|
// INTERNAL: Used to rewrite task references in transclusions
|
|
rewriteRefsAndFederationLinks(template: string, page: string) {
|
|
// Rewrite all task references to include a page ref
|
|
// Parse template into a tree
|
|
const tree = parse(extendedMarkdownLanguage, template);
|
|
// Find tasks and rewrite them
|
|
traverseTree(tree, (node) => {
|
|
if (node.type === "Task") {
|
|
const taskRefWikiLink = findNodeOfType(node, "WikiLinkPage");
|
|
if (taskRefWikiLink) {
|
|
// Check if this looks like a task reference
|
|
const taskRef = taskRefWikiLink.children![0].text!;
|
|
if (taskRef.includes("@")) {
|
|
// Ok, this already has a page ref, skipping
|
|
return true;
|
|
}
|
|
}
|
|
// No task ref found, let's splice it in
|
|
node.children!.splice(1, 0, { text: ` [[${page}@${node.from!}]]` });
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
// And rewrite federation links as well
|
|
if (isFederationPath(page)) {
|
|
rewritePageRefs(tree, page);
|
|
}
|
|
return renderToText(tree);
|
|
},
|
|
|
|
// INTERNAL: Used to implement resolving [[links]] in expressions, also supports [[link#header]] and [[link@pos]] as well as [[link$anchor]]
|
|
async readPage(name: string): Promise<string> {
|
|
const cachedPage = pageCache.get(name);
|
|
if (cachedPage) {
|
|
return cachedPage;
|
|
} else {
|
|
const pageRef = parsePageRef(name);
|
|
try {
|
|
let page: string = await system.localSyscall("space.readPage", [
|
|
pageRef.page,
|
|
]);
|
|
|
|
// Extract page section if pos, anchor, or header are included
|
|
if (pageRef.pos) {
|
|
if (pageRef.pos instanceof Object) {
|
|
pageRef.pos = positionOfLine(
|
|
page,
|
|
pageRef.pos.line,
|
|
pageRef.pos.column,
|
|
);
|
|
}
|
|
// If the page link includes a position, slice the page from that position
|
|
page = page.slice(pageRef.pos);
|
|
} else if (pageRef.anchor) {
|
|
// If the page link includes an anchor, slice the page from that anchor
|
|
const pos = page.indexOf(`$${pageRef.anchor}`);
|
|
page = page.slice(pos);
|
|
} else if (pageRef.header) {
|
|
// If the page link includes a header, select that header (up to the next header at the same level)
|
|
// Note: this an approximation, should ideally use the AST
|
|
let pos = page.indexOf(`# ${pageRef.header}\n`);
|
|
let headingLevel = 1;
|
|
while (page.charAt(pos - 1) === "#") {
|
|
pos--;
|
|
headingLevel++;
|
|
}
|
|
page = page.slice(pos);
|
|
|
|
// Slice up to the next equal or higher level heading
|
|
const headRegex = new RegExp(
|
|
`[^#]#{1,${headingLevel}} [^\n]*\n`,
|
|
"g",
|
|
);
|
|
const endPos = page.slice(headingLevel).search(headRegex) +
|
|
headingLevel;
|
|
if (endPos) {
|
|
page = page.slice(0, endPos);
|
|
}
|
|
}
|
|
|
|
pageCache.set(name, page, pageCacheTtl);
|
|
|
|
return page;
|
|
} catch (e: any) {
|
|
if (e.message === "Not found") {
|
|
throw new Error(`Page not found: ${name}`);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|