Commit fec31b71 authored by Rajat Jain's avatar Rajat Jain

Add functionality to render individual mermaids

With 5000 chars as a hard limit, there are many cases where we'd want
to render individual mermaid diagrams. With this we're adding a render
button which will render the nearest mermaid svg.
parent 062dcdf9
import flash from '~/flash'; import flash from '~/flash';
import $ from 'jquery'; import $ from 'jquery';
import { sprintf, __ } from '../../locale'; import { __, sprintf } from '~/locale';
import { once } from 'lodash';
// Renders diagrams and flowcharts from text using Mermaid in any element with the // Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class. // `js-render-mermaid` class.
...@@ -18,14 +19,10 @@ import { sprintf, __ } from '../../locale'; ...@@ -18,14 +19,10 @@ import { sprintf, __ } from '../../locale';
// This is an arbitrary number; Can be iterated upon when suitable. // This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000; const MAX_CHAR_LIMIT = 5000;
let mermaidModule = {};
function renderMermaids($els) { function importMermaidModule() {
if (!$els.length) return; return import(/* webpackChunkName: 'mermaid' */ 'mermaid')
// A diagram may have been truncated in search results which will cause errors, so abort the render.
if (document.querySelector('body').dataset.page === 'search:show') return;
import(/* webpackChunkName: 'mermaid' */ 'mermaid')
.then(mermaid => { .then(mermaid => {
mermaid.initialize({ mermaid.initialize({
// mermaid core options // mermaid core options
...@@ -41,63 +38,127 @@ function renderMermaids($els) { ...@@ -41,63 +38,127 @@ function renderMermaids($els) {
securityLevel: 'strict', securityLevel: 'strict',
}); });
mermaidModule = mermaid;
return mermaid;
})
.catch(err => {
flash(sprintf(__("Can't load mermaid module: %{err}"), { err }));
// eslint-disable-next-line no-console
console.error(err);
});
}
function fixElementSource(el) {
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
return { source };
}
function renderMermaidEl(el) {
mermaidModule.init(undefined, el, id => {
const source = el.textContent;
const svg = document.getElementById(id);
// As of https://github.com/knsv/mermaid/commit/57b780a0d,
// Mermaid will make two init callbacks:one to initialize the
// flow charts, and another to initialize the Gannt charts.
// Guard against an error caused by double initialization.
if (svg.classList.contains('mermaid')) {
return;
}
svg.classList.add('mermaid');
// pre > code > svg
svg.closest('pre').replaceWith(svg);
// We need to add the original source into the DOM to allow Copy-as-GFM
// to access it.
const sourceEl = document.createElement('text');
sourceEl.classList.add('source');
sourceEl.setAttribute('display', 'none');
sourceEl.textContent = source;
svg.appendChild(sourceEl);
});
}
function renderMermaids($els) {
if (!$els.length) return;
// A diagram may have been truncated in search results which will cause errors, so abort the render.
if (document.querySelector('body').dataset.page === 'search:show') return;
importMermaidModule()
.then(() => {
let renderedChars = 0; let renderedChars = 0;
$els.each((i, el) => { $els.each((i, el) => {
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly. const { source } = fixElementSource(el);
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
/** /**
* Restrict the rendering to a certain amount of character to * Restrict the rendering to a certain amount of character to
* prevent mermaidjs from hanging up the entire thread and * prevent mermaidjs from hanging up the entire thread and
* causing a DoS. * causing a DoS.
*/ */
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) { if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
el.textContent = sprintf( const html = `
__( <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.', <div>
), <div class="display-flex">
{ charLimit: MAX_CHAR_LIMIT }, <div>${__(
); 'Warning: Displaying this diagram might cause performance issues on this page.',
)}</div>
<div class="gl-alert-actions">
<button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md new-gl-button">Display</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
`;
const $parent = $(el).parent();
if (!$parent.hasClass('lazy-alert-shown')) {
$parent.after(html);
$parent.addClass('lazy-alert-shown');
}
return; return;
} }
renderedChars += source.length; renderedChars += source.length;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
mermaid.init(undefined, el, id => {
const svg = document.getElementById(id);
// As of https://github.com/knsv/mermaid/commit/57b780a0d,
// Mermaid will make two init callbacks:one to initialize the
// flow charts, and another to initialize the Gannt charts.
// Guard against an error caused by double initialization.
if (svg.classList.contains('mermaid')) {
return;
}
svg.classList.add('mermaid');
// pre > code > svg
svg.closest('pre').replaceWith(svg);
// We need to add the original source into the DOM to allow Copy-as-GFM renderMermaidEl(el);
// to access it.
const sourceEl = document.createElement('text');
sourceEl.classList.add('source');
sourceEl.setAttribute('display', 'none');
sourceEl.textContent = source;
svg.appendChild(sourceEl);
});
}); });
}) })
.catch(err => { .catch(err => {
flash(`Can't load mermaid module: ${err}`); flash(sprintf(__('Encountered an error while rendering: %{err}'), { err }));
// eslint-disable-next-line no-console
console.error(err);
}); });
} }
const hookLazyRenderMermaidEvent = once(() => {
$(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
const parent = $(this).closest('.js-lazy-render-mermaid-container');
const pre = parent.prev();
const el = pre.find('.js-render-mermaid');
parent.remove();
renderMermaidEl(el);
});
});
export default function renderMermaid($els) { export default function renderMermaid($els) {
if (!$els.length) return; if (!$els.length) return;
...@@ -112,4 +173,6 @@ export default function renderMermaid($els) { ...@@ -112,4 +173,6 @@ export default function renderMermaid($els) {
renderMermaids($(this).find('.js-render-mermaid')); renderMermaids($(this).find('.js-render-mermaid'));
} }
}); });
hookLazyRenderMermaidEvent();
} }
---
title: Add functionality to render individual mermaids
merge_request: 26564
author:
type: changed
...@@ -3256,6 +3256,9 @@ msgstr "" ...@@ -3256,6 +3256,9 @@ msgstr ""
msgid "Can't find variable: ZiteReader" msgid "Can't find variable: ZiteReader"
msgstr "" msgstr ""
msgid "Can't load mermaid module: %{err}"
msgstr ""
msgid "Can't remove group members without group managed account" msgid "Can't remove group members without group managed account"
msgstr "" msgstr ""
...@@ -3304,9 +3307,6 @@ msgstr "" ...@@ -3304,9 +3307,6 @@ msgstr ""
msgid "Cannot refer to a group milestone by an internal id!" msgid "Cannot refer to a group milestone by an internal id!"
msgstr "" msgstr ""
msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded."
msgstr ""
msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above." msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above."
msgstr "" msgstr ""
...@@ -7492,6 +7492,9 @@ msgstr "" ...@@ -7492,6 +7492,9 @@ msgstr ""
msgid "Enabling this will only make licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public." msgid "Enabling this will only make licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public."
msgstr "" msgstr ""
msgid "Encountered an error while rendering: %{err}"
msgstr ""
msgid "End date" msgid "End date"
msgstr "" msgstr ""
...@@ -22341,6 +22344,9 @@ msgstr "" ...@@ -22341,6 +22344,9 @@ msgstr ""
msgid "Warning:" msgid "Warning:"
msgstr "" msgstr ""
msgid "Warning: Displaying this diagram might cause performance issues on this page."
msgstr ""
msgid "We could not determine the path to remove the epic" msgid "We could not determine the path to remove the epic"
msgstr "" msgstr ""
......
...@@ -38,7 +38,9 @@ describe 'Mermaid rendering', :js do ...@@ -38,7 +38,9 @@ describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
expected = '<text><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>' wait_for_requests
expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
expect(page.html.scan(expected).count).to be(4) expect(page.html.scan(expected).count).to be(4)
end end
...@@ -121,4 +123,40 @@ describe 'Mermaid rendering', :js do ...@@ -121,4 +123,40 @@ describe 'Mermaid rendering', :js do
expect(svg[:width].to_i).to eq(100) expect(svg[:width].to_i).to eq(100)
expect(svg[:height].to_i).to eq(0) expect(svg[:height].to_i).to eq(0)
end end
it 'display button when diagram exceeds length', :js do
graph_edges = "A-->B;B-->A;" * 420
description = <<~MERMAID
```mermaid
graph LR
#{graph_edges}
```
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
page.within('.description') do
expect(page).not_to have_selector('svg')
expect(page).to have_selector('pre.mermaid')
expect(page).to have_selector('.lazy-alert-shown')
expect(page).to have_selector('.js-lazy-render-mermaid-container')
end
wait_for_requests
find('.js-lazy-render-mermaid').click
page.within('.description') do
expect(page).to have_selector('svg')
expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
end
end
end end
...@@ -7766,10 +7766,10 @@ merge2@^1.2.3: ...@@ -7766,10 +7766,10 @@ merge2@^1.2.3:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==
mermaid@^8.4.5: mermaid@^8.4.8:
version "8.4.5" version "8.4.8"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.5.tgz#48d5722cbc72be2ad01002795835d7ca1b48e000" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.8.tgz#8adcfdbc505d6bca52df167cff690427c9727b60"
integrity sha512-oJWgZBtT2rvAdmqHvKjDwb3tOut1+ksfgDdZrVhhNcdzNibzGPjCsmMPpVXjkFYzKZCVunIbAkfxltSuaGIhaw== integrity sha512-sumTNBFwMX7oMQgogdr3NhgTeQOiwcEsm23rQ4KHGW7tpmvMwER1S+1gjCSSnqlmM/zw7Ga7oesYCYicKboRwQ==
dependencies: dependencies:
"@braintree/sanitize-url" "^3.1.0" "@braintree/sanitize-url" "^3.1.0"
crypto-random-string "^3.0.1" crypto-random-string "^3.0.1"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment