feat(markdown): add markdown
This commit is contained in:
parent
6117b9e887
commit
0193880cb0
4036 changed files with 25057 additions and 1990 deletions
195
src/markdown/shiki.ts
Normal file
195
src/markdown/shiki.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import type { Element, Root } from "hast";
|
||||
import {
|
||||
createCssVariablesTheme,
|
||||
createHighlighterCore,
|
||||
type HighlighterCore,
|
||||
type LanguageInput,
|
||||
} from "shiki/core";
|
||||
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
||||
|
||||
const THEME_NAME = "methanium-css";
|
||||
const CACHE_LIMIT = 200;
|
||||
|
||||
export const shikiPreClassName =
|
||||
"shiki m-0 w-max min-w-full bg-[var(--markdown-code-background)]! p-4 font-mono text-[0.82rem]/[1.7] text-[var(--markdown-code-foreground)]! [tab-size:2]";
|
||||
const shikiLineClassNames =
|
||||
"line -mx-4 inline-block min-w-full px-4 group-data-[line-numbers]/code-block:before:inline-block group-data-[line-numbers]/code-block:before:w-10 group-data-[line-numbers]/code-block:before:pe-4 group-data-[line-numbers]/code-block:before:text-end group-data-[line-numbers]/code-block:before:text-muted-foreground group-data-[line-numbers]/code-block:before:select-none group-data-[line-numbers]/code-block:before:content-[attr(data-line)]";
|
||||
const highlightedLineClassNames =
|
||||
"bg-primary/12 shadow-[inset_0.18rem_0_var(--primary-foreground-alt)]";
|
||||
|
||||
const aliases: Record<string, string> = {
|
||||
"c++": "cpp",
|
||||
cjs: "javascript",
|
||||
gql: "graphql",
|
||||
html: "html",
|
||||
js: "javascript",
|
||||
javascript: "javascript",
|
||||
jsx: "jsx",
|
||||
markdown: "markdown",
|
||||
md: "markdown",
|
||||
mjs: "javascript",
|
||||
nix: "nix",
|
||||
py: "python",
|
||||
rs: "rust",
|
||||
sh: "shellscript",
|
||||
shell: "shellscript",
|
||||
bash: "shellscript",
|
||||
ts: "typescript",
|
||||
tsx: "tsx",
|
||||
typescript: "typescript",
|
||||
yml: "yaml",
|
||||
};
|
||||
|
||||
const lazyLanguages: Record<string, LanguageInput> = {
|
||||
c: () => import("@shikijs/langs/c"),
|
||||
cpp: () => import("@shikijs/langs/cpp"),
|
||||
diff: () => import("@shikijs/langs/diff"),
|
||||
dockerfile: () => import("@shikijs/langs/dockerfile"),
|
||||
go: () => import("@shikijs/langs/go"),
|
||||
graphql: () => import("@shikijs/langs/graphql"),
|
||||
java: () => import("@shikijs/langs/java"),
|
||||
kotlin: () => import("@shikijs/langs/kotlin"),
|
||||
lua: () => import("@shikijs/langs/lua"),
|
||||
python: () => import("@shikijs/langs/python"),
|
||||
rust: () => import("@shikijs/langs/rust"),
|
||||
sql: () => import("@shikijs/langs/sql"),
|
||||
};
|
||||
|
||||
const supportedLanguages = new Set([
|
||||
...Object.values(aliases),
|
||||
...Object.keys(lazyLanguages),
|
||||
"css",
|
||||
"json",
|
||||
"toml",
|
||||
"yaml",
|
||||
]);
|
||||
const cache = new Map<string, Root>();
|
||||
const loadingLanguages = new Map<string, Promise<void>>();
|
||||
let highlighterPromise: Promise<HighlighterCore> | undefined;
|
||||
|
||||
function getHighlighter() {
|
||||
highlighterPromise ??= createHighlighterCore({
|
||||
engine: createJavaScriptRegexEngine({ target: "auto" }),
|
||||
themes: [
|
||||
createCssVariablesTheme({
|
||||
name: THEME_NAME,
|
||||
variablePrefix: "--markdown-code-",
|
||||
variableDefaults: {
|
||||
background: "transparent",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
fontStyle: true,
|
||||
}),
|
||||
],
|
||||
langs: [
|
||||
import("@shikijs/langs/nix"),
|
||||
import("@shikijs/langs/javascript"),
|
||||
import("@shikijs/langs/typescript"),
|
||||
import("@shikijs/langs/jsx"),
|
||||
import("@shikijs/langs/tsx"),
|
||||
import("@shikijs/langs/json"),
|
||||
import("@shikijs/langs/shellscript"),
|
||||
import("@shikijs/langs/html"),
|
||||
import("@shikijs/langs/css"),
|
||||
import("@shikijs/langs/markdown"),
|
||||
import("@shikijs/langs/yaml"),
|
||||
import("@shikijs/langs/toml"),
|
||||
],
|
||||
});
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
function normalizeLanguage(language?: string) {
|
||||
const normalized = language?.trim().toLowerCase();
|
||||
if (!normalized) return undefined;
|
||||
return aliases[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
async function ensureLanguage(highlighter: HighlighterCore, language: string) {
|
||||
if (highlighter.getLoadedLanguages().includes(language)) return;
|
||||
const loader = lazyLanguages[language];
|
||||
if (!loader) return;
|
||||
|
||||
let pending = loadingLanguages.get(language);
|
||||
if (!pending) {
|
||||
pending = highlighter.loadLanguage(loader).then(() => undefined);
|
||||
loadingLanguages.set(language, pending);
|
||||
}
|
||||
await pending;
|
||||
}
|
||||
|
||||
function addClassNames(node: Element, classNames: string) {
|
||||
const current = node.properties.className ?? node.properties.class;
|
||||
const classes = Array.isArray(current)
|
||||
? current.map(String)
|
||||
: current === undefined
|
||||
? []
|
||||
: [String(current)];
|
||||
node.properties.className = [...classes, ...classNames.split(" ")];
|
||||
}
|
||||
|
||||
function decorateTree(tree: Root, highlightedLines: ReadonlySet<number>) {
|
||||
const pre = tree.children.find(
|
||||
(node): node is Element =>
|
||||
node.type === "element" && node.tagName === "pre",
|
||||
);
|
||||
if (pre) addClassNames(pre, shikiPreClassName);
|
||||
|
||||
const code = pre?.children.find(
|
||||
(node): node is Element =>
|
||||
node.type === "element" && node.tagName === "code",
|
||||
);
|
||||
let line = 0;
|
||||
for (const node of code?.children ?? []) {
|
||||
if (node.type !== "element") continue;
|
||||
const className = node.properties.className ?? node.properties.class;
|
||||
const classes = Array.isArray(className)
|
||||
? className.map(String)
|
||||
: className === undefined
|
||||
? []
|
||||
: [String(className)];
|
||||
if (!classes.includes("line")) continue;
|
||||
line += 1;
|
||||
node.properties["data-line"] = String(line);
|
||||
addClassNames(node, shikiLineClassNames);
|
||||
if (highlightedLines.has(line))
|
||||
addClassNames(node, highlightedLineClassNames);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSupportedLanguage(language?: string) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
return normalized !== undefined && supportedLanguages.has(normalized);
|
||||
}
|
||||
|
||||
export async function highlightCode(
|
||||
code: string,
|
||||
language: string,
|
||||
highlightedLines: ReadonlySet<number>,
|
||||
) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
if (!normalized || !supportedLanguages.has(normalized)) return null;
|
||||
|
||||
const highlighted = [...highlightedLines].sort((left, right) => left - right);
|
||||
const key = JSON.stringify([code, normalized, highlighted]);
|
||||
const cached = cache.get(key);
|
||||
if (cached) {
|
||||
cache.delete(key);
|
||||
cache.set(key, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
const highlighter = await getHighlighter();
|
||||
await ensureLanguage(highlighter, normalized);
|
||||
const tree = highlighter.codeToHast(code, {
|
||||
lang: normalized,
|
||||
theme: THEME_NAME,
|
||||
});
|
||||
decorateTree(tree, highlightedLines);
|
||||
cache.set(key, tree);
|
||||
if (cache.size > CACHE_LIMIT) {
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest !== undefined) cache.delete(oldest);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
Loading…
Reference in a new issue