quickshell-web/src/config/io/markdown.ts

171 lines
5.4 KiB
TypeScript

import { visit, CONTINUE, SKIP } from "unist-util-visit";
import { fromHtml } from "hast-util-from-html";
import type * as Unist from "unist";
import type * as Html from "hast";
import type * as Md from "mdast";
import type {
AstroMarkdownOptions,
MarkdownProcessor,
RemarkPlugin,
RehypePlugin,
} from "@astrojs/markdown-remark";
import { createMarkdownProcessor } from "@astrojs/markdown-remark";
import { remarkAlert } from "remark-github-blockquote-alert";
import rehypeShiki from "@shikijs/rehype";
import sectionize from "@hbsnow/rehype-sectionize";
import type { ShikiTransformer } from "shiki";
import {
getQMLTypeLinkObject,
getQMLTypeLink,
getIconForLink,
} from "./helpers.ts";
const remarkParseAtTypes: RemarkPlugin<[]> = () => {
return (root: Md.Root): Md.Root => {
visit(
root as Unist.Parent,
(rawNode: Unist.Node) => {
if (rawNode.type === "text" || (rawNode.type === "code" && (rawNode as Md.Code).lang === "qml")) {
const node = rawNode as Md.Literal;
node.value = node.value.replace(
/@@((?<module>([A-Z]\w*\.)*)(?<type>([A-Z]\w*))\.?)?((?<member>[a-z]\w*)((?<function>\(\))|(?<signal>\(s\)))?)?(?=[$.,;:\s]|$)/g,
(_full, ...args) => {
type Capture = {
module: string | undefined;
type: string | undefined;
member: string | undefined;
function: string | undefined;
signal: string | undefined;
}
const groups = args.pop() as Capture;
if (groups.module) {
groups.module = groups.module.substring(0, groups.module.length - 1);
const isQs = groups.module.startsWith("Quickshell");
groups.module = `99M${isQs ? "QS" : "QT_qml"}_${groups.module.replace(".", "_")}`;
} else groups.module = ""; // WARNING: rehype parser can't currently handle intra-module links
groups.type = groups.type ? `99N${groups.type}` : "";
groups.member = groups.member ? `99V${groups.member}` : "";
const type = groups.member ? `99T${groups.function ? "func" : groups.signal ? "signal" : "prop"}` : "";
return `TYPE${groups.module}${groups.type}${groups.member}${type}99TYPE`;
}
);
}
}
);
return root;
};
};
const rehypeRewriteTypelinks: RehypePlugin<[]> = () => {
return (root: Html.Root): Html.Root => {
visit(
root as Unist.Parent,
"text",
(node: Html.Text, index: number, parent: Html.Parent) => {
let changed = false;
node.value = node.value.replace(
/TYPE99(\w+.)99TYPE/g,
(_full: string, match: string) => {
changed = true;
const linkObject = getQMLTypeLinkObject(match);
const link = getQMLTypeLink(linkObject);
const icon =
linkObject.mtype && linkObject.mtype !== "func"
? getIconForLink(linkObject.mtype, false)
: null;
const hasParens =
linkObject.mtype === "func" ||
linkObject.mtype === "signal";
const hasDot = linkObject.name && linkObject.mname;
return `<a class="type${linkObject.mtype}-link typedata-link" href=${link}>${icon ?? ""}${linkObject.name ?? ""}${hasDot ? "." : ""}${linkObject.mname ?? ""}${hasParens ? "()" : ""}</a>`;
}
);
if (changed) {
const fragment = fromHtml(node.value, {
fragment: true,
});
parent.children.splice(index, 1, ...fragment.children);
return SKIP;
}
return CONTINUE;
}
);
return root;
};
};
const shikiRewriteTypelinks: ShikiTransformer = {
name: "rewrite-typelinks",
postprocess(code, _options) {
// WARN: need to change the code link identifier to this
const regExp = /TYPE99(\w+.)99TYPE/g;
const hasTypelinks = code.search(regExp) !== -1;
if (hasTypelinks) {
code.replace(regExp, (_full: string, match: string) => {
const linkObject = getQMLTypeLinkObject(match);
const link = getQMLTypeLink(linkObject);
return `<a href=${link}>${linkObject.name ?? ""}</a>`;
});
}
},
};
export const markdownConfig: AstroMarkdownOptions = {
syntaxHighlight: false,
remarkPlugins: [remarkParseAtTypes, [remarkAlert, { legacyTitle: true }]],
rehypePlugins: [
[rehypeShiki, {
themes: {
light: "slack-ochin",
dark: "slack-dark",
},
colorReplacements: {
"slack-ochin": {
"#357b42": "#989eb9", // comments
"#b1108e": "#224bbb", // fields
},
"slack-dark": {
"#222222": "#0f111a", // bg
"#6a9955": "#525666", // comments
},
},
defaultColor: false,
wrap: true,
transformers: [shikiRewriteTypelinks],
}],
// FIXME: incompatible types between unified/Plugin and Astro/RehypePlugin
[sectionize as RehypePlugin, { idPropertyName: "id" }],
rehypeRewriteTypelinks,
],
};
let globalMarkdownProcessor: Promise<MarkdownProcessor>;
async function getMarkdownProcessor(): Promise<MarkdownProcessor> {
if (!globalMarkdownProcessor) {
globalMarkdownProcessor =
createMarkdownProcessor(markdownConfig);
}
return globalMarkdownProcessor;
}
export async function processMarkdown(
markdown: string
): Promise<string> {
return (await (await getMarkdownProcessor()).render(markdown))
.code;
}