mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 14:29:25 +02:00
Merge pull request #5376 from mermaid-js/sidv/editableExamples
feat: Make the examples interactive in the documentation site
This commit is contained in:
@@ -252,11 +252,12 @@ export function transformMarkdownAst({
|
|||||||
node.lang = MERMAID_KEYWORD;
|
node.lang = MERMAID_KEYWORD;
|
||||||
return [node];
|
return [node];
|
||||||
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
||||||
// Return 2 nodes:
|
// If Vitepress, return only the original node with the language now set to 'mermaid-example' (will be rendered using custom renderer)
|
||||||
|
// Else Return 2 nodes:
|
||||||
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
||||||
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
||||||
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
||||||
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
return vitepress ? [node] : [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform these blocks into block quotes.
|
// Transform these blocks into block quotes.
|
||||||
|
@@ -9,35 +9,15 @@ const MermaidExample = async (md: MarkdownRenderer) => {
|
|||||||
|
|
||||||
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
||||||
const token = tokens[index];
|
const token = tokens[index];
|
||||||
|
const language = token.info.trim();
|
||||||
if (token.info.trim() === 'mermaid-example') {
|
if (language.startsWith('mermaid')) {
|
||||||
if (!md.options.highlight) {
|
|
||||||
// this function is always created by vitepress, but we need to check it
|
|
||||||
// anyway to make TypeScript happy
|
|
||||||
throw new Error(
|
|
||||||
'Missing MarkdownIt highlight function (should be automatically created by vitepress'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// doing ```mermaid-example {line-numbers=5 highlight=14-17} is not supported
|
|
||||||
const langAttrs = '';
|
|
||||||
return `
|
|
||||||
<h5>Code:</h5>
|
|
||||||
<div class="language-mermaid">
|
|
||||||
<button class="copy"></button>
|
|
||||||
<span class="lang">mermaid</span>
|
|
||||||
${
|
|
||||||
// html is pre-escaped by the highlight function
|
|
||||||
// (it also adds `v-pre` to ignore Vue template syntax)
|
|
||||||
md.options.highlight(token.content, 'mermaid', langAttrs)
|
|
||||||
}
|
|
||||||
</div>`;
|
|
||||||
} else if (token.info.trim() === 'mermaid') {
|
|
||||||
const key = index;
|
const key = index;
|
||||||
return `
|
return `
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<template #default>
|
<template #default>
|
||||||
<Mermaid id="mermaid-${key}" graph="${encodeURIComponent(token.content)}"></Mermaid>
|
<Mermaid id="mermaid-${key}" :showCode="${
|
||||||
|
language === 'mermaid-example'
|
||||||
|
}" graph="${encodeURIComponent(token.content)}"></Mermaid>
|
||||||
</template>
|
</template>
|
||||||
<!-- loading state via #fallback slot -->
|
<!-- loading state via #fallback slot -->
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
@@ -45,25 +25,18 @@ const MermaidExample = async (md: MarkdownRenderer) => {
|
|||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
`;
|
`;
|
||||||
}
|
} else if (language === 'warning') {
|
||||||
if (token.info.trim() === 'warning') {
|
|
||||||
return `<div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>${token.content}}</p></div>`;
|
return `<div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>${token.content}}</p></div>`;
|
||||||
}
|
} else if (language === 'note') {
|
||||||
|
|
||||||
if (token.info.trim() === 'note') {
|
|
||||||
return `<div class="tip custom-block"><p class="custom-block-title">NOTE</p><p>${token.content}}</p></div>`;
|
return `<div class="tip custom-block"><p class="custom-block-title">NOTE</p><p>${token.content}}</p></div>`;
|
||||||
}
|
} else if (language === 'regexp') {
|
||||||
|
|
||||||
if (token.info.trim() === 'regexp') {
|
|
||||||
// shiki doesn't yet support regexp code blocks, but the javascript
|
// shiki doesn't yet support regexp code blocks, but the javascript
|
||||||
// one still makes RegExes look good
|
// one still makes RegExes look good
|
||||||
token.info = 'javascript';
|
token.info = 'javascript';
|
||||||
// use trimEnd to move trailing `\n` outside if the JavaScript regex `/` block
|
// use trimEnd to move trailing `\n` outside if the JavaScript regex `/` block
|
||||||
token.content = `/${token.content.trimEnd()}/\n`;
|
token.content = `/${token.content.trimEnd()}/\n`;
|
||||||
return defaultRenderer(tokens, index, options, env, slf);
|
return defaultRenderer(tokens, index, options, env, slf);
|
||||||
}
|
} else if (language === 'jison') {
|
||||||
|
|
||||||
if (token.info.trim() === 'jison') {
|
|
||||||
return `<div class="language-">
|
return `<div class="language-">
|
||||||
<button class="copy"></button>
|
<button class="copy"></button>
|
||||||
<span class="lang">jison</span>
|
<span class="lang">jison</span>
|
||||||
|
@@ -1,4 +1,16 @@
|
|||||||
<template>
|
<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="plaintext-only" @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>
|
<div v-html="svg"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -15,18 +27,38 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showCode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const svg = ref('');
|
const svg = ref('');
|
||||||
|
const code = ref(decodeURIComponent(props.graph));
|
||||||
|
const ctrlSymbol = ref(navigator.platform.includes('Mac') ? '⌘' : 'Ctrl');
|
||||||
|
const editableContent = ref(null);
|
||||||
|
|
||||||
let mut = null;
|
let mut = null;
|
||||||
|
|
||||||
|
const updateCode = (event) => {
|
||||||
|
code.value = event.target.innerText;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
mut = new MutationObserver(() => renderChart());
|
mut = new MutationObserver(() => renderChart());
|
||||||
mut.observe(document.documentElement, { attributes: true });
|
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();
|
await renderChart();
|
||||||
|
|
||||||
//refresh images on first render
|
//refresh images on first render
|
||||||
const hasImages = /<img([\w\W]+?)>/.exec(decodeURIComponent(props.graph))?.length > 0;
|
const hasImages = /<img([\w\W]+?)>/.exec(code.value)?.length > 0;
|
||||||
if (hasImages)
|
if (hasImages)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let imgElements = document.getElementsByTagName('img');
|
let imgElements = document.getElementsByTagName('img');
|
||||||
@@ -51,16 +83,14 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => mut.disconnect());
|
onUnmounted(() => mut.disconnect());
|
||||||
|
|
||||||
const renderChart = async () => {
|
const renderChart = async () => {
|
||||||
console.log('rendering chart' + props.id + props.graph);
|
console.log('rendering chart' + props.id + code.value);
|
||||||
const hasDarkClass = document.documentElement.classList.contains('dark');
|
const hasDarkClass = document.documentElement.classList.contains('dark');
|
||||||
const mermaidConfig = {
|
const mermaidConfig = {
|
||||||
securityLevel: 'loose',
|
securityLevel: 'loose',
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
theme: hasDarkClass ? 'dark' : 'default',
|
theme: hasDarkClass ? 'dark' : 'default',
|
||||||
};
|
};
|
||||||
|
let svgCode = await render(props.id, code.value, mermaidConfig);
|
||||||
console.log({ mermaidConfig });
|
|
||||||
let svgCode = await render(props.id, decodeURIComponent(props.graph), mermaidConfig);
|
|
||||||
// This is a hack to force v-html to re-render, otherwise the diagram disappears
|
// This is a hack to force v-html to re-render, otherwise the diagram disappears
|
||||||
// when **switching themes** or **reloading the page**.
|
// when **switching themes** or **reloading the page**.
|
||||||
// The cause is that the diagram is deleted during rendering (out of Vue's knowledge).
|
// The cause is that the diagram is deleted during rendering (out of Vue's knowledge).
|
||||||
@@ -70,3 +100,35 @@ const renderChart = async () => {
|
|||||||
svg.value = `${svgCode} <span style="display: none">${salt}</span>`;
|
svg.value = `${svgCode} <span style="display: none">${salt}</span>`;
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
||||||
|
Reference in New Issue
Block a user