summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/behaviors/markdown/render_mermaid.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/behaviors/markdown/render_mermaid.js')
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js153
1 files changed, 108 insertions, 45 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index b5e17a0587d..fe63ebd470d 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -1,6 +1,7 @@
import flash from '~/flash';
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
// `js-render-mermaid` class.
@@ -18,14 +19,10 @@ import { sprintf, __ } from '../../locale';
// This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000;
+let mermaidModule = {};
-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;
-
- import(/* webpackChunkName: 'mermaid' */ 'mermaid')
+function importMermaidModule() {
+ return import(/* webpackChunkName: 'mermaid' */ 'mermaid')
.then(mermaid => {
mermaid.initialize({
// mermaid core options
@@ -41,63 +38,127 @@ function renderMermaids($els) {
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;
$els.each((i, 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>');
-
+ const { source } = fixElementSource(el);
/**
* Restrict the rendering to a certain amount of character to
* prevent mermaidjs from hanging up the entire thread and
* causing a DoS.
*/
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
- el.textContent = sprintf(
- __(
- 'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
- ),
- { charLimit: MAX_CHAR_LIMIT },
- );
+ 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">
+ <div>
+ <div class="display-flex">
+ <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;
}
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
- // to access it.
- const sourceEl = document.createElement('text');
- sourceEl.classList.add('source');
- sourceEl.setAttribute('display', 'none');
- sourceEl.textContent = source;
-
- svg.appendChild(sourceEl);
- });
+ renderMermaidEl(el);
});
})
.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) {
if (!$els.length) return;
@@ -112,4 +173,6 @@ export default function renderMermaid($els) {
renderMermaids($(this).find('.js-render-mermaid'));
}
});
+
+ hookLazyRenderMermaidEvent();
}