Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-16 15:09:26 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-16 15:09:26 +0300
commitb019dc959ec16b15fe42a680dbd729542ff61537 (patch)
treecde5c3d0582bd7542e1e195d83ad7a50079f4e49 /app/assets/javascripts/content_editor
parentb7b44de429911864686599ef1643baf525bf75ec (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor')
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue51
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue34
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/code_block.vue79
-rw-r--r--app/assets/javascripts/content_editor/extensions/diagram.js22
-rw-r--r--app/assets/javascripts/content_editor/services/asset_resolver.js15
-rw-r--r--app/assets/javascripts/content_editor/services/code_block_language_loader.js6
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js4
8 files changed, 188 insertions, 37 deletions
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
index 4ed89140e5b..6c0ac8e54d2 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
@@ -36,16 +36,19 @@ export default {
directives: {
GlTooltip,
},
- inject: ['tiptapEditor'],
+ inject: ['tiptapEditor', 'contentEditor'],
data() {
return {
codeBlockType: undefined,
- selectedLanguage: {},
filterTerm: '',
filteredLanguages: [],
showCustomLanguageInput: false,
customLanguageType: '',
+
+ selectedLanguage: {},
+ isDiagram: false,
+ showPreview: false,
};
},
watch: {
@@ -61,21 +64,36 @@ export default {
return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type));
},
- updateSelectedLanguage() {
+ async updateCodeBlockInfoToState() {
this.codeBlockType = CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type));
- if (this.codeBlockType) {
- const { language } = this.tiptapEditor.getAttributes(this.codeBlockType);
- this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(language);
- }
+ if (!this.codeBlockType) return;
+
+ const { language, isDiagram, showPreview } = this.tiptapEditor.getAttributes(
+ this.codeBlockType,
+ );
+ this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(
+ language,
+ isDiagram,
+ );
+ this.isDiagram = isDiagram;
+ this.showPreview = showPreview;
},
- copyCodeBlockText() {
+ getCodeBlockText() {
const { view } = this.tiptapEditor;
const { from } = this.tiptapEditor.state.selection;
const node = getParentByTagName(view.domAtPos(from).node, 'pre');
+ return node?.textContent || '';
+ },
- navigator.clipboard.writeText(node?.textContent || '');
+ copyCodeBlockText() {
+ navigator.clipboard.writeText(this.getCodeBlockText());
+ },
+
+ togglePreview() {
+ this.showPreview = !this.showPreview;
+ this.tiptapEditor.commands.updateAttributes(Diagram.name, { showPreview: this.showPreview });
},
async applyLanguage(language) {
@@ -125,7 +143,7 @@ export default {
getReferenceClientRect,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
>
- <editor-state-observer @transaction="updateSelectedLanguage">
+ <editor-state-observer @transaction="updateCodeBlockInfoToState">
<gl-button-group>
<gl-dropdown
category="tertiary"
@@ -228,6 +246,19 @@ export default {
@click="copyCodeBlockText"
/>
<gl-button
+ v-if="isDiagram"
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ :class="{ active: showPreview }"
+ data-testid="preview-diagram"
+ :aria-label="__('Preview diagram')"
+ :title="__('Preview diagram')"
+ icon="eye"
+ @click="togglePreview"
+ />
+ <gl-button
v-gl-tooltip
variant="default"
category="tertiary"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
new file mode 100644
index 00000000000..ecde593147c
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor'],
+ methods: {
+ execute(contentType, attrs) {
+ this.tiptapEditor.chain().focus().setNode(contentType, attrs).run();
+
+ this.$emit('execute', { contentType });
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown size="small" category="tertiary" icon="plus">
+ <gl-dropdown-item @click="execute('diagram', { language: 'mermaid' })">
+ {{ __('Mermaid diagram') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="execute('diagram', { language: 'plantuml' })">
+ {{ __('PlantUML diagram') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="execute('horizontalRule')">
+ {{ __('Horizontal rule') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index 19e150a4da9..b652e634b0c 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -5,6 +5,7 @@ import ToolbarImageButton from './toolbar_image_button.vue';
import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
+import ToolbarMoreDropdown from './toolbar_more_dropdown.vue';
export default {
components: {
@@ -13,6 +14,7 @@ export default {
ToolbarLinkButton,
ToolbarTableButton,
ToolbarImageButton,
+ ToolbarMoreDropdown,
},
methods: {
trackToolbarControlExecution({ contentType, value }) {
@@ -117,16 +119,8 @@ export default {
:label="__('Add a collapsible section')"
@execute="trackToolbarControlExecution"
/>
- <toolbar-button
- data-testid="horizontal-rule"
- content-type="horizontalRule"
- icon-name="dash"
- class="gl-mx-2"
- editor-command="setHorizontalRule"
- :label="__('Add a horizontal rule')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-table-button @execute="trackToolbarControlExecution" />
+ <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" />
+ <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index 647f0798364..81f9b1f0af5 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -1,15 +1,26 @@
<script>
+import { debounce } from 'lodash';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue';
import codeBlockLanguageLoader from '../../services/code_block_language_loader';
+import EditorStateObserver from '../editor_state_observer.vue';
export default {
name: 'CodeBlock',
components: {
NodeViewWrapper,
NodeViewContent,
+ EditorStateObserver,
+ SandboxedMermaid,
},
+ inject: ['contentEditor'],
props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
node: {
type: Object,
required: true,
@@ -18,13 +29,48 @@ export default {
type: Function,
required: true,
},
+ selected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ diagramUrl: '',
+ diagramSource: '',
+ };
},
async mounted() {
+ this.updateDiagramPreview = debounce(
+ this.updateDiagramPreview,
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ );
+
const lang = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(this.node.attrs.language);
await codeBlockLanguageLoader.loadLanguage(lang.syntax);
this.updateAttributes({ language: this.node.attrs.language });
},
+ methods: {
+ async updateDiagramPreview() {
+ if (!this.node.attrs.showPreview) {
+ this.diagramSource = '';
+ return;
+ }
+
+ if (!this.editor.isActive('diagram')) return;
+
+ this.diagramSource = this.$refs.nodeViewContent.$el.textContent;
+
+ if (this.node.attrs.language !== 'mermaid') {
+ this.diagramUrl = await this.contentEditor.renderDiagram(
+ this.diagramSource,
+ this.node.attrs.language,
+ );
+ }
+ },
+ },
i18n: {
frontmatter: __('frontmatter'),
},
@@ -32,17 +78,26 @@ export default {
};
</script>
<template>
- <node-view-wrapper
- :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`"
- as="pre"
- >
- <span
- v-if="node.attrs.isFrontmatter"
- data-testid="frontmatter-label"
- class="gl-absolute gl-top-0 gl-right-3"
- contenteditable="false"
- >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span
+ <editor-state-observer @transaction="updateDiagramPreview">
+ <node-view-wrapper
+ :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`"
+ as="pre"
>
- <node-view-content as="code" />
- </node-view-wrapper>
+ <div
+ v-if="node.attrs.showPreview"
+ class="gl-mt-n3! gl-ml-n4! gl-mr-n4! gl-mb-3 gl-bg-white! gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ >
+ <sandboxed-mermaid v-if="node.attrs.language === 'mermaid'" :source="diagramSource" />
+ <img v-else ref="diagramContainer" :src="diagramUrl" />
+ </div>
+ <span
+ v-if="node.attrs.isFrontmatter"
+ data-testid="frontmatter-label"
+ class="gl-absolute gl-top-0 gl-right-3"
+ contenteditable="false"
+ >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span
+ >
+ <node-view-content ref="nodeViewContent" as="code" />
+ </node-view-wrapper>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js
index f9dfeb92e9a..c59ca8a28b8 100644
--- a/app/assets/javascripts/content_editor/extensions/diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/diagram.js
@@ -1,6 +1,10 @@
+import { textblockTypeInputRule } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+import languageLoader from '../services/code_block_language_loader';
import CodeBlockHighlight from './code_block_highlight';
+const backtickInputRegex = /^```(mermaid|plantuml)[\s\n]$/;
+
export default CodeBlockHighlight.extend({
name: 'diagram',
@@ -17,6 +21,9 @@ export default CodeBlockHighlight.extend({
isDiagram: {
default: true,
},
+ showPreview: {
+ default: true,
+ },
};
},
@@ -24,6 +31,11 @@ export default CodeBlockHighlight.extend({
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
+ tag: 'pre[lang="mermaid"]',
+ getAttrs: () => ({ language: 'mermaid' }),
+ },
+ {
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: '[data-diagram]',
getContent(element, schema) {
const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', ''));
@@ -54,6 +66,14 @@ export default CodeBlockHighlight.extend({
},
addInputRules() {
- return [];
+ const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {};
+
+ return [
+ textblockTypeInputRule({
+ find: backtickInputRegex,
+ type: this.type,
+ getAttributes,
+ }),
+ ];
},
});
diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js
index 942457b9664..c0bcddbe58d 100644
--- a/app/assets/javascripts/content_editor/services/asset_resolver.js
+++ b/app/assets/javascripts/content_editor/services/asset_resolver.js
@@ -1,13 +1,24 @@
import { memoize } from 'lodash';
+const parser = new DOMParser();
+
export default ({ renderMarkdown }) => ({
resolveUrl: memoize(async (canonicalSrc) => {
const html = await renderMarkdown(`[link](${canonicalSrc})`);
if (!html) return canonicalSrc;
- const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html');
-
return body.querySelector('a').getAttribute('href');
}),
+
+ renderDiagram: memoize(async (code, language) => {
+ const backticks = '`'.repeat(4);
+ const html = await renderMarkdown(`${backticks}${language}\n${code}\n${backticks}`);
+
+ const { body } = parser.parseFromString(html, 'text/html');
+ const img = body.querySelector('img');
+ if (!img) return '';
+
+ return img.dataset.src || img.getAttribute('src');
+ }),
});
diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
index fa0549d4183..b7cf1bb087c 100644
--- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js
+++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
@@ -8,7 +8,7 @@ const codeBlockLanguageLoader = {
allLanguages: CODE_BLOCK_LANGUAGES,
- findOrCreateLanguageBySyntax(value) {
+ findOrCreateLanguageBySyntax(value, isDiagram) {
const lowercaseValue = value?.toLowerCase() || 'plaintext';
return (
this.allLanguages.find(
@@ -16,7 +16,9 @@ const codeBlockLanguageLoader = {
syntax === lowercaseValue || variants?.toLowerCase().split(', ').includes(lowercaseValue),
) || {
syntax: lowercaseValue,
- label: sprintf(__(`Custom (%{language})`), { language: lowercaseValue }),
+ label: sprintf(isDiagram ? __(`Diagram (%{language})`) : __(`Custom (%{language})`), {
+ language: lowercaseValue,
+ }),
}
);
},
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 52dacb84153..06757e7a280 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -52,6 +52,10 @@ export class ContentEditor {
return this._assetResolver.resolveUrl(canonicalSrc);
}
+ renderDiagram(code, language) {
+ return this._assetResolver.renderDiagram(code, language);
+ }
+
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _eventHub: eventHub } = this;
const { doc, tr } = editor.state;