diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-21 12:09:11 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-21 12:09:11 +0300 |
commit | 49abdb108a4d3c3f2ef9b27c7c4dcde43da1016a (patch) | |
tree | 31dee66af9f14c3bd320c349810d877d96fd66cf /app/assets/javascripts/vue_shared/components/markdown | |
parent | 760dc7721406a82bbea06f3512a4e270d0bb3f0a (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/markdown')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js | 89 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue | 120 |
2 files changed, 209 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js new file mode 100644 index 00000000000..0ba6a44d153 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js @@ -0,0 +1,89 @@ +import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue'; + +export default { + title: 'vue_shared/non_gfm_markdown', + component: Markdown, + parameters: { + docs: { + description: { + component: ` +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. +`, + }, + }, + }, +}; + +const Template = (args, { argTypes }) => ({ + components: { Markdown }, + props: Object.keys(argTypes), + template: '<markdown v-bind="$props" />', +}); + +const textWithCodeblock = ` +#### Here is the text with the code block. + +\`\`\`javascript +function sayHi(name) { + console.log('Hi ' + name || 'Mark'); +} +\`\`\` + +It *can* have **formatting** as well +`; + +export const OneCodeBlock = Template.bind({}); +OneCodeBlock.args = { markdown: textWithCodeblock }; + +const textWithMultipleCodeBlocks = ` +#### Here is the text with the code block. + +\`\`\`javascript +function sayHi(name) { + console.log('Hi ' + name || 'Mark'); +} +\`\`\` + +Note that the copy buttons are appearing independently + +\`\`\`yaml +stages: + - build + - test + - deploy +\`\`\` +`; + +export const MultipleCodeBlocks = Template.bind({}); +MultipleCodeBlocks.args = { markdown: textWithMultipleCodeBlocks }; + +const textUndefinedLanguage = ` +#### Here is the code block with no language provided. + +\`\`\` +function sayHi(name) { + console.log('Hi ' + name || 'Mark'); +} +\`\`\` +`; + +export const UndefinedLanguage = Template.bind({}); +UndefinedLanguage.args = { markdown: textUndefinedLanguage }; + +const textCodeOneLiner = ` +#### Here is the text with the one-liner code block. + +Note that copy button rendering is ok. + +\`\`\`javascript +const foo = 'bar'; +\`\`\` +`; + +export const CodeOneLiner = Template.bind({}); +CodeOneLiner.args = { markdown: textCodeOneLiner }; 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> |