summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-07 18:10:23 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-07 18:10:23 +0000
commit21e144f387bc4d77f6128ee87549daf174467518 (patch)
treea1f7ca7673e157e9175b527fc435b480c974645a /app/assets/javascripts/vue_shared
parent79f98200f84590af39cf1af7f57f6e8ba89d2bb6 (diff)
downloadgitlab-ce-21e144f387bc4d77f6128ee87549daf174467518.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js57
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue134
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue91
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue150
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js42
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js109
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js116
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js63
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js23
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js38
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue31
21 files changed, 0 insertions, 1058 deletions
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
deleted file mode 100644
index cbb30baa488..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { __ } from '~/locale';
-
-export const CUSTOM_EVENTS = {
- openAddImageModal: 'gl_openAddImageModal',
- openInsertVideoModal: 'gl_openInsertVideoModal',
-};
-
-export const YOUTUBE_URL = 'https://www.youtube.com';
-
-export const YOUTUBE_EMBED_URL = `${YOUTUBE_URL}/embed`;
-
-export const ALLOWED_VIDEO_ORIGINS = [YOUTUBE_URL];
-
-/* eslint-disable @gitlab/require-i18n-strings */
-export const TOOLBAR_ITEM_CONFIGS = [
- { icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
- { icon: 'bold', command: 'Bold', tooltip: __('Add bold text') },
- { icon: 'italic', command: 'Italic', tooltip: __('Add italic text') },
- { icon: 'strikethrough', command: 'Strike', tooltip: __('Add strikethrough text') },
- { isDivider: true },
- { icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') },
- { icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') },
- { isDivider: true },
- { icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') },
- { icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') },
- { icon: 'list-task', command: 'Task', tooltip: __('Add a task list') },
- { icon: 'list-indent', command: 'Indent', tooltip: __('Indent') },
- { icon: 'list-outdent', command: 'Outdent', tooltip: __('Outdent') },
- { isDivider: true },
- { icon: 'dash', command: 'HR', tooltip: __('Add a line') },
- { icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') },
- { icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
- { icon: 'live-preview', event: CUSTOM_EVENTS.openInsertVideoModal, tooltip: __('Insert video') },
- { isDivider: true },
- { icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
- { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
-];
-
-export const EDITOR_TYPES = {
- markdown: 'markdown',
- wysiwyg: 'wysiwyg',
-};
-
-export const EDITOR_HEIGHT = '100%';
-
-export const EDITOR_PREVIEW_STYLE = 'horizontal';
-
-export const IMAGE_TABS = { UPLOAD_TAB: 0, URL_TAB: 1 };
-
-export const MAX_FILE_SIZE = 2097152; // 2Mb
-
-export const VIDEO_ATTRIBUTES = {
- width: '560',
- height: '315',
- frameBorder: '0',
- allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
-};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
deleted file mode 100644
index 82060d2e4ad..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<script>
-import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
-import { isSafeURL, joinPaths } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import { IMAGE_TABS } from '../../constants';
-import UploadImageTab from './upload_image_tab.vue';
-
-export default {
- components: {
- UploadImageTab,
- GlModal,
- GlFormGroup,
- GlFormInput,
- GlTabs,
- GlTab,
- },
- props: {
- imageRoot: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- file: null,
- urlError: null,
- imageUrl: null,
- description: null,
- tabIndex: IMAGE_TABS.UPLOAD_TAB,
- uploadImageTab: null,
- };
- },
- modalTitle: __('Image details'),
- okTitle: __('Insert image'),
- urlTabTitle: __('Link to an image'),
- urlLabel: __('Image URL'),
- descriptionLabel: __('Description'),
- uploadTabTitle: __('Upload an image'),
- computed: {
- altText() {
- return this.description;
- },
- },
- methods: {
- show() {
- this.file = null;
- this.urlError = null;
- this.imageUrl = null;
- this.description = null;
- this.tabIndex = IMAGE_TABS.UPLOAD_TAB;
-
- this.$refs.modal.show();
- },
- onOk(event) {
- if (this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
- this.submitFile(event);
- return;
- }
- this.submitURL(event);
- },
- setFile(file) {
- this.file = file;
- },
- submitFile(event) {
- const { file, altText } = this;
- const { uploadImageTab } = this.$refs;
-
- uploadImageTab.validateFile();
-
- if (uploadImageTab.fileError) {
- event.preventDefault();
- return;
- }
-
- const imageUrl = joinPaths(this.imageRoot, file.name);
-
- this.$emit('addImage', { imageUrl, file, altText: altText || file.name });
- },
- submitURL(event) {
- if (!this.validateUrl()) {
- event.preventDefault();
- return;
- }
-
- const { imageUrl, altText } = this;
-
- this.$emit('addImage', { imageUrl, altText: altText || imageUrl });
- },
- validateUrl() {
- if (!isSafeURL(this.imageUrl)) {
- this.urlError = __('Please provide a valid URL');
- this.$refs.urlInput.$el.focus();
- return false;
- }
-
- return true;
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ref="modal"
- modal-id="add-image-modal"
- :title="$options.modalTitle"
- :ok-title="$options.okTitle"
- @ok="onOk"
- >
- <gl-tabs v-model="tabIndex">
- <!-- Upload file Tab -->
- <gl-tab :title="$options.uploadTabTitle">
- <upload-image-tab ref="uploadImageTab" @input="setFile" />
- </gl-tab>
-
- <!-- By URL Tab -->
- <gl-tab :title="$options.urlTabTitle">
- <gl-form-group
- class="gl-mt-5 gl-mb-3"
- :label="$options.urlLabel"
- label-for="url-input"
- :state="!Boolean(urlError)"
- :invalid-feedback="urlError"
- >
- <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
- </gl-form-group>
- </gl-tab>
- </gl-tabs>
-
- <!-- Description Input -->
- <gl-form-group :label="$options.descriptionLabel" label-for="description-input">
- <gl-form-input id="description-input" ref="descriptionInput" v-model="description" />
- </gl-form-group>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
deleted file mode 100644
index 9baa7f286d7..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<script>
-import { GlFormGroup } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { MAX_FILE_SIZE } from '../../constants';
-
-export default {
- components: {
- GlFormGroup,
- },
- data() {
- return {
- file: null,
- fileError: null,
- };
- },
- fileLabel: __('Select file'),
- methods: {
- onInput(event) {
- [this.file] = event.target.files;
-
- this.validateFile();
-
- if (!this.fileError) {
- this.$emit('input', this.file);
- }
- },
- validateFile() {
- this.fileError = null;
-
- if (!this.file) {
- this.fileError = __('Please choose a file');
- } else if (this.file.size > MAX_FILE_SIZE) {
- this.fileError = __('Maximum file size is 2MB. Please select a smaller file.');
- }
- },
- },
-};
-</script>
-<template>
- <gl-form-group
- class="gl-mt-5 gl-mb-3"
- :label="$options.fileLabel"
- label-for="file-input"
- :state="!Boolean(fileError)"
- :invalid-feedback="fileError"
- >
- <input
- id="file-input"
- ref="fileInput"
- class="gl-mt-3 gl-mb-2"
- type="file"
- accept="image/*"
- @input="onInput"
- />
- </gl-form-group>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue
deleted file mode 100644
index 99bb2080610..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import { GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
-import { isSafeURL } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import { YOUTUBE_URL, YOUTUBE_EMBED_URL } from '../constants';
-
-export default {
- components: {
- GlModal,
- GlFormGroup,
- GlFormInput,
- GlSprintf,
- },
- data() {
- return {
- url: null,
- urlError: null,
- description: __(
- 'If the YouTube URL is https://www.youtube.com/watch?v=0t1DgySidms then the video ID is %{id}',
- ),
- };
- },
- modalTitle: __('Insert a video'),
- okTitle: __('Insert video'),
- label: __('YouTube URL or ID'),
- methods: {
- show() {
- this.urlError = null;
- this.url = null;
-
- this.$refs.modal.show();
- },
- onPrimary(event) {
- this.submitURL(event);
- },
- submitURL(event) {
- const url = this.generateUrl();
-
- if (!url) {
- event.preventDefault();
- return;
- }
-
- this.$emit('insertVideo', url);
- },
- generateUrl() {
- let { url } = this;
- const reYouTubeId = /^[A-z0-9]*$/;
- const reYouTubeUrl = RegExp(`${YOUTUBE_URL}/(embed/|watch\\?v=)([A-z0-9]+)`);
-
- if (reYouTubeId.test(url)) {
- url = `${YOUTUBE_EMBED_URL}/${url}`;
- } else if (reYouTubeUrl.test(url)) {
- url = `${YOUTUBE_EMBED_URL}/${reYouTubeUrl.exec(url)[2]}`;
- }
-
- if (!isSafeURL(url) || !reYouTubeUrl.test(url)) {
- this.urlError = __('Please provide a valid YouTube URL or ID');
- this.$refs.urlInput.$el.focus();
- return null;
- }
-
- return url;
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ref="modal"
- size="sm"
- modal-id="insert-video-modal"
- :title="$options.modalTitle"
- :ok-title="$options.okTitle"
- @primary="onPrimary"
- >
- <gl-form-group
- :label="$options.label"
- label-for="video-modal-url-input"
- :state="!Boolean(urlError)"
- :invalid-feedback="urlError"
- >
- <gl-form-input id="video-modal-url-input" ref="urlInput" v-model="url" />
- <gl-sprintf slot="description" :message="description" class="text-gl-muted">
- <template #id>
- <strong>{{ __('0t1DgySidms') }}</strong>
- </template>
- </gl-sprintf>
- </gl-form-group>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
deleted file mode 100644
index 8988dab85d2..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<script>
-import 'codemirror/lib/codemirror.css';
-import '@toast-ui/editor/dist/toastui-editor.css';
-
-import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
-import AddImageModal from './modals/add_image/add_image_modal.vue';
-import InsertVideoModal from './modals/insert_video_modal.vue';
-
-import {
- registerHTMLToMarkdownRenderer,
- getEditorOptions,
- addCustomEventListener,
- removeCustomEventListener,
- addImage,
- getMarkdown,
- insertVideo,
-} from './services/editor_service';
-
-export default {
- components: {
- ToastEditor: () =>
- import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then(
- (toast) => toast.Editor,
- ),
- AddImageModal,
- InsertVideoModal,
- },
- props: {
- content: {
- type: String,
- required: true,
- },
- options: {
- type: Object,
- required: false,
- default: () => null,
- },
- initialEditType: {
- type: String,
- required: false,
- default: EDITOR_TYPES.wysiwyg,
- },
- height: {
- type: String,
- required: false,
- default: EDITOR_HEIGHT,
- },
- previewStyle: {
- type: String,
- required: false,
- default: EDITOR_PREVIEW_STYLE,
- },
- imageRoot: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- editorApi: null,
- previousMode: null,
- };
- },
- computed: {
- editorInstance() {
- return this.$refs.editor;
- },
- customEventListeners() {
- return [
- { event: CUSTOM_EVENTS.openAddImageModal, listener: this.onOpenAddImageModal },
- { event: CUSTOM_EVENTS.openInsertVideoModal, listener: this.onOpenInsertVideoModal },
- ];
- },
- },
- created() {
- this.editorOptions = getEditorOptions(this.options);
- },
- beforeDestroy() {
- this.removeListeners();
- },
- methods: {
- addListeners(editorApi) {
- this.customEventListeners.forEach(({ event, listener }) => {
- addCustomEventListener(editorApi, event, listener);
- });
-
- editorApi.eventManager.listen('changeMode', this.onChangeMode);
- },
- removeListeners() {
- this.customEventListeners.forEach(({ event, listener }) => {
- removeCustomEventListener(this.editorApi, event, listener);
- });
-
- this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode);
- },
- resetInitialValue(newVal) {
- this.editorInstance.invoke('setMarkdown', newVal);
- },
- onContentChanged() {
- this.$emit('input', getMarkdown(this.editorInstance));
- },
- onLoad(editorApi) {
- this.editorApi = editorApi;
-
- registerHTMLToMarkdownRenderer(editorApi);
-
- this.addListeners(editorApi);
-
- this.$emit('load', { formattedMarkdown: editorApi.getMarkdown() });
- },
- onOpenAddImageModal() {
- this.$refs.addImageModal.show();
- },
- onAddImage({ imageUrl, altText, file }) {
- const image = { imageUrl, altText };
-
- if (file) {
- this.$emit('uploadImage', { file, imageUrl });
- }
-
- addImage(this.editorInstance, image, file);
- },
- onOpenInsertVideoModal() {
- this.$refs.insertVideoModal.show();
- },
- onInsertVideo(url) {
- insertVideo(this.editorInstance, url);
- },
- onChangeMode(newMode) {
- this.$emit('modeChange', newMode);
- },
- },
-};
-</script>
-<template>
- <div>
- <toast-editor
- ref="editor"
- :initial-value="content"
- :options="editorOptions"
- :preview-style="previewStyle"
- :initial-edit-type="initialEditType"
- :height="height"
- @change="onContentChanged"
- @load="onLoad"
- />
- <add-image-modal ref="addImageModal" :image-root="imageRoot" @addImage="onAddImage" />
- <insert-video-modal ref="insertVideoModal" @insertVideo="onInsertVideo" />
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
deleted file mode 100644
index 6ffd280e005..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { union, mapValues } from 'lodash';
-import renderAttributeDefinition from './renderers/render_attribute_definition';
-import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
-import renderHeading from './renderers/render_heading';
-import renderBlockHtml from './renderers/render_html_block';
-import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
-import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
-import renderListItem from './renderers/render_list_item';
-import renderSoftbreak from './renderers/render_softbreak';
-
-const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
-const htmlBlockRenderers = [renderBlockHtml];
-const headingRenderers = [renderHeading];
-const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml];
-const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition];
-const listItemRenderers = [renderListItem];
-const softbreakRenderers = [renderSoftbreak];
-
-const executeRenderer = (renderers, node, context) => {
- const availableRenderer = renderers.find((renderer) => renderer.canRender(node, context));
-
- return availableRenderer ? availableRenderer.render(node, context) : context.origin();
-};
-
-const buildCustomHTMLRenderer = (customRenderers) => {
- const renderersByType = {
- ...customRenderers,
- htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
- htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
- heading: union(headingRenderers, customRenderers?.heading),
- item: union(listItemRenderers, customRenderers?.listItem),
- paragraph: union(paragraphRenderers, customRenderers?.paragraph),
- text: union(textRenderers, customRenderers?.text),
- softbreak: union(softbreakRenderers, customRenderers?.softbreak),
- };
-
- return mapValues(renderersByType, (renderers) => {
- return (node, context) => executeRenderer(renderers, node, context);
- });
-};
-
-export default buildCustomHTMLRenderer;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
deleted file mode 100644
index 273e0a59963..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-import { defaults, repeat } from 'lodash';
-
-const DEFAULTS = {
- subListIndentSpaces: 4,
- unorderedListBulletChar: '-',
- incrementListMarker: false,
- strong: '*',
- emphasis: '_',
-};
-
-const countIndentSpaces = (text) => {
- const matches = text.match(/^\s+/m);
-
- return matches ? matches[0].length : 0;
-};
-
-const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
- const {
- subListIndentSpaces,
- unorderedListBulletChar,
- incrementListMarker,
- strong,
- emphasis,
- } = defaults(formattingPreferences, DEFAULTS);
- const sublistNode = 'LI OL, LI UL';
- const unorderedListItemNode = 'UL LI';
- const orderedListItemNode = 'OL LI';
- const emphasisNode = 'EM, I';
- const strongNode = 'STRONG, B';
- const headingNode = 'H1, H2, H3, H4, H5, H6';
- const preCodeNode = 'PRE CODE';
-
- return {
- TEXT_NODE(node) {
- return baseRenderer.getSpaceControlled(
- baseRenderer.trim(baseRenderer.getSpaceCollapsedText(node.nodeValue)),
- node,
- );
- },
- /*
- * This converter overwrites the default indented list converter
- * to allow us to parameterize the number of indent spaces for
- * sublists.
- *
- * See the original implementation in
- * https://github.com/nhn/tui.editor/blob/master/libs/to-mark/src/renderer.basic.js#L161
- */
- [sublistNode](node, subContent) {
- const baseResult = baseRenderer.convert(node, subContent);
- // Default to 1 to prevent possible divide by 0
- const firstLevelIndentSpacesCount = countIndentSpaces(baseResult) || 1;
- const reindentedList = baseResult
- .split('\n')
- .map((line) => {
- const itemIndentSpacesCount = countIndentSpaces(line);
- const nestingLevel = Math.ceil(itemIndentSpacesCount / firstLevelIndentSpacesCount);
- const indentSpaces = repeat(' ', subListIndentSpaces * nestingLevel);
-
- return line.replace(/^ +/, indentSpaces);
- })
- .join('\n');
-
- return reindentedList;
- },
- [unorderedListItemNode](node, subContent) {
- const baseResult = baseRenderer.convert(node, subContent);
- const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
- const { attributeDefinition } = node.dataset;
-
- return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted;
- },
- [orderedListItemNode](node, subContent) {
- const baseResult = baseRenderer.convert(node, subContent);
-
- return incrementListMarker ? baseResult : baseResult.replace(/^(\s*)\d+?\./, '$11.');
- },
- [emphasisNode](node, subContent) {
- const result = baseRenderer.convert(node, subContent);
-
- return result.replace(/(^[*_]{1}|[*_]{1}$)/g, emphasis);
- },
- [strongNode](node, subContent) {
- const result = baseRenderer.convert(node, subContent);
- const strongSyntax = repeat(strong, 2);
-
- return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax);
- },
- [headingNode](node, subContent) {
- const result = baseRenderer.convert(node, subContent);
- const { attributeDefinition } = node.dataset;
-
- return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result;
- },
- [preCodeNode](node, subContent) {
- const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition);
-
- return isReferenceDefinition
- ? `\n\n${node.innerText}\n\n`
- : baseRenderer.convert(node, subContent);
- },
- IMG(node) {
- const { originalSrc } = node.dataset;
- return `![${node.alt}](${originalSrc || node.src})`;
- },
- };
-};
-
-export default buildHTMLToMarkdownRender;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
deleted file mode 100644
index 026a4069d9b..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { defaults } from 'lodash';
-import Vue from 'vue';
-import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
-import ToolbarItem from '../toolbar_item.vue';
-import buildCustomHTMLRenderer from './build_custom_renderer';
-import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
-import sanitizeHTML from './sanitize_html';
-
-const buildWrapper = (propsData) => {
- const instance = new Vue({
- render(createElement) {
- return createElement(ToolbarItem, propsData);
- },
- });
-
- instance.$mount();
- return instance.$el;
-};
-
-const buildVideoIframe = (src) => {
- const wrapper = document.createElement('figure');
- const iframe = document.createElement('iframe');
- const videoAttributes = { ...VIDEO_ATTRIBUTES, src };
- const wrapperClasses = ['gl-relative', 'gl-h-0', 'video_container'];
- const iframeClasses = ['gl-absolute', 'gl-top-0', 'gl-left-0', 'gl-w-full', 'gl-h-full'];
-
- wrapper.setAttribute('contenteditable', 'false');
- wrapper.classList.add(...wrapperClasses);
- iframe.classList.add(...iframeClasses);
- Object.assign(iframe, videoAttributes);
-
- wrapper.appendChild(iframe);
-
- return wrapper;
-};
-
-const buildImg = (alt, originalSrc, file) => {
- const img = document.createElement('img');
- const src = file ? URL.createObjectURL(file) : originalSrc;
- const attributes = { alt, src };
-
- if (file) {
- img.dataset.originalSrc = originalSrc;
- }
-
- Object.assign(img, attributes);
-
- return img;
-};
-
-export const generateToolbarItem = (config) => {
- const { icon, classes, event, command, tooltip, isDivider } = config;
-
- if (isDivider) {
- return 'divider';
- }
-
- return {
- type: 'button',
- options: {
- el: buildWrapper({ props: { icon, tooltip }, class: classes }),
- event,
- command,
- },
- };
-};
-
-export const addCustomEventListener = (editorApi, event, handler) => {
- editorApi.eventManager.addEventType(event);
- editorApi.eventManager.listen(event, handler);
-};
-
-export const removeCustomEventListener = (editorApi, event, handler) =>
- editorApi.eventManager.removeEventHandler(event, handler);
-
-export const addImage = ({ editor }, { altText, imageUrl }, file) => {
- if (editor.isWysiwygMode()) {
- const img = buildImg(altText, imageUrl, file);
- editor.getSquire().insertElement(img);
- } else {
- editor.insertText(`![${altText}](${imageUrl})`);
- }
-};
-
-export const insertVideo = ({ editor }, url) => {
- const videoIframe = buildVideoIframe(url);
-
- if (editor.isWysiwygMode()) {
- editor.getSquire().insertElement(videoIframe);
- } else {
- editor.insertText(videoIframe.outerHTML);
- }
-};
-
-export const getMarkdown = (editorInstance) => editorInstance.invoke('getMarkdown');
-
-/**
- * This function allow us to extend Toast UI HTML to Markdown renderer. It is
- * a temporary measure because Toast UI does not provide an API
- * to achieve this goal.
- */
-export const registerHTMLToMarkdownRenderer = (editorApi) => {
- const { renderer } = editorApi.toMarkOptions;
-
- Object.assign(editorApi.toMarkOptions, {
- renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)),
- });
-};
-
-export const getEditorOptions = (externalOptions) => {
- return defaults({
- customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers),
- toolbarItems: TOOLBAR_ITEM_CONFIGS.map((toolbarItem) => generateToolbarItem(toolbarItem)),
- customHTMLSanitizer: (html) => sanitizeHTML(html),
- });
-};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
deleted file mode 100644
index 638e5fd6f60..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const buildToken = (type, tagName, props) => {
- return { type, tagName, ...props };
-};
-
-const TAG_TYPES = {
- block: 'div',
- inline: 'a',
-};
-
-// Open helpers (singular and multiple)
-
-const buildUneditableOpenToken = (tagType = TAG_TYPES.block) =>
- buildToken('openTag', tagType, {
- attributes: { contenteditable: false },
- classNames: [
- 'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
- ],
- });
-
-export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => {
- return [buildUneditableOpenToken(tagType), token];
-};
-
-// Close helpers (singular and multiple)
-
-export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) =>
- buildToken('closeTag', tagType);
-
-export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => {
- return [token, buildUneditableCloseToken(tagType)];
-};
-
-// Complete helpers (open plus close)
-
-export const buildTextToken = (content) => buildToken('text', null, { content });
-
-export const buildUneditableBlockTokens = (token) => {
- return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
-};
-
-export const buildUneditableInlineTokens = (token) => {
- return [
- ...buildUneditableOpenTokens(token, TAG_TYPES.inline),
- buildUneditableCloseToken(TAG_TYPES.inline),
- ];
-};
-
-export const buildUneditableHtmlAsTextTokens = (node) => {
- /*
- Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain
- nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want
- to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html`
- type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass `
- to prevent their persistence within the `text` content as the user did not intend these as edits.
-
- https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72
- */
- const regex = / data-tomark-pass /gm;
- const content = node.literal.replace(regex, '');
- const htmlAsTextToken = buildToken('text', null, { content });
-
- return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()];
-};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js
deleted file mode 100644
index bd419447a48..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { isAttributeDefinition } from './render_utils';
-
-const canRender = ({ literal }) => isAttributeDefinition(literal);
-
-const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' });
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
deleted file mode 100644
index 0e122f598e5..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { renderUneditableLeaf as render } from './render_utils';
-
-const embeddedRubyRegex = /(^<%.+%>$)/;
-
-const canRender = ({ literal }) => {
- return embeddedRubyRegex.test(literal);
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
deleted file mode 100644
index 572f6e3cf9d..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { buildUneditableInlineTokens } from './build_uneditable_token';
-
-const fontAwesomeRegexOpen = /<i class="fa.+>/;
-
-const canRender = ({ literal }) => {
- return fontAwesomeRegexOpen.test(literal);
-};
-
-const render = (_, { origin }) => buildUneditableInlineTokens(origin());
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js
deleted file mode 100644
index 71026fd0d65..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- renderWithAttributeDefinitions as render,
- willAlwaysRender as canRender,
-} from './render_utils';
-
-export default { render, canRender };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
deleted file mode 100644
index 710b807275b..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { getURLOrigin } from '~/lib/utils/url_utility';
-import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
-import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
-
-const isVideoFrame = (html) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const {
- children: { length },
- } = doc;
- const iframe = doc.querySelector('iframe');
- const origin = iframe && getURLOrigin(iframe.getAttribute('src'));
-
- return length === 1 && ALLOWED_VIDEO_ORIGINS.includes(origin);
-};
-
-const canRender = ({ type, literal }) => {
- return type === 'htmlBlock' && !isVideoFrame(literal);
-};
-
-const render = (node) => buildUneditableHtmlAsTextTokens(node);
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js
deleted file mode 100644
index d7716543b53..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { buildTextToken, buildUneditableInlineTokens } from './build_uneditable_token';
-
-/*
-Use case examples:
-- Majority: two bracket pairs, back-to-back, each with content (including spaces)
- - `[environment terraform plans][terraform]`
- - `[an issue labelled `~"master:broken"`][broken-master-issues]`
-- Minority: two bracket pairs the latter being empty or only one pair with content (including spaces)
- - `[this link][]`
- - `[this link]`
-
-Regexp notes:
- - `(?:\[.+?\]){1}`: Always one bracket pair with content (including spaces)
- - `(?:\[\]|\[.+?\])?`: Optional second pair that may or may not contain content (including spaces)
- - `(?!:)`: Never followed by a `:` which is reserved for identifier definition syntax (`[identifier]: /the-link`)
- - Each of the three parts is non-captured, but the match as a whole is captured
-*/
-const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g;
-
-const isIdentifierInstance = (literal) => {
- // Reset lastIndex as global flag in regexp are stateful (https://stackoverflow.com/a/11477448)
- identifierInstanceRegex.lastIndex = 0;
- return identifierInstanceRegex.test(literal);
-};
-
-const canRender = ({ literal }) => isIdentifierInstance(literal);
-
-const tokenize = (text) => {
- const matches = text.split(identifierInstanceRegex);
- const tokens = matches.map((match) => {
- const token = buildTextToken(match);
- return isIdentifierInstance(match) ? buildUneditableInlineTokens(token) : token;
- });
-
- return tokens.flat();
-};
-
-const render = (_, { origin }) => tokenize(origin().content);
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
deleted file mode 100644
index 4829f0f2243..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const identifierRegex = /(^\[.+\]: .+)/;
-
-const isIdentifier = (text) => {
- return identifierRegex.test(text);
-};
-
-const canRender = (node, context) => {
- return isIdentifier(context.getChildrenText(node));
-};
-
-const getReferenceDefinitions = (node, definitions = '') => {
- if (!node) {
- return definitions;
- }
-
- const definition = node.type === 'text' ? node.literal : '\n';
-
- return getReferenceDefinitions(node.next, `${definitions}${definition}`);
-};
-
-const render = (node, { skipChildren }) => {
- const content = getReferenceDefinitions(node.firstChild);
-
- skipChildren();
-
- return [
- {
- type: 'openTag',
- tagName: 'pre',
- classNames: ['code-block', 'language-markdown'],
- attributes: { 'data-sse-reference-definition': true },
- },
- { type: 'openTag', tagName: 'code' },
- { type: 'text', content },
- { type: 'closeTag', tagName: 'code' },
- { type: 'closeTag', tagName: 'pre' },
- ];
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js
deleted file mode 100644
index 71026fd0d65..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- renderWithAttributeDefinitions as render,
- willAlwaysRender as canRender,
-} from './render_utils';
-
-export default { render, canRender };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js
deleted file mode 100644
index c004e839821..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const canRender = (node) => ['emph', 'strong'].includes(node.parent?.type);
-const render = () => ({
- type: 'text',
- content: ' ',
-});
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
deleted file mode 100644
index eff5dbf59f2..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import {
- buildUneditableBlockTokens,
- buildUneditableOpenTokens,
- buildUneditableCloseToken,
-} from './build_uneditable_token';
-
-export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockTokens(origin());
-
-export const renderUneditableBranch = (_, { entering, origin }) =>
- entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
-
-const attributeDefinitionRegexp = /(^{:.+}$)/;
-
-export const isAttributeDefinition = (text) => attributeDefinitionRegexp.test(text);
-
-const findAttributeDefinition = (node) => {
- const literal =
- node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items;
-
- return isAttributeDefinition(literal) ? literal : null;
-};
-
-export const renderWithAttributeDefinitions = (node, { origin }) => {
- const attributes = findAttributeDefinition(node);
- const token = origin();
-
- if (token.type === 'openTag' && attributes) {
- Object.assign(token, {
- attributes: {
- 'data-attribute-definition': attributes,
- },
- });
- }
-
- return token;
-};
-
-export const willAlwaysRender = () => true;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
deleted file mode 100644
index 486d88466b7..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import createSanitizer from 'dompurify';
-import { getURLOrigin } from '~/lib/utils/url_utility';
-import { ALLOWED_VIDEO_ORIGINS } from '../constants';
-
-const sanitizer = createSanitizer(window);
-const ADD_TAGS = ['iframe'];
-
-sanitizer.addHook('uponSanitizeElement', (node) => {
- if (node.tagName !== 'IFRAME') {
- return;
- }
-
- const origin = getURLOrigin(node.getAttribute('src'));
-
- if (!ALLOWED_VIDEO_ORIGINS.includes(origin)) {
- node.remove();
- }
-});
-
-const sanitize = (content) => sanitizer.sanitize(content, { ADD_TAGS });
-
-export default sanitize;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue
deleted file mode 100644
index 85a67c087bb..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-
-export default {
- components: {
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- icon: {
- type: String,
- required: true,
- },
- tooltip: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <button
- v-gl-tooltip="{ title: tooltip }"
- :aria-label="tooltip"
- class="p-0 gl-display-flex toolbar-button"
- >
- <gl-icon class="gl-mx-auto gl-align-self-center" :name="icon" />
- </button>
-</template>