2024-08-07 02:11:38 +08:00
|
|
|
import {
|
2024-10-11 21:39:14 +08:00
|
|
|
collectNodesOfType,
|
2024-08-07 02:11:38 +08:00
|
|
|
findNodeOfType,
|
|
|
|
renderToText,
|
|
|
|
traverseTree,
|
|
|
|
} from "@silverbulletmd/silverbullet/lib/tree";
|
|
|
|
import type {
|
|
|
|
IndexTreeEvent,
|
|
|
|
ObjectValue,
|
|
|
|
} from "@silverbulletmd/silverbullet/types";
|
|
|
|
import {
|
|
|
|
isLocalPath,
|
|
|
|
resolvePath,
|
|
|
|
} from "@silverbulletmd/silverbullet/lib/resolve";
|
2023-10-03 20:16:33 +08:00
|
|
|
import { indexObjects, queryObjects } from "./api.ts";
|
2024-08-07 02:11:38 +08:00
|
|
|
import { extractFrontmatter } from "@silverbulletmd/silverbullet/lib/frontmatter";
|
|
|
|
import { updateITags } from "@silverbulletmd/silverbullet/lib/tags";
|
|
|
|
import {
|
|
|
|
looksLikePathWithExtension,
|
|
|
|
parsePageRef,
|
|
|
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
2024-03-02 21:48:02 +08:00
|
|
|
import { extractSnippetAroundIndex } from "./snippet_extractor.ts";
|
2024-08-02 22:47:36 +08:00
|
|
|
import {
|
|
|
|
mdLinkRegex,
|
|
|
|
wikiLinkRegex,
|
|
|
|
} from "$common/markdown_parser/constants.ts";
|
2024-08-24 18:35:09 +08:00
|
|
|
import { space } from "@silverbulletmd/silverbullet/syscalls";
|
2023-07-28 21:20:56 +08:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
>;
|
2023-07-28 21:20:56 +08:00
|
|
|
|
2024-08-24 18:35:09 +08:00
|
|
|
/**
|
|
|
|
* Represents a page that does not yet exist, but is being linked to. A page "aspiring" to be created.
|
|
|
|
*/
|
|
|
|
export type AspiringPageObject = ObjectValue<{
|
|
|
|
// ref: page@pos
|
|
|
|
// The page the link appears on
|
|
|
|
page: string;
|
|
|
|
// And the position
|
|
|
|
pos: number;
|
|
|
|
// The page the link points to
|
|
|
|
name: string;
|
|
|
|
}>;
|
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
|
|
|
const links: ObjectValue<LinkObject>[] = [];
|
2024-01-11 20:20:50 +08:00
|
|
|
const frontmatter = await extractFrontmatter(tree);
|
2023-10-03 20:16:33 +08:00
|
|
|
const pageText = renderToText(tree);
|
2023-08-02 03:35:19 +08:00
|
|
|
|
2023-07-28 21:20:56 +08:00
|
|
|
traverseTree(tree, (n): boolean => {
|
2024-05-28 02:33:41 +08:00
|
|
|
// Index [[WikiLinks]]
|
2023-07-28 21:20:56 +08:00
|
|
|
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!);
|
2023-10-03 20:16:33 +08:00
|
|
|
const pos = wikiLinkPage.from!;
|
2024-05-28 02:33:41 +08:00
|
|
|
|
|
|
|
const link: any = {
|
2023-10-03 20:16:33 +08:00
|
|
|
ref: `${name}@${pos}`,
|
2024-01-11 20:20:50 +08:00
|
|
|
tag: "link",
|
2024-03-02 21:48:02 +08:00
|
|
|
snippet: extractSnippetAroundIndex(pageText, pos),
|
2023-10-03 20:16:33 +08:00
|
|
|
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
|
2024-07-13 19:51:49 +08:00
|
|
|
if (looksLikePathWithExtension(url)) {
|
2024-05-28 02:33:41 +08:00
|
|
|
link.toFile = url;
|
|
|
|
} else {
|
|
|
|
link.toPage = parsePageRef(url).page;
|
|
|
|
}
|
2023-07-28 21:20:56 +08:00
|
|
|
if (wikiLinkAlias) {
|
2023-10-03 20:16:33 +08:00
|
|
|
link.alias = wikiLinkAlias.children![0].text!;
|
2023-07-28 21:20:56 +08:00
|
|
|
}
|
2024-01-11 20:20:50 +08:00
|
|
|
updateITags(link, frontmatter);
|
2023-10-03 20:16:33 +08:00
|
|
|
links.push(link);
|
2023-07-28 21:20:56 +08:00
|
|
|
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
|
2024-07-13 19:51:49 +08:00
|
|
|
if (looksLikePathWithExtension(url)) {
|
2024-05-28 02:33:41 +08:00
|
|
|
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
|
2024-07-13 19:51:49 +08:00
|
|
|
if (looksLikePathWithExtension(url)) {
|
2024-05-28 02:33:41 +08:00
|
|
|
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}`,
|
2024-01-11 20:20:50 +08:00
|
|
|
tag: "link",
|
2023-11-19 19:18:16 +08:00
|
|
|
page: name,
|
2024-03-02 21:48:02 +08:00
|
|
|
snippet: extractSnippetAroundIndex(pageText, pos),
|
2023-11-19 19:18:16 +08:00
|
|
|
pos: pos,
|
|
|
|
asTemplate: true,
|
2024-01-11 20:20:50 +08:00
|
|
|
};
|
2024-07-13 19:51:49 +08:00
|
|
|
if (looksLikePathWithExtension(url)) {
|
2024-05-28 02:33:41 +08:00
|
|
|
link.toFile = resolvePath(name, url);
|
|
|
|
} else {
|
|
|
|
link.toPage = resolvePath(name, parsePageRef(url).page);
|
|
|
|
}
|
|
|
|
if (alias) {
|
|
|
|
link.alias = alias;
|
|
|
|
}
|
2024-01-11 20:20:50 +08:00
|
|
|
updateITags(link, frontmatter);
|
|
|
|
links.push(link);
|
2023-11-19 19:18:16 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-10-11 21:39:14 +08:00
|
|
|
|
|
|
|
// Also index links used inside quoted frontmatter strings like "[[Page]]"
|
|
|
|
// must match the full string node, only allowing for quotes and whitespace around it
|
|
|
|
if (n.type === "FrontMatter") {
|
|
|
|
// The YAML in frontmatter is parsed by CodeMirror itself
|
|
|
|
for (const textNode of collectNodesOfType(n, "string")) {
|
|
|
|
const text = textNode.children![0].text!;
|
|
|
|
const trimmed = text.replace(/^["'\s]*/, "").replace(/["'\s]*$/, "");
|
|
|
|
// Make sure we search from the beginning, when reusing a Regex object with global flag
|
|
|
|
wikiLinkRegex.lastIndex = 0;
|
|
|
|
const match = wikiLinkRegex.exec(text);
|
|
|
|
// Search in entire node text to get correct position, but check for full match against trimmed
|
|
|
|
if (match && match[0] === trimmed) {
|
|
|
|
const [_fullMatch, firstMark, url, alias, _lastMark] = match;
|
|
|
|
const pos = textNode.from! + match.index! + firstMark.length;
|
|
|
|
const link: any = {
|
|
|
|
ref: `${name}@${pos}`,
|
|
|
|
tag: "link",
|
|
|
|
page: name,
|
|
|
|
snippet: extractSnippetAroundIndex(pageText, pos),
|
|
|
|
pos: pos,
|
|
|
|
asTemplate: false,
|
|
|
|
};
|
|
|
|
if (looksLikePathWithExtension(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-07-28 21:20:56 +08:00
|
|
|
return false;
|
|
|
|
});
|
2024-08-24 18:35:09 +08:00
|
|
|
|
2023-11-19 19:18:16 +08:00
|
|
|
// console.log("Found", links, "page link(s)");
|
2024-08-24 18:35:09 +08:00
|
|
|
if (links.length > 0) {
|
|
|
|
await indexObjects(name, links);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now let's check which are aspiring pages
|
|
|
|
const aspiringPages: ObjectValue<AspiringPageObject>[] = [];
|
|
|
|
for (const link of links) {
|
|
|
|
if (link.toPage) {
|
|
|
|
// No federated links, nothing with template directives
|
|
|
|
if (link.toPage.startsWith("!") || link.toPage.includes("{{")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!await space.fileExists(`${link.toPage}.md`)) {
|
|
|
|
aspiringPages.push({
|
|
|
|
ref: `${name}@${link.pos}`,
|
|
|
|
tag: "aspiring-page",
|
|
|
|
page: name,
|
|
|
|
pos: link.pos,
|
|
|
|
name: link.toPage,
|
|
|
|
} as AspiringPageObject);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aspiringPages.length > 0) {
|
|
|
|
await indexObjects(name, aspiringPages);
|
|
|
|
}
|
2023-07-28 21:20:56 +08:00
|
|
|
}
|
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
export async function getBackLinks(
|
2024-05-28 02:33:41 +08:00
|
|
|
name: string,
|
2023-10-03 20:16:33 +08:00
|
|
|
): 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]]],
|
2023-10-03 20:16:33 +08:00
|
|
|
}));
|
2023-07-28 21:20:56 +08:00
|
|
|
}
|