Files
plainleaf/client/codemirror/wiki_link_processor.ts
T

133 lines
3.6 KiB
TypeScript

import type { EditorState } from "@codemirror/state";
import { Decoration } from "@codemirror/view";
import type { Client } from "../client.ts";
import {
fileName,
isBuiltinPath,
} from "@silverbulletmd/silverbullet/lib/resolve";
import {
encodePageURI,
encodeRef,
getNameFromPath,
parseToRef,
} from "@silverbulletmd/silverbullet/lib/ref";
import { isCursorInRange, LinkWidget } from "./util.ts";
export interface WikiLinkMatch {
leadingTrivia: string;
stringRef: string;
alias?: string;
trailingTrivia: string;
}
export interface WikiLinkProcessorOptions {
from: number;
to: number;
match: WikiLinkMatch;
matchFrom: number;
matchTo: number;
client: Client;
state: EditorState;
shortWikiLinks: boolean;
callback: (e: MouseEvent, ref: any) => void;
}
export function processWikiLink(options: WikiLinkProcessorOptions): any[] {
const { from, to, match, matchFrom, matchTo, client, state, callback } =
options;
const widgets: any[] = [];
const { leadingTrivia, stringRef, alias, trailingTrivia } = match;
const ref = parseToRef(stringRef);
let linkStatus: "file-missing" | "default" | "invalid" = "default";
if (!ref) {
linkStatus = "invalid";
} else if (ref.path === "" || isBuiltinPath(ref.path)) {
linkStatus = "default";
} else if (
Array.from(client.clientSystem.allKnownFiles).some((file) =>
file === ref.path
)
) {
linkStatus = "default";
} else if (client.fullSyncCompleted || client.clientSystem.knownFilesLoaded) {
linkStatus = "file-missing";
}
let css = {
"file-missing": "sb-wiki-link-missing",
"invalid": "sb-wiki-link-invalid",
"default": "",
}[linkStatus];
const renderingSyntax = client.ui.viewState.uiOptions.markdownSyntaxRendering;
if (isCursorInRange(state, [from, to]) || renderingSyntax) {
// Only attach a CSS class, then get out
if (linkStatus !== "default") {
widgets.push(
Decoration.mark({
class: css,
}).range(from + leadingTrivia.length, to - trailingTrivia.length),
);
}
return widgets;
}
const cleanedPath = ref ? getNameFromPath(ref.path) : stringRef;
const helpText = {
"default": `Navigate to ${cleanedPath}`,
"file-missing": `Create ${cleanedPath}`,
"invalid": `Cannot create invalid file ${cleanedPath}`,
}[linkStatus];
let linkText = alias || stringRef;
// The `&& ref` is only there to make typescript happy
if (linkStatus === "default" && ref) {
const meta = client.ui.viewState.allPages.find((p) =>
parseToRef(p.ref)?.path === ref.path
);
const renderedRef = structuredClone(ref);
// We don't want to render the meta
renderedRef.meta = false;
// We also don't want to rendered the prefix of the path
renderedRef.path = options.shortWikiLinks
? fileName(renderedRef.path)
: renderedRef.path;
const prefix = (ref.details?.type === "position" ||
ref.details?.type === "linecolumn")
? ""
: (meta?.pageDecoration?.prefix ?? "");
linkText = alias || (prefix + encodeRef(renderedRef));
if (meta?.pageDecoration?.cssClasses) {
css += " sb-decorated-object " +
meta.pageDecoration.cssClasses
.join(" ")
.replaceAll(/[^a-zA-Z0-9-_ ]/g, "");
}
}
widgets.push(
Decoration.replace({
widget: new LinkWidget({
text: linkText,
title: helpText,
href: ref ? encodePageURI(encodeRef(ref)) : undefined,
cssClass: "sb-wiki-link " + css,
from: matchFrom,
callback: (e) => callback(e, ref),
}),
}).range(matchFrom, matchTo),
);
return widgets;
}