diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-05 21:08:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-05 21:08:56 +0300 |
commit | 092e41f5660a356a6cebc26cd0274b531d8c70c6 (patch) | |
tree | e696f9ad230bc5d5a7222fb690e3699a1a8abe78 /app/assets/javascripts/behaviors | |
parent | 8ec882085e734458ffe0fff8e2e4b72bc3871419 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/behaviors')
-rw-r--r-- | app/assets/javascripts/behaviors/markdown/nodes/code_block.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/behaviors/markdown/render_mermaid.js | 231 |
2 files changed, 1 insertions, 232 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js index 0ff59779e7d..b862d111de7 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js @@ -37,7 +37,7 @@ export default () => ({ attrs: { lang: 'math' }, }, // Matches HTML generated by Banzai::Filter::MermaidFilter, - // after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js + // after being transformed by app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js { tag: 'svg.mermaid', preserveWhitespace: 'full', diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js deleted file mode 100644 index 2df0f7387fb..00000000000 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ /dev/null @@ -1,231 +0,0 @@ -import $ from 'jquery'; -import { once, countBy } from 'lodash'; -import createFlash from '~/flash'; -import { darkModeEnabled } from '~/lib/utils/color_utils'; -import { __, sprintf } from '~/locale'; -import { unrestrictedPages } from './constants'; - -// 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> -// - -// This is an arbitrary number; Can be iterated upon when suitable. -const MAX_CHAR_LIMIT = 2000; -// Max # of mermaid blocks that can be rendered in a page. -const MAX_MERMAID_BLOCK_LIMIT = 50; -// Max # of `&` allowed in Chaining of links syntax -const MAX_CHAINING_OF_LINKS_LIMIT = 30; -// Keep a map of mermaid blocks we've already rendered. -const elsProcessingMap = new WeakMap(); -let renderedMermaidBlocks = 0; - -let mermaidModule = {}; - -export function initMermaid(mermaid) { - let theme = 'neutral'; - - if (darkModeEnabled()) { - theme = 'dark'; - } - - mermaid.initialize({ - // mermaid core options - mermaid: { - startOnLoad: false, - }, - // mermaidAPI options - theme, - flowchart: { - useMaxWidth: true, - htmlLabels: true, - }, - secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'htmlLabels'], - securityLevel: 'strict', - }); - - return mermaid; -} - -function importMermaidModule() { - return import(/* webpackChunkName: 'mermaid' */ 'mermaid') - .then(({ default: mermaid }) => { - mermaidModule = initMermaid(mermaid); - }) - .catch((err) => { - createFlash({ - message: sprintf(__("Can't load mermaid module: %{err}"), { err }), - }); - // eslint-disable-next-line no-console - console.error(err); - }); -} - -function shouldLazyLoadMermaidBlock(source) { - /** - * If source contains `&`, which means that it might - * contain Chaining of links a new syntax in Mermaid. - */ - if (countBy(source)['&'] > MAX_CHAINING_OF_LINKS_LIMIT) { - return true; - } - - return false; -} - -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; - - const pageName = document.querySelector('body').dataset.page; - - // A diagram may have been truncated in search results which will cause errors, so abort the render. - if (pageName === 'search:show') return; - - importMermaidModule() - .then(() => { - let renderedChars = 0; - - $els.each((i, el) => { - // Skipping all the elements which we've already queued in requestIdleCallback - if (elsProcessingMap.has(el)) { - return; - } - - const { source } = fixElementSource(el); - /** - * Restrict the rendering to a certain amount of character - * and mermaid blocks to prevent mermaidjs from hanging - * up the entire thread and causing a DoS. - */ - if ( - !unrestrictedPages.includes(pageName) && - ((source && source.length > MAX_CHAR_LIMIT) || - renderedChars > MAX_CHAR_LIMIT || - renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT || - shouldLazyLoadMermaidBlock(source)) - ) { - 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-confirm btn-md gl-button">Display</button> - </div> - </div> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</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; - renderedMermaidBlocks += 1; - - const requestId = window.requestIdleCallback(() => { - renderMermaidEl(el); - }); - - elsProcessingMap.set(el, requestId); - }); - }) - .catch((err) => { - createFlash({ - message: 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; - - const visibleMermaids = $els.filter(function filter() { - return $(this).closest('details').length === 0 && $(this).is(':visible'); - }); - - renderMermaids(visibleMermaids); - - $els.closest('details').one('toggle', function toggle() { - if (this.open) { - renderMermaids($(this).find('.js-render-mermaid')); - } - }); - - hookLazyRenderMermaidEvent(); -} |