Skip to content

安装

参考 https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs/.vitepress

依赖

npp add mermaid @mermaid-js/layout-elk @mermaid-js/layout-tidy-tree @mermaid-js/mermaid-zenuml

参考

    "@mermaid-js/layout-elk": "^0.2.1",
    "@mermaid-js/layout-tidy-tree": "^0.2.1",
    "@mermaid-js/mermaid-zenuml": "^0.2.2",
    "mermaid": "^11.14.0",

添加代码

.vitepress/mermaid-markdown-all.ts

ts
import type { MarkdownRenderer } from "vitepress";

const MermaidExample = (md: MarkdownRenderer) => {
  const defaultRenderer = md.renderer.rules.fence;

  if (!defaultRenderer) {
    throw new Error("defaultRenderer is undefined");
  }

  md.renderer.rules.fence = (tokens, index, options, env, slf) => {
    const token = tokens[index];
    const language = token.info.trim();
    if (language.startsWith("mermaid")) {
      const key = index;
      return `
      <Suspense> 
      <template #default>
      <Mermaid id="mermaid-${key}" :showCode="${
        language === "mermaid-example"
      }" graph="${encodeURIComponent(token.content)}"></Mermaid>
      </template>
        <!-- loading state via #fallback slot -->
        <template #fallback>
          Loading...
        </template>
      </Suspense>
`;
    } else if (language === "warning") {
      return `<div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>${token.content}}</p></div>`;
    } else if (language === "note") {
      return `<div class="tip custom-block"><p class="custom-block-title">NOTE</p><p>${token.content}}</p></div>`;
    } else if (language === "regexp") {
      // shiki doesn't yet support regexp code blocks, but the javascript
      // one still makes RegExes look good
      token.info = "javascript";
      // use trimEnd to move trailing `\n` outside if the JavaScript regex `/` block
      token.content = `/${token.content.trimEnd()}/\n`;
      return defaultRenderer(tokens, index, options, env, slf);
    } else if (language === "jison") {
      return `<div class="language-">
      <button class="copy"></button>
      <span class="lang">jison</span>
      <pre>
      <code>${token.content.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code>
      </pre>
      </div>`;
    }

    return defaultRenderer(tokens, index, options, env, slf);
  };
};

export default MermaidExample;

.vitepress/theme/Mermaid.vue

vue
<template>
  <div v-if="props.showCode">
    <h5>Code:</h5>
    <div class="language-mermaid">
      <button class="copy"></button>
      <span class="lang">mermaid</span>
      <pre><code :contenteditable="contentEditable" @input="updateCode"  @keydown.meta.enter="renderChart" @keydown.ctrl.enter="renderChart" ref="editableContent" class="editable-code"></code></pre>
      <div class="buttons-container">
        <span>{{ ctrlSymbol }} + Enter</span><span>|</span>
        <button @click="renderChart">Run ▶</button>
      </div>
    </div>
  </div>
  <div v-html="svg"></div>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { render } from "./mermaid";

const props = defineProps({
  graph: {
    type: String,
    required: true,
  },
  id: {
    type: String,
    required: true,
  },
  showCode: {
    type: Boolean,
    default: true,
  },
});

const svg = ref("");
const code = ref(decodeURIComponent(props.graph));
const ctrlSymbol = ref(navigator.platform.includes("Mac") ? "⌘" : "Ctrl");
const editableContent = ref(null);
const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
const contentEditable = ref(isFirefox ? "true" : "plaintext-only");

let mut = null;

const updateCode = (event) => {
  code.value = event.target.innerText;
};

onMounted(async () => {
  mut = new MutationObserver(() => renderChart());
  mut.observe(document.documentElement, { attributes: true });

  if (editableContent.value) {
    // Set the initial value of the contenteditable element
    // We cannot bind using `{{ code }}` because it will rerender the whole component
    // when the value changes, shifting the cursor when enter is used
    editableContent.value.textContent = code.value;
  }

  await renderChart();

  //refresh images on first render
  const hasImages = /<img([\w\W]+?)>/.exec(code.value)?.length > 0;
  if (hasImages)
    setTimeout(() => {
      let imgElements = document.getElementsByTagName("img");
      let imgs = Array.from(imgElements);
      if (imgs.length) {
        Promise.all(
          imgs
            .filter((img) => !img.complete)
            .map(
              (img) =>
                new Promise((resolve) => {
                  img.onload = img.onerror = resolve;
                }),
            ),
        ).then(() => {
          renderChart();
        });
      }
    }, 100);
});

onUnmounted(() => mut.disconnect());

const renderChart = async () => {
  console.log("rendering chart" + props.id + code.value);
  const hasDarkClass = document.documentElement.classList.contains("dark");
  const mermaidConfig = {
    securityLevel: "loose",
    startOnLoad: false,
    theme: hasDarkClass ? "dark" : "default",
  };
  let svgCode = await render(props.id, code.value, mermaidConfig);
  // This is a hack to force v-html to re-render, otherwise the diagram disappears
  // when **switching themes** or **reloading the page**.
  // The cause is that the diagram is deleted during rendering (out of Vue's knowledge).
  // Because svgCode does NOT change, v-html does not re-render.
  // This is not required for all diagrams, but it is required for c4c, mindmap and zenuml.
  const salt = Math.random().toString(36).substring(7);
  svg.value = `${svgCode} <span style="display: none">${salt}</span>`;
};
</script>

<style>
.editable-code:focus {
  outline: none; /* Removes the default focus indicator */
}

.buttons-container {
  position: absolute;
  bottom: 0;
  right: 0;
  z-index: 1;
  padding: 0.5rem;
  display: flex;
  gap: 0.5rem;
}

.buttons-container > span {
  cursor: default;
  opacity: 0.5;
  font-size: 0.8rem;
}

.buttons-container > button {
  color: #007bffbf;
  font-weight: bold;
  cursor: pointer;
}

.buttons-container > button:hover {
  color: #007bff;
}
</style>

.vitepress/theme/mermaid.ts

ts
import mermaid, { type MermaidConfig } from "mermaid";
import zenuml from "@mermaid-js/mermaid-zenuml";
import tidyTreeLayout from "@mermaid-js/layout-tidy-tree";
import layouts from "@mermaid-js/layout-elk";

const init = Promise.all([
  mermaid.registerExternalDiagrams([zenuml]),
  mermaid.registerLayoutLoaders(layouts),
  mermaid.registerLayoutLoaders(tidyTreeLayout),
]);
mermaid.registerIconPacks([
  {
    name: "logos",
    loader: () =>
      fetch("https://unpkg.com/@iconify-json/logos/icons.json").then((res) =>
        res.json(),
      ),
  },
]);

export const render = async (
  id: string,
  code: string,
  config: MermaidConfig,
): Promise<string> => {
  await init;
  mermaid.initialize(config);
  const { svg } = await mermaid.render(id, code);
  return svg;
};

修改配置

.vitepress/theme/index.ts

ts
import Mermaid from "./Mermaid.vue";

export default {
  // ...
  enhanceApp({ app }) {
    // ...

    app.component("Mermaid", Mermaid);

    // ...
  },
};

.vitepress/config.ts

ts
import { defineConfig, type MarkdownOptions } from "vitepress";
import MermaidExample from "./mermaid-markdown-all";

const allMarkdownTransformers: MarkdownOptions = {
  // 这里还有很多其他 markdown 配置
  lineNumbers: true,

  config: (md) => {
    MermaidExample(md);
  },
};

export default defineConfig({
  markdown: allMarkdownTransformers, // 非常重要!!!
});