diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue new file mode 100644 index 00000000000..814e59681d0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue @@ -0,0 +1,120 @@ +<script> +/* +This component is designed to render the markdown, which is **not** the GitLab Flavored Markdown. + +It renders the code snippets the same way GitLab Flavored Markdown code snippets are rendered +respecting the user's preferred color scheme and featuring a copy-code button. + +This component can be used to render client-side markdown that doesn't have GitLab-specific markdown elements such as issue links. +*/ +import { marked } from 'marked'; +import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { sanitize } from '~/lib/dompurify'; +import { markdownConfig } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; + +export default { + components: { + CodeBlockHighlighted, + ModalCopyButton, + }, + directives: { + SafeHtml, + }, + props: { + markdown: { + type: String, + required: true, + }, + }, + data() { + return { + hoverMap: {}, + }; + }, + computed: { + markdownBlocks() { + // we use lexer https://marked.js.org/using_pro#lexer + // to get an array of tokens that marked npm module uses. + // We will use these tokens to override rendering of some of them + // with our vue components + const tokens = marked.lexer(this.markdown); + + // since we only want to differentiate between code and non-code blocks + // we want non-code blocks merged together so that the markdown parser could render + // them according to the markdown rules. + // This way we introduce minimum extra wrapper mark-up + const flattenedTokens = []; + + for (const token of tokens) { + const lastFlattenedToken = flattenedTokens[flattenedTokens.length - 1]; + if (token.type === 'code') { + flattenedTokens.push(token); + } else if (lastFlattenedToken?.type === 'markdown') { + lastFlattenedToken.raw += token.raw; + } else { + flattenedTokens.push({ type: 'markdown', raw: token.raw }); + } + } + + return flattenedTokens; + }, + }, + methods: { + getSafeHtml(markdown) { + return sanitize(marked.parse(markdown), markdownConfig); + }, + setHoverOn(key) { + this.hoverMap = { ...this.hoverMap, [key]: true }; + }, + setHoverOff(key) { + this.hoverMap = { ...this.hoverMap, [key]: false }; + }, + isLastElement(index) { + return index === this.markdownBlocks.length - 1; + }, + }, + safeHtmlConfig: { + ADD_TAGS: ['use', 'gl-emoji', 'copy-code'], + }, + i18n: { + copyCodeTitle: __('Copy code'), + }, + fallbackLanguage: 'text', +}; +</script> +<template> + <div> + <template v-for="(block, index) in markdownBlocks"> + <div + v-if="block.type === 'code'" + :key="`code-${index}`" + :class="{ 'gl-relative': true, 'gl-mb-4': !isLastElement(index) }" + data-testid="code-block-wrapper" + @mouseenter="setHoverOn(`code-${index}`)" + @mouseleave="setHoverOff(`code-${index}`)" + > + <modal-copy-button + v-if="hoverMap[`code-${index}`]" + :title="$options.i18n.copyCodeTitle" + :text="block.text" + class="gl-absolute gl-top-3 gl-right-3 gl-z-index-1 gl-transition-duration-medium" + /> + <code-block-highlighted + class="gl-border gl-rounded-0! gl-p-4 gl-mb-0 gl-overflow-y-auto" + :language="block.lang || $options.fallbackLanguage" + :code="block.text" + /> + </div> + <div + v-else + :key="`text-${index}`" + v-safe-html:[$options.safeHtmlConfig]="getSafeHtml(block.raw)" + :class="{ 'non-gfm-markdown-block': true, 'gl-mb-4': !isLastElement(index) }" + data-testid="non-code-markdown" + ></div> + </template> + </div> +</template> |