Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"axios": "^1.13.1",
"bytemd": "^1.22.0",
"github-markdown-css": "^5.8.1",
"mermaid": "^10.9.4",
"rehype-slug": "^5.1.0",
"vue": "^3.5.22"
},
Expand Down
498 changes: 498 additions & 0 deletions console/pnpm-lock.yaml

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions console/src/components/bytemd.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<script setup lang="ts">
import { Editor } from "@bytemd/vue-next";
import gfm from "@bytemd/plugin-gfm";
import { markdownTable, pluginSlug, vim } from "../plugins";
import {
markdownTable,
mermaidPlugin,
pluginSlug,
renderMermaidInHtml,
vim,
} from "../plugins";
import {
getProcessor,
type BytemdEditorContext,
Expand All @@ -19,6 +25,7 @@ import "../styles/main.scss";
const basePlugins: BytemdPlugin[] = [
gfm(),
pluginSlug(),
mermaidPlugin(),
math(),
breaks(),
{
Expand All @@ -43,6 +50,7 @@ const createPlugins = (useVimKeymap = false): BytemdPlugin[] => {
};

const plugins = ref<BytemdPlugin[]>(createPlugins());
let contentRenderVersion = 0;

const VIM_KEYMAP_NAME = "vim";

Expand Down Expand Up @@ -93,11 +101,16 @@ onMounted(async () => {

watch(
() => props.raw,
(value) => {
async (value) => {
const version = ++contentRenderVersion;
const processor = getProcessor({ plugins: plugins.value }).processSync(
value
);
emit("update:content", processor.toString());
const content = await renderMermaidInHtml(processor.toString());

if (version === contentRenderVersion) {
emit("update:content", content);
}
},
{
immediate: true,
Expand Down
1 change: 1 addition & 0 deletions console/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BytemdPlugin, BytemdEditorContext } from "bytemd";
import rehypeSlug from "rehype-slug";
import useVim from "codemirror-ssr/keymap/vim";
export { markdownTable } from "./markdown-table";
export { mermaidPlugin, renderMermaidInHtml } from "./mermaid";

export function pluginSlug(): BytemdPlugin {
return {
Expand Down
227 changes: 227 additions & 0 deletions console/src/plugins/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import type { BytemdPlugin } from "bytemd";

const MERMAID_SELECTOR = "pre > code.language-mermaid";
const MERMAID_THEME_FALLBACKS = {
background: "white",
primaryColor: "#ECECFF",
primaryTextColor: "#131300",
primaryBorderColor: "hsl(240, 60%, 86.2745098039%)",
secondaryColor: "#ffffde",
secondaryTextColor: "#000021",
secondaryBorderColor: "hsl(60, 60%, 83.5294117647%)",
tertiaryColor: "hsl(80, 100%, 96.2745098039%)",
tertiaryTextColor: "rgb(6.3333333334, 0, 19.0000000001)",
tertiaryBorderColor: "hsl(80, 60%, 86.2745098039%)",
noteBkgColor: "#fff5ad",
noteTextColor: "black",
noteBorderColor: "#aaaa33",
lineColor: "#333333",
textColor: "#333",
mainBkg: "#ECECFF",
errorBkgColor: "#552222",
errorTextColor: "#552222",
nodeBorder: "#9370DB",
clusterBkg: "#ffffde",
clusterBorder: "#aaaa33",
defaultLinkColor: "#333333",
titleColor: "#333",
edgeLabelBackground: "#e8e8e8",
actorBkg: "#ECECFF",
actorBorder: "hsl(259.6261682243, 59.7765363128%, 87.9019607843%)",
actorTextColor: "black",
actorLineColor: "grey",
signalColor: "#333",
signalTextColor: "#333",
labelBoxBkgColor: "#ECECFF",
labelBoxBorderColor: "hsl(259.6261682243, 59.7765363128%, 87.9019607843%)",
labelTextColor: "black",
loopTextColor: "black",
activationBorderColor: "#666",
activationBkgColor: "#f4f4f4",
sequenceNumberColor: "white",
altBackground: "#f0f0f0",
classText: "#131300",
cScale0: "hsl(240, 100%, 76.2745098039%)",
cScale1: "hsl(60, 100%, 73.5294117647%)",
cScale2: "hsl(80, 100%, 76.2745098039%)",
cScale3: "hsl(270, 100%, 76.2745098039%)",
cScale4: "hsl(300, 100%, 76.2745098039%)",
cScale5: "hsl(330, 100%, 76.2745098039%)",
cScale6: "hsl(0, 100%, 76.2745098039%)",
cScale7: "hsl(30, 100%, 76.2745098039%)",
cScale8: "hsl(90, 100%, 76.2745098039%)",
cScale9: "hsl(150, 100%, 76.2745098039%)",
cScale10: "hsl(180, 100%, 76.2745098039%)",
cScale11: "hsl(210, 100%, 76.2745098039%)",
cScaleInv0: "hsl(60, 100%, 86.2745098039%)",
cScaleInv1: "hsl(240, 100%, 83.5294117647%)",
cScaleInv2: "hsl(260, 100%, 86.2745098039%)",
cScaleInv3: "hsl(90, 100%, 86.2745098039%)",
cScaleInv4: "hsl(120, 100%, 86.2745098039%)",
cScaleInv5: "hsl(150, 100%, 86.2745098039%)",
cScaleInv6: "hsl(180, 100%, 86.2745098039%)",
cScaleInv7: "hsl(210, 100%, 86.2745098039%)",
cScaleInv8: "hsl(270, 100%, 86.2745098039%)",
cScaleInv9: "hsl(330, 100%, 86.2745098039%)",
cScaleInv10: "hsl(0, 100%, 86.2745098039%)",
cScaleInv11: "hsl(30, 100%, 86.2745098039%)",
cScaleLabel0: "#ffffff",
cScaleLabel1: "black",
cScaleLabel2: "black",
cScaleLabel3: "#ffffff",
cScaleLabel4: "black",
cScaleLabel5: "black",
cScaleLabel6: "black",
cScaleLabel7: "black",
cScaleLabel8: "black",
cScaleLabel9: "black",
cScaleLabel10: "black",
cScaleLabel11: "black",
};
const MERMAID_THEME_VARIABLES = {
...MERMAID_THEME_FALLBACKS,
};
const MERMAID_THEME_COLOR_REPLACEMENTS = Object.entries(
MERMAID_THEME_FALLBACKS
).map(([key, color]) => {
return [color, `var(--bytemd-mermaid-${toKebabCase(key)}, ${color})`] as [
string,
string
];
});

let renderId = 0;
let initialized = false;
let mermaidPromise: Promise<typeof import("mermaid").default> | undefined;

async function getMermaid() {
if (!mermaidPromise) {
mermaidPromise = import("mermaid").then((module) => module.default);
}

return mermaidPromise;
}

async function initializeMermaid() {
const mermaid = await getMermaid();

if (initialized) {
return mermaid;
}

mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: "base",
themeVariables: MERMAID_THEME_VARIABLES,
});
initialized = true;
return mermaid;
}

async function renderMermaidSvg(definition: string): Promise<string> {
const mermaid = await initializeMermaid();

const id = `bytemd-mermaid-${Date.now()}-${renderId++}`;
const { svg } = await mermaid.render(id, definition);
return replaceMermaidThemeColors(svg);
}

function replaceMermaidThemeColors(svg: string) {
return MERMAID_THEME_COLOR_REPLACEMENTS.reduce(
(result, [color, variable]) => {
return result.replace(new RegExp(escapeRegExp(color), "gi"), variable);
},
svg
);
}

function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function toKebabCase(value: string) {
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
}

async function renderMermaidElement(
codeElement: HTMLElement,
shouldRender: () => boolean = () => true
) {
const container = codeElement.closest("pre");

if (!container) {
return;
}

const definition = codeElement.textContent || "";
const wrapper = document.createElement("div");
wrapper.className = "bytemd-mermaid";

try {
wrapper.innerHTML = await renderMermaidSvg(definition);
} catch {
wrapper.classList.add("bytemd-mermaid-error");
wrapper.textContent = "Mermaid render failed";
}

if (shouldRender()) {
container.replaceWith(wrapper);
}
}

export async function renderMermaidInHtml(html: string): Promise<string> {
if (!html.includes("language-mermaid")) {
return html;
}

const htmlDocument = new DOMParser().parseFromString(html, "text/html");
const codeElements = Array.from(
htmlDocument.body.querySelectorAll<HTMLElement>(MERMAID_SELECTOR)
);

await Promise.all(
codeElements.map(async (codeElement) => {
const container = codeElement.closest("pre");

if (!container) {
return;
}

try {
const wrapper = htmlDocument.createElement("div");
wrapper.className = "bytemd-mermaid";
wrapper.innerHTML = await renderMermaidSvg(
codeElement.textContent || ""
);
container.replaceWith(wrapper);
} catch {
// Keep the original code block when rendering fails.
}
})
);

return htmlDocument.body.innerHTML;
}

export function mermaidPlugin(): BytemdPlugin {
return {
viewerEffect({ markdownBody }) {
let disposed = false;
const codeElements = Array.from(
markdownBody.querySelectorAll<HTMLElement>(MERMAID_SELECTOR)
);

codeElements.forEach((codeElement) => {
renderMermaidElement(
codeElement,
() => !disposed && markdownBody.contains(codeElement)
);
});

return () => {
disposed = true;
};
},
};
}
98 changes: 97 additions & 1 deletion console/src/styles/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,100 @@
}
.bytemd .markdown-body ol {
list-style: decimal;
}
}
.bytemd-mermaid {
--bytemd-mermaid-background: white;
--bytemd-mermaid-primary-color: #ececff;
--bytemd-mermaid-primary-text-color: #131300;
--bytemd-mermaid-primary-border-color: hsl(240, 60%, 86.2745098039%);
--bytemd-mermaid-secondary-color: #ffffde;
--bytemd-mermaid-secondary-text-color: #000021;
--bytemd-mermaid-secondary-border-color: hsl(60, 60%, 83.5294117647%);
--bytemd-mermaid-tertiary-color: hsl(80, 100%, 96.2745098039%);
--bytemd-mermaid-tertiary-text-color: rgb(6.3333333334, 0, 19.0000000001);
--bytemd-mermaid-tertiary-border-color: hsl(80, 60%, 86.2745098039%);
--bytemd-mermaid-note-bkg-color: #fff5ad;
--bytemd-mermaid-note-text-color: black;
--bytemd-mermaid-note-border-color: #aaaa33;
--bytemd-mermaid-line-color: #333333;
--bytemd-mermaid-text-color: #333;
--bytemd-mermaid-main-bkg: #ececff;
--bytemd-mermaid-error-bkg-color: #552222;
--bytemd-mermaid-error-text-color: #552222;
--bytemd-mermaid-node-border: #9370db;
--bytemd-mermaid-cluster-bkg: #ffffde;
--bytemd-mermaid-cluster-border: #aaaa33;
--bytemd-mermaid-default-link-color: #333333;
--bytemd-mermaid-title-color: #333;
--bytemd-mermaid-edge-label-background: #e8e8e8;
--bytemd-mermaid-actor-bkg: #ececff;
--bytemd-mermaid-actor-border: hsl(
259.6261682243,
59.7765363128%,
87.9019607843%
);
--bytemd-mermaid-actor-text-color: black;
--bytemd-mermaid-actor-line-color: grey;
--bytemd-mermaid-signal-color: #333;
--bytemd-mermaid-signal-text-color: #333;
--bytemd-mermaid-label-box-bkg-color: #ececff;
--bytemd-mermaid-label-box-border-color: hsl(
259.6261682243,
59.7765363128%,
87.9019607843%
);
--bytemd-mermaid-label-text-color: black;
--bytemd-mermaid-loop-text-color: black;
--bytemd-mermaid-activation-border-color: #666;
--bytemd-mermaid-activation-bkg-color: #f4f4f4;
--bytemd-mermaid-sequence-number-color: white;
--bytemd-mermaid-alt-background: #f0f0f0;
--bytemd-mermaid-class-text: #131300;
--bytemd-mermaid-c-scale0: hsl(240, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale1: hsl(60, 100%, 73.5294117647%);
--bytemd-mermaid-c-scale2: hsl(80, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale3: hsl(270, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale4: hsl(300, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale5: hsl(330, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale6: hsl(0, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale7: hsl(30, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale8: hsl(90, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale9: hsl(150, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale10: hsl(180, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale11: hsl(210, 100%, 76.2745098039%);
--bytemd-mermaid-c-scale-inv0: hsl(60, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv1: hsl(240, 100%, 83.5294117647%);
--bytemd-mermaid-c-scale-inv2: hsl(260, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv3: hsl(90, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv4: hsl(120, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv5: hsl(150, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv6: hsl(180, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv7: hsl(210, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv8: hsl(270, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv9: hsl(330, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv10: hsl(0, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-inv11: hsl(30, 100%, 86.2745098039%);
--bytemd-mermaid-c-scale-label0: #ffffff;
--bytemd-mermaid-c-scale-label1: black;
--bytemd-mermaid-c-scale-label2: black;
--bytemd-mermaid-c-scale-label3: #ffffff;
--bytemd-mermaid-c-scale-label4: black;
--bytemd-mermaid-c-scale-label5: black;
--bytemd-mermaid-c-scale-label6: black;
--bytemd-mermaid-c-scale-label7: black;
--bytemd-mermaid-c-scale-label8: black;
--bytemd-mermaid-c-scale-label9: black;
--bytemd-mermaid-c-scale-label10: black;
--bytemd-mermaid-c-scale-label11: black;
margin: 1rem 0;
overflow-x: auto;
text-align: center;
}
.bytemd-mermaid svg {
max-width: 100%;
height: auto;
}
.bytemd-mermaid-error {
color: #b91c1c;
text-align: left;
}
Loading
Loading