mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-27 20:29:47 +08:00
feat(baoyu-md): add Mermaid preprocessing and utility modules
This commit is contained in:
Vendored
+205
-5120
File diff suppressed because one or more lines are too long
Vendored
+205
-5120
File diff suppressed because one or more lines are too long
@@ -5,6 +5,8 @@ export * from "./document.js";
|
|||||||
export * from "./extend-config.js";
|
export * from "./extend-config.js";
|
||||||
export * from "./html-builder.js";
|
export * from "./html-builder.js";
|
||||||
export * from "./images.js";
|
export * from "./images.js";
|
||||||
|
export * from "./mermaid-preprocess.js";
|
||||||
|
export * from "./mermaid-utils.js";
|
||||||
export * from "./renderer.js";
|
export * from "./renderer.js";
|
||||||
export * from "./themes.js";
|
export * from "./themes.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
preprocessMermaidInMarkdown,
|
||||||
|
type MermaidRenderFn,
|
||||||
|
} from "./mermaid-preprocess.ts";
|
||||||
|
|
||||||
|
function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mermaid-preprocess-test-"));
|
||||||
|
return fn(dir).finally(() => {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stubPngBytes = Buffer.from([
|
||||||
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stubRender: MermaidRenderFn = async (_code, outPath) => {
|
||||||
|
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
|
||||||
|
await fs.promises.writeFile(outPath, stubPngBytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("preprocessMermaidInMarkdown skips when disabled", async () => {
|
||||||
|
await withTempDir(async (baseDir) => {
|
||||||
|
const markdown = "```mermaid\ngraph TD\nA-->B\n```";
|
||||||
|
const result = await preprocessMermaidInMarkdown(markdown, {
|
||||||
|
baseDir,
|
||||||
|
renderFn: stubRender,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
assert.equal(result.markdown, markdown);
|
||||||
|
assert.equal(result.images.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preprocessMermaidInMarkdown skips when renderFn is missing", async () => {
|
||||||
|
await withTempDir(async (baseDir) => {
|
||||||
|
const markdown = "```mermaid\ngraph TD\nA-->B\n```";
|
||||||
|
const result = await preprocessMermaidInMarkdown(markdown, { baseDir });
|
||||||
|
assert.equal(result.markdown, markdown);
|
||||||
|
assert.equal(result.images.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preprocessMermaidInMarkdown deduplicates identical blocks via hashed cache", async () => {
|
||||||
|
await withTempDir(async (baseDir) => {
|
||||||
|
const block = "```mermaid\ngraph TD\nA-->B\n```";
|
||||||
|
const markdown = `${block}\n\nsome text\n\n${block}\n\nother text\n\n\`\`\`mermaid\nflowchart LR\nX-->Y\n\`\`\``;
|
||||||
|
|
||||||
|
let renderCalls = 0;
|
||||||
|
const renderFn: MermaidRenderFn = async (code, outPath) => {
|
||||||
|
renderCalls += 1;
|
||||||
|
await stubRender(code, outPath, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await preprocessMermaidInMarkdown(markdown, {
|
||||||
|
baseDir,
|
||||||
|
renderFn,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(renderCalls, 2, "should render two distinct blocks");
|
||||||
|
assert.equal(result.images.length, 3, "all three blocks produce image entries");
|
||||||
|
const uniqueHashes = new Set(result.images.map((image) => image.hash));
|
||||||
|
assert.equal(uniqueHashes.size, 2);
|
||||||
|
|
||||||
|
const matches = result.markdown.match(/!\[Mermaid diagram\]/g) ?? [];
|
||||||
|
assert.equal(matches.length, 3);
|
||||||
|
assert.ok(!result.markdown.includes("```mermaid"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preprocessMermaidInMarkdown reuses cached files (cached=true when file exists)", async () => {
|
||||||
|
await withTempDir(async (baseDir) => {
|
||||||
|
const markdown = "```mermaid\ngraph TD\nA-->B\n```";
|
||||||
|
|
||||||
|
let renderCalls = 0;
|
||||||
|
const renderFn: MermaidRenderFn = async (code, outPath) => {
|
||||||
|
renderCalls += 1;
|
||||||
|
await stubRender(code, outPath, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = await preprocessMermaidInMarkdown(markdown, { baseDir, renderFn });
|
||||||
|
assert.equal(renderCalls, 1);
|
||||||
|
assert.equal(first.images[0]!.cached, false);
|
||||||
|
|
||||||
|
const second = await preprocessMermaidInMarkdown(markdown, { baseDir, renderFn });
|
||||||
|
assert.equal(renderCalls, 1, "second pass hits cache");
|
||||||
|
assert.equal(second.images[0]!.cached, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preprocessMermaidInMarkdown survives renderFn errors and keeps raw block", async () => {
|
||||||
|
await withTempDir(async (baseDir) => {
|
||||||
|
const markdown = "```mermaid\ninvalid syntax!!\n```\n\nrest";
|
||||||
|
const errors: string[] = [];
|
||||||
|
const failingRender: MermaidRenderFn = async () => {
|
||||||
|
throw new Error("render boom");
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await preprocessMermaidInMarkdown(markdown, {
|
||||||
|
baseDir,
|
||||||
|
renderFn: failingRender,
|
||||||
|
onError: (error) => {
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(errors.length, 1);
|
||||||
|
assert.equal(errors[0], "render boom");
|
||||||
|
assert.equal(result.images.length, 0);
|
||||||
|
assert.ok(result.markdown.includes("```mermaid"));
|
||||||
|
assert.ok(result.markdown.includes("rest"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preprocessMermaidInMarkdown writes PNGs under imgs/.mermaid-cache/", async () => {
|
||||||
|
await withTempDir(async (baseDir) => {
|
||||||
|
const markdown = "```mermaid\ngraph TD\nA-->B\n```";
|
||||||
|
const result = await preprocessMermaidInMarkdown(markdown, {
|
||||||
|
baseDir,
|
||||||
|
renderFn: stubRender,
|
||||||
|
});
|
||||||
|
assert.equal(result.images.length, 1);
|
||||||
|
const image = result.images[0]!;
|
||||||
|
assert.ok(image.localPath.includes(path.join("imgs", ".mermaid-cache")));
|
||||||
|
assert.ok(image.mdRef.includes("imgs/.mermaid-cache/"));
|
||||||
|
assert.ok(fs.existsSync(image.localPath));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MERMAID_VERSION,
|
||||||
|
extractMermaidBlocks,
|
||||||
|
hashMermaidCode,
|
||||||
|
replaceMermaidBlocks,
|
||||||
|
type MermaidBlock,
|
||||||
|
} from "./mermaid-utils.js";
|
||||||
|
|
||||||
|
export interface MermaidRenderOptions {
|
||||||
|
theme?: string;
|
||||||
|
scale?: number;
|
||||||
|
background?: string;
|
||||||
|
minWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MermaidRenderFn = (
|
||||||
|
code: string,
|
||||||
|
outputPath: string,
|
||||||
|
options: MermaidRenderOptions,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
export interface MermaidPreprocessOptions extends MermaidRenderOptions {
|
||||||
|
baseDir: string;
|
||||||
|
imgSubdir?: string;
|
||||||
|
renderFn?: MermaidRenderFn;
|
||||||
|
enabled?: boolean;
|
||||||
|
alt?: string;
|
||||||
|
onError?: (error: unknown, block: MermaidBlock) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MermaidPreprocessedImage {
|
||||||
|
raw: string;
|
||||||
|
code: string;
|
||||||
|
hash: string;
|
||||||
|
localPath: string;
|
||||||
|
mdRef: string;
|
||||||
|
cached: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MermaidPreprocessResult {
|
||||||
|
markdown: string;
|
||||||
|
images: MermaidPreprocessedImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preprocessMermaidInMarkdown(
|
||||||
|
markdown: string,
|
||||||
|
options: MermaidPreprocessOptions,
|
||||||
|
): Promise<MermaidPreprocessResult> {
|
||||||
|
const {
|
||||||
|
baseDir,
|
||||||
|
imgSubdir = "imgs/.mermaid-cache",
|
||||||
|
renderFn,
|
||||||
|
enabled = true,
|
||||||
|
theme,
|
||||||
|
scale,
|
||||||
|
background,
|
||||||
|
minWidth,
|
||||||
|
alt = "Mermaid diagram",
|
||||||
|
onError,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (!enabled || !renderFn) {
|
||||||
|
return { markdown, images: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = extractMermaidBlocks(markdown);
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
return { markdown, images: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheDir = path.resolve(baseDir, imgSubdir);
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
|
||||||
|
const replacements = new Map<string, string>();
|
||||||
|
const images: MermaidPreprocessedImage[] = [];
|
||||||
|
const renderedHashes = new Set<string>();
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const hash = hashMermaidCode({
|
||||||
|
code: block.code,
|
||||||
|
theme,
|
||||||
|
scale,
|
||||||
|
background,
|
||||||
|
minWidth,
|
||||||
|
version: MERMAID_VERSION,
|
||||||
|
});
|
||||||
|
const filename = `mermaid-${hash}.png`;
|
||||||
|
const localPath = path.join(cacheDir, filename);
|
||||||
|
const mdRef = `})`;
|
||||||
|
|
||||||
|
const cached = fs.existsSync(localPath);
|
||||||
|
|
||||||
|
if (!cached && !renderedHashes.has(hash)) {
|
||||||
|
try {
|
||||||
|
await renderFn(block.code, localPath, { theme, scale, background, minWidth });
|
||||||
|
renderedHashes.add(hash);
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error, block);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[mermaid] render failed for block (hash ${hash}): ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(localPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements.set(block.raw, mdRef);
|
||||||
|
images.push({
|
||||||
|
raw: block.raw,
|
||||||
|
code: block.code,
|
||||||
|
hash,
|
||||||
|
localPath,
|
||||||
|
mdRef,
|
||||||
|
cached,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMarkdown = replaceMermaidBlocks(markdown, replacements);
|
||||||
|
return { markdown: newMarkdown, images };
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MERMAID_VERSION,
|
||||||
|
extractMermaidBlocks,
|
||||||
|
hashMermaidCode,
|
||||||
|
replaceMermaidBlocks,
|
||||||
|
} from "./mermaid-utils.ts";
|
||||||
|
|
||||||
|
test("extractMermaidBlocks finds fenced mermaid blocks at the top level", () => {
|
||||||
|
const markdown = `Intro
|
||||||
|
|
||||||
|
\`\`\`mermaid
|
||||||
|
graph TD
|
||||||
|
A --> B
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Outro`;
|
||||||
|
|
||||||
|
const blocks = extractMermaidBlocks(markdown);
|
||||||
|
assert.equal(blocks.length, 1);
|
||||||
|
assert.equal(blocks[0]!.code.trim(), "graph TD\n A --> B");
|
||||||
|
assert.equal(blocks[0]!.infoString, "");
|
||||||
|
assert.ok(blocks[0]!.raw.includes("```mermaid"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("extractMermaidBlocks preserves info-string suffixes", () => {
|
||||||
|
const markdown = "```mermaid theme=dark\nflowchart LR\n A --> B\n```";
|
||||||
|
const blocks = extractMermaidBlocks(markdown);
|
||||||
|
assert.equal(blocks.length, 1);
|
||||||
|
assert.equal(blocks[0]!.infoString, "theme=dark");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("extractMermaidBlocks finds blocks nested inside lists", () => {
|
||||||
|
const markdown = `Steps:
|
||||||
|
|
||||||
|
1. First, render the diagram:
|
||||||
|
|
||||||
|
\`\`\`mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
Alice->>Bob: Hello
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Then do something else.`;
|
||||||
|
|
||||||
|
const blocks = extractMermaidBlocks(markdown);
|
||||||
|
assert.equal(blocks.length, 1);
|
||||||
|
assert.equal(blocks[0]!.code.includes("sequenceDiagram"), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("extractMermaidBlocks ignores non-mermaid fences and empty blocks", () => {
|
||||||
|
const markdown = `\`\`\`ts
|
||||||
|
const x = 1;
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`\`\`mermaid
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`\`\`mermaidsomething
|
||||||
|
not a real lang
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const blocks = extractMermaidBlocks(markdown);
|
||||||
|
assert.equal(blocks.length, 1);
|
||||||
|
assert.equal(blocks[0]!.infoString, "something");
|
||||||
|
assert.equal(blocks[0]!.code.trim(), "not a real lang");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaceMermaidBlocks performs exact string replacement", () => {
|
||||||
|
const markdown = "before\n\n```mermaid\ngraph TD\nA-->B\n```\n\nafter";
|
||||||
|
const blocks = extractMermaidBlocks(markdown);
|
||||||
|
const map = new Map([[blocks[0]!.raw, ""]]);
|
||||||
|
const replaced = replaceMermaidBlocks(markdown, map);
|
||||||
|
assert.equal(replaced, "before\n\n\n\nafter");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaceMermaidBlocks leaves markdown unchanged when no replacements match", () => {
|
||||||
|
const markdown = "hello\n\nworld";
|
||||||
|
const replaced = replaceMermaidBlocks(markdown, new Map([["nope", "x"]]));
|
||||||
|
assert.equal(replaced, markdown);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hashMermaidCode is stable for the same inputs", () => {
|
||||||
|
const a = hashMermaidCode({ code: "graph TD\nA-->B" });
|
||||||
|
const b = hashMermaidCode({ code: "graph TD\nA-->B" });
|
||||||
|
assert.equal(a, b);
|
||||||
|
assert.equal(a.length, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hashMermaidCode defaults to 2x render scale", () => {
|
||||||
|
const implicit = hashMermaidCode({ code: "graph TD\nA-->B" });
|
||||||
|
const explicit = hashMermaidCode({ code: "graph TD\nA-->B", scale: 2 });
|
||||||
|
assert.equal(implicit, explicit);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hashMermaidCode ignores trailing whitespace", () => {
|
||||||
|
const a = hashMermaidCode({ code: "graph TD\nA-->B" });
|
||||||
|
const b = hashMermaidCode({ code: "graph TD\nA-->B \n\n" });
|
||||||
|
assert.equal(a, b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hashMermaidCode reflects theme/scale/background/version changes", () => {
|
||||||
|
const base = hashMermaidCode({ code: "graph TD\nA-->B" });
|
||||||
|
assert.notEqual(base, hashMermaidCode({ code: "graph TD\nA-->B", theme: "dark" }));
|
||||||
|
assert.notEqual(base, hashMermaidCode({ code: "graph TD\nA-->B", scale: 3 }));
|
||||||
|
assert.notEqual(base, hashMermaidCode({ code: "graph TD\nA-->B", minWidth: 860 }));
|
||||||
|
assert.notEqual(base, hashMermaidCode({ code: "graph TD\nA-->B", background: "#000" }));
|
||||||
|
assert.notEqual(base, hashMermaidCode({ code: "graph TD\nA-->B", version: "x.y.z" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("MERMAID_VERSION matches the vendored bundle (10.x)", () => {
|
||||||
|
assert.match(MERMAID_VERSION, /^10\./);
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { Marked, type Tokens } from "marked";
|
||||||
|
|
||||||
|
export const MERMAID_VERSION = "10.9.1";
|
||||||
|
|
||||||
|
export interface MermaidBlock {
|
||||||
|
raw: string;
|
||||||
|
code: string;
|
||||||
|
infoString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HashMermaidInput {
|
||||||
|
code: string;
|
||||||
|
theme?: string;
|
||||||
|
scale?: number;
|
||||||
|
background?: string;
|
||||||
|
minWidth?: number;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractMermaidBlocks(markdown: string): MermaidBlock[] {
|
||||||
|
const blocks: MermaidBlock[] = [];
|
||||||
|
const lexer = new Marked({ breaks: true });
|
||||||
|
const tokens = lexer.lexer(markdown);
|
||||||
|
walkTokens(tokens, (token) => {
|
||||||
|
if (token.type !== "code") return;
|
||||||
|
const codeToken = token as Tokens.Code;
|
||||||
|
const lang = (codeToken.lang ?? "").trim();
|
||||||
|
if (!lang.startsWith("mermaid")) return;
|
||||||
|
const infoString = lang.slice("mermaid".length).trim();
|
||||||
|
const code = codeToken.text ?? "";
|
||||||
|
if (code.trim() === "") return;
|
||||||
|
blocks.push({
|
||||||
|
raw: codeToken.raw,
|
||||||
|
code,
|
||||||
|
infoString,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceMermaidBlocks(
|
||||||
|
markdown: string,
|
||||||
|
replacements: Map<string, string>,
|
||||||
|
): string {
|
||||||
|
let result = markdown;
|
||||||
|
for (const [raw, replacement] of replacements) {
|
||||||
|
if (!raw || replacement === undefined) continue;
|
||||||
|
result = result.split(raw).join(replacement);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashMermaidCode(input: HashMermaidInput): string {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
code: input.code.replace(/\s+$/g, ""),
|
||||||
|
theme: input.theme ?? "default",
|
||||||
|
scale: input.scale ?? 2,
|
||||||
|
minWidth: input.minWidth ?? null,
|
||||||
|
background: input.background ?? "white",
|
||||||
|
version: input.version ?? MERMAID_VERSION,
|
||||||
|
});
|
||||||
|
return createHash("sha256").update(payload).digest("hex").slice(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyToken = { type?: string; tokens?: AnyToken[]; items?: AnyToken[] };
|
||||||
|
|
||||||
|
function walkTokens(tokens: AnyToken[], visit: (token: AnyToken) => void): void {
|
||||||
|
for (const token of tokens) {
|
||||||
|
visit(token);
|
||||||
|
if (Array.isArray(token.tokens)) walkTokens(token.tokens, visit);
|
||||||
|
if (Array.isArray(token.items)) walkTokens(token.items, visit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,7 +119,6 @@ function wrapInlineCode(value: string): string {
|
|||||||
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
||||||
const footnotes: [number, string, string][] = [];
|
const footnotes: [number, string, string][] = [];
|
||||||
let footnoteIndex = 0;
|
let footnoteIndex = 0;
|
||||||
let codeIndex = 0;
|
|
||||||
const listOrderedStack: boolean[] = [];
|
const listOrderedStack: boolean[] = [];
|
||||||
const listCounters: number[] = [];
|
const listCounters: number[] = [];
|
||||||
const isBrowser = typeof window !== "undefined";
|
const isBrowser = typeof window !== "undefined";
|
||||||
@@ -208,20 +207,7 @@ export function initRenderer(opts: IOpts = {}): RendererAPI {
|
|||||||
|
|
||||||
code({ text, lang = "" }: Tokens.Code): string {
|
code({ text, lang = "" }: Tokens.Code): string {
|
||||||
if (lang.startsWith("mermaid")) {
|
if (lang.startsWith("mermaid")) {
|
||||||
if (isBrowser) {
|
return `<pre class="mermaid">${escapeHtml(text)}</pre>`;
|
||||||
clearTimeout(codeIndex as any);
|
|
||||||
codeIndex = setTimeout(async () => {
|
|
||||||
const windowRef = typeof window !== "undefined" ? (window as any) : undefined;
|
|
||||||
if (windowRef && windowRef.mermaid) {
|
|
||||||
const mermaid = windowRef.mermaid;
|
|
||||||
await mermaid.run();
|
|
||||||
} else {
|
|
||||||
const mermaid = await import("mermaid");
|
|
||||||
await mermaid.default.run();
|
|
||||||
}
|
|
||||||
}, 0) as any as number;
|
|
||||||
}
|
|
||||||
return `<pre class="mermaid">${text}</pre>`;
|
|
||||||
}
|
}
|
||||||
const langText = lang.split(" ")[0];
|
const langText = lang.split(" ")[0];
|
||||||
const isLanguageRegistered = hljs.getLanguage(langText);
|
const isLanguageRegistered = hljs.getLanguage(langText);
|
||||||
|
|||||||
Reference in New Issue
Block a user