Skip to content

Commit

Permalink
Merge pull request #89 from zanfee/feat/mermaid-support
Browse files Browse the repository at this point in the history
feat: mermaid support
  • Loading branch information
miggi92 authored Apr 25, 2024
2 parents 7e2a4e4 + 2863c87 commit f78c895
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 5 deletions.
12 changes: 7 additions & 5 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { defineConfig } from 'vitepress'
import { withMermaid } from "vitepress-plugin-mermaid";
import MermaidExample from './mermaid-markdown-all';

import { version } from "../../package.json";

// https://vitepress.dev/reference/site-config
export default
// withMermaid(

defineConfig({
title: "ABAP OData Framework",
lang: 'en-US',
base: '/odata-fw/',
description: "A odata framework for a SAP System. ",

head: [
['link', { rel: 'icon', href: '/favicon.ico' }],
['meta', { property: 'og:type', content: 'website' }],
Expand All @@ -24,6 +22,11 @@ export default
sitemap: {
hostname: "https://miggi92.github.io/odata-fw/"
},
markdown: {
config: async (md) => {
await MermaidExample(md);
}
},
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: nav(),
Expand Down Expand Up @@ -71,8 +74,7 @@ export default
},
}
}
// )
);
);

function nav() {
return [
Expand Down
53 changes: 53 additions & 0 deletions docs/.vitepress/mermaid-markdown-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { MarkdownRenderer } from 'vitepress';

const MermaidExample = async (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;
136 changes: 136 additions & 0 deletions docs/.vitepress/theme/Mermaid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<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>
11 changes: 11 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import DefaultTheme from 'vitepress/theme';
import Mermaid from './Mermaid.vue';
import type { EnhanceAppContext } from 'vitepress';

export default {
...DefaultTheme,
enhanceApp({ app }: EnhanceAppContext) {
// register global components
app.component('Mermaid', Mermaid);
},
};
7 changes: 7 additions & 0 deletions docs/.vitepress/theme/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import mermaid, { type MermaidConfig } from 'mermaid';

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

0 comments on commit f78c895

Please sign in to comment.