Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make the examples interactive in the documentation site #5376

Merged
merged 3 commits into from
Mar 11, 2024
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
5 changes: 3 additions & 2 deletions packages/mermaid/scripts/docs.mts
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,12 @@ export function transformMarkdownAst({
node.lang = MERMAID_KEYWORD;
return [node];
} 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
// 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;
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
return vitepress ? [node] : [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
}

// Transform these blocks into block quotes.
Expand Down
45 changes: 9 additions & 36 deletions packages/mermaid/src/docs/.vitepress/mermaid-markdown-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,34 @@ const MermaidExample = async (md: MarkdownRenderer) => {

md.renderer.rules.fence = (tokens, index, options, env, slf) => {
const token = tokens[index];

if (token.info.trim() === 'mermaid-example') {
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 language = token.info.trim();
if (language.startsWith('mermaid')) {
const key = index;
return `
<Suspense>
<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>
<!-- loading state via #fallback slot -->
<template #fallback>
Loading...
</template>
</Suspense>
`;
}
if (token.info.trim() === 'warning') {
} else if (language === 'warning') {
return `<div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>${token.content}}</p></div>`;
}

if (token.info.trim() === 'note') {
} else if (language === 'note') {
return `<div class="tip custom-block"><p class="custom-block-title">NOTE</p><p>${token.content}}</p></div>`;
}

if (token.info.trim() === 'regexp') {
} 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);
}

if (token.info.trim() === 'jison') {
} else if (language === 'jison') {
return `<div class="language-">
<button class="copy"></button>
<span class="lang">jison</span>
Expand Down
72 changes: 67 additions & 5 deletions packages/mermaid/src/docs/.vitepress/theme/Mermaid.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
<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>
</template>

Expand All @@ -15,18 +27,38 @@ const props = defineProps({
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);

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(decodeURIComponent(props.graph))?.length > 0;
const hasImages = /<img([\w\W]+?)>/.exec(code.value)?.length > 0;
if (hasImages)
setTimeout(() => {
let imgElements = document.getElementsByTagName('img');
Expand All @@ -51,16 +83,14 @@ onMounted(async () => {
onUnmounted(() => mut.disconnect());

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 mermaidConfig = {
securityLevel: 'loose',
startOnLoad: false,
theme: hasDarkClass ? 'dark' : 'default',
};

console.log({ mermaidConfig });
let svgCode = await render(props.id, decodeURIComponent(props.graph), mermaidConfig);
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).
Expand All @@ -70,3 +100,35 @@ const renderChart = async () => {
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>
Loading