render_mermaid.js 2.77 KB
Newer Older
1
import flash from '~/flash';
2
import { sprintf, __ } from '../../locale';
3

4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
//
// Example markup:
//
// <pre class="js-render-mermaid">
//  graph TD;
//    A-- > B;
//    A-- > C;
//    B-- > D;
//    C-- > D;
// </pre>
//

18
// This is an arbitrary number; Can be iterated upon when suitable.
19 20
const MAX_CHAR_LIMIT = 5000;

Phil Hughes's avatar
Phil Hughes committed
21 22
export default function renderMermaid($els) {
  if (!$els.length) return;
23

blackst0ne's avatar
blackst0ne committed
24
  import(/* webpackChunkName: 'mermaid' */ 'mermaid')
25 26 27 28 29 30 31 32
    .then(mermaid => {
      mermaid.initialize({
        // mermaid core options
        mermaid: {
          startOnLoad: false,
        },
        // mermaidAPI options
        theme: 'neutral',
33 34 35
        flowchart: {
          htmlLabels: false,
        },
Stan Hu's avatar
Stan Hu committed
36
        securityLevel: 'strict',
37
      });
38

39 40
      let renderedChars = 0;

41
      $els.each((i, el) => {
42 43
        // 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>');
Douwe Maan's avatar
Douwe Maan committed
44

45 46 47 48 49
        /**
         * Restrict the rendering to a certain amount of character to
         * prevent mermaidjs from hanging up the entire thread and
         * causing a DoS.
         */
50
        if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
51 52 53 54 55 56 57 58 59
          el.textContent = sprintf(
            __(
              'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
            ),
            { charLimit: MAX_CHAR_LIMIT },
          );
          return;
        }

60
        renderedChars += source.length;
61 62
        // Remove any extra spans added by the backend syntax highlighting.
        Object.assign(el, { textContent: source });
63

64 65
        mermaid.init(undefined, el, id => {
          const svg = document.getElementById(id);
Douwe Maan's avatar
Douwe Maan committed
66

Stan Hu's avatar
Stan Hu committed
67 68 69 70 71 72 73 74
          // 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;
          }

75
          svg.classList.add('mermaid');
Douwe Maan's avatar
Douwe Maan committed
76

77 78
          // pre > code > svg
          svg.closest('pre').replaceWith(svg);
Douwe Maan's avatar
Douwe Maan committed
79

80 81 82 83 84 85
          // 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;
Douwe Maan's avatar
Douwe Maan committed
86

87 88
          svg.appendChild(sourceEl);
        });
Douwe Maan's avatar
Douwe Maan committed
89
      });
90 91 92
    })
    .catch(err => {
      flash(`Can't load mermaid module: ${err}`);
Phil Hughes's avatar
Phil Hughes committed
93 94
    });
}