silverbullet/plugs/index/page_links.ts

195 lines
5.9 KiB
TypeScript
Raw Normal View History

2024-02-29 22:23:05 +08:00
import { findNodeOfType, renderToText, traverseTree } from "$sb/lib/tree.ts";
2024-05-28 02:33:41 +08:00
import { IndexTreeEvent, ObjectValue } from "$sb/types.ts";
import { isLocalPath, resolvePath } from "$sb/lib/resolve.ts";
import { indexObjects, queryObjects } from "./api.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { updateITags } from "$sb/lib/tags.ts";
import { parsePageRef } from "$sb/lib/page_ref.ts";
import { extractSnippetAroundIndex } from "./snippet_extractor.ts";
2024-05-28 02:33:41 +08:00
import { mdLinkRegex, wikiLinkRegex } from "$common/markdown_parser/parser.ts";
2024-05-28 02:33:41 +08:00
export type LinkObject = ObjectValue<
{
//Page Link
// The page the link points to
toPage: string;
// The page the link occurs in
page: string;
pos: number;
snippet: string;
alias?: string;
asTemplate: boolean;
toFile?: never;
} | {
// Attachment Link
// The file the link points to
toFile: string;
// The page the link occurs in
page: string;
pos: number;
snippet: string;
alias?: string;
asTemplate: boolean;
toPage?: never;
}
>;
export async function indexLinks({ name, tree }: IndexTreeEvent) {
const links: ObjectValue<LinkObject>[] = [];
const frontmatter = await extractFrontmatter(tree);
const pageText = renderToText(tree);
2023-08-02 03:35:19 +08:00
traverseTree(tree, (n): boolean => {
2024-05-28 02:33:41 +08:00
// Index [[WikiLinks]]
if (n.type === "WikiLink") {
const wikiLinkPage = findNodeOfType(n, "WikiLinkPage")!;
const wikiLinkAlias = findNodeOfType(n, "WikiLinkAlias");
2024-05-28 02:33:41 +08:00
const url = resolvePath(name, "/" + wikiLinkPage.children![0].text!);
const pos = wikiLinkPage.from!;
2024-05-28 02:33:41 +08:00
const link: any = {
ref: `${name}@${pos}`,
tag: "link",
snippet: extractSnippetAroundIndex(pageText, pos),
pos,
page: name,
asTemplate: false,
};
2024-05-28 02:33:41 +08:00
// Assume link is to an attachment if it has
// an extension, to a page otherwise
if (/\.[a-zA-Z0-9]+$/.test(url)) {
link.toFile = url;
} else {
link.toPage = parsePageRef(url).page;
}
if (wikiLinkAlias) {
link.alias = wikiLinkAlias.children![0].text!;
}
updateITags(link, frontmatter);
links.push(link);
return true;
}
2023-11-19 19:18:16 +08:00
2024-05-28 02:33:41 +08:00
// Also index [Markdown style]() links
if (n.type === "URL") {
const linkNode = findNodeOfType(n, "URL")!;
if (!linkNode) {
return false;
}
const text = /\[(?<title>[^\]]*)\]\((?<url>.+)\)/
.exec(renderToText(linkNode.parent));
if (!text) {
return false;
}
let [/* fullMatch */, alias, url] = text;
// Check if local link
if (!isLocalPath(url)) {
return false;
}
const pos = linkNode.from!;
url = resolvePath(name, decodeURI(url));
const link: any = {
ref: `${name}@${pos}`,
tag: "link",
snippet: extractSnippetAroundIndex(pageText, pos),
pos,
page: name,
asTemplate: false,
};
// Assume link is to an attachment if it has
// an extension, to a page otherwise
if (/\.[a-zA-Z0-9]+$/.test(url)) {
link.toFile = url;
} else {
link.toPage = parsePageRef(url).page;
}
if (alias) {
link.alias = alias;
}
updateITags(link, frontmatter);
links.push(link);
return true;
}
2023-11-19 19:18:16 +08:00
// Also index links used inside query and template fenced code blocks
if (n.type === "FencedCode") {
const codeInfo = findNodeOfType(n, "CodeInfo")!;
if (!codeInfo) {
return false;
}
const codeLang = codeInfo.children![0].text!;
if (codeLang === "template" || codeLang === "query") {
const codeText = findNodeOfType(n, "CodeText");
if (!codeText) {
return false;
}
const code = codeText.children![0].text!;
2024-05-28 02:33:41 +08:00
const wikiLinkMatches = code.matchAll(wikiLinkRegex);
for (const match of wikiLinkMatches) {
const [_fullMatch, firstMark, url, alias, _lastMark] = match;
const pos = codeText.from! + match.index! + firstMark.length;
const link: any = {
ref: `${name}@${pos}`,
tag: "link",
page: name,
snippet: extractSnippetAroundIndex(pageText, pos),
pos: pos,
asTemplate: true,
};
// Assume link is to an attachment if it has
// an extension, to a page otherwise
if (/\.[a-zA-Z0-9]+$/.test(url)) {
link.toFile = resolvePath(name, "/" + url);
} else {
link.toPage = resolvePath(name, "/" + parsePageRef(url).page);
}
if (alias) {
link.alias = alias;
}
updateITags(link, frontmatter);
links.push(link);
}
const mdLinkMatches = code.matchAll(mdLinkRegex);
for (const match of mdLinkMatches) {
const [_fullMatch, alias, url] = match;
const pos = codeText.from! + match.index! + 1;
const link: any = {
2023-11-19 19:18:16 +08:00
ref: `${name}@${pos}`,
tag: "link",
2023-11-19 19:18:16 +08:00
page: name,
snippet: extractSnippetAroundIndex(pageText, pos),
2023-11-19 19:18:16 +08:00
pos: pos,
asTemplate: true,
};
2024-05-28 02:33:41 +08:00
if (/\.[a-zA-Z0-9]+$/.test(url)) {
link.toFile = resolvePath(name, url);
} else {
link.toPage = resolvePath(name, parsePageRef(url).page);
}
if (alias) {
link.alias = alias;
}
updateITags(link, frontmatter);
links.push(link);
2023-11-19 19:18:16 +08:00
}
}
}
return false;
});
2023-11-19 19:18:16 +08:00
// console.log("Found", links, "page link(s)");
await indexObjects(name, links);
}
export async function getBackLinks(
2024-05-28 02:33:41 +08:00
name: string,
): Promise<LinkObject[]> {
return (await queryObjects<LinkObject>("link", {
2024-05-28 02:33:41 +08:00
filter: ["or", ["=", ["attr", "toPage"], ["string", name]], ["=", [
"attr",
"toFile",
], ["string", name]]],
}));
}