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:
Diffstat (limited to 'app/assets/javascripts/content_editor/extensions/paste_markdown.js')
-rw-r--r--app/assets/javascripts/content_editor/extensions/paste_markdown.js102
1 files changed, 73 insertions, 29 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
index 82fa5ce6c1d..db13438de5e 100644
--- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js
+++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
@@ -1,5 +1,7 @@
+import OrderedMap from 'orderedmap';
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from '@tiptap/pm/state';
+import { Schema, DOMParser as ProseMirrorDOMParser, DOMSerializer } from '@tiptap/pm/model';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/alert';
import createMarkdownDeserializer from '../services/gl_api_markdown_deserializer';
@@ -9,47 +11,55 @@ import Diagram from './diagram';
import Frontmatter from './frontmatter';
const TEXT_FORMAT = 'text/plain';
+const GFM_FORMAT = 'text/x-gfm';
const HTML_FORMAT = 'text/html';
const VS_CODE_FORMAT = 'vscode-editor-data';
const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+function parseHTML(schema, html) {
+ const parser = new DOMParser();
+ const startTag = '<body>';
+ const endTag = '</body>';
+ const { body } = parser.parseFromString(startTag + html + endTag, 'text/html');
+ return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
+}
+
export default Extension.create({
name: 'pasteMarkdown',
priority: EXTENSION_PRIORITY_HIGHEST,
addOptions() {
return {
renderMarkdown: null,
+ serializer: null,
};
},
addCommands() {
return {
- pasteMarkdown: (markdown) => () => {
+ pasteContent: (content = '', processMarkdown = true) => async () => {
const { editor, options } = this;
const { renderMarkdown, eventHub } = options;
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- deserializer
- .deserialize({ schema: editor.schema, markdown })
+ const pasteSchemaSpec = { ...editor.schema.spec };
+ pasteSchemaSpec.marks = OrderedMap.from(pasteSchemaSpec.marks).remove('span');
+ pasteSchemaSpec.nodes = OrderedMap.from(pasteSchemaSpec.nodes).remove('div').remove('pre');
+ const schema = new Schema(pasteSchemaSpec);
+
+ const promise = processMarkdown
+ ? deserializer.deserialize({ schema, markdown: content })
+ : Promise.resolve(parseHTML(schema, content));
+
+ promise
.then(({ document }) => {
- if (!document) {
- return;
- }
+ if (!document) return;
- const { state, view } = editor;
- const { tr, selection } = state;
const { firstChild } = document.content;
- const content =
+ const toPaste =
document.content.childCount === 1 && firstChild.type.name === 'paragraph'
? firstChild.content
: document.content;
- if (selection.to - selection.from > 0) {
- tr.replaceWith(selection.from, selection.to, content);
- } else {
- tr.insert(selection.from, content);
- }
-
- view.dispatch(tr);
+ editor.commands.insertContent(toPaste.toJSON());
})
.catch(() => {
eventHub.$emit(ALERT_EVENT, {
@@ -65,24 +75,57 @@ export default Extension.create({
addProseMirrorPlugins() {
let pasteRaw = false;
+ const handleCutAndCopy = (view, event) => {
+ const slice = view.state.selection.content();
+ const gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
+ slice.content,
+ );
+ const div = document.createElement('div');
+ div.appendChild(documentFragment);
+
+ event.clipboardData.setData(TEXT_FORMAT, div.innerText);
+ event.clipboardData.setData(HTML_FORMAT, div.innerHTML);
+ event.clipboardData.setData(GFM_FORMAT, gfmContent);
+
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
return [
new Plugin({
key: new PluginKey('pasteMarkdown'),
props: {
+ handleDOMEvents: {
+ copy: handleCutAndCopy,
+ cut: (view, event) => {
+ handleCutAndCopy(view, event);
+ this.editor.commands.deleteSelection();
+ },
+ },
handleKeyDown: (_, event) => {
pasteRaw = event.key === 'v' && (event.metaKey || event.ctrlKey) && event.shiftKey;
},
handlePaste: (view, event) => {
const { clipboardData } = event;
- const content = clipboardData.getData(TEXT_FORMAT);
- const { state } = view;
- const { tr, selection } = state;
- const { from, to } = selection;
+
+ const gfmContent = clipboardData.getData(GFM_FORMAT);
+
+ if (gfmContent) {
+ return this.editor.commands.pasteContent(gfmContent, true);
+ }
+
+ const textContent = clipboardData.getData(TEXT_FORMAT);
+ const htmlContent = clipboardData.getData(HTML_FORMAT);
+
+ const { from, to } = view.state.selection;
if (pasteRaw) {
- tr.insertText(content.replace(/^\s+|\s+$/gm, ''), from, to);
- view.dispatch(tr);
+ this.editor.commands.insertContentAt(
+ { from, to },
+ textContent.replace(/^\s+|\s+$/gm, ''),
+ );
return true;
}
@@ -91,18 +134,19 @@ export default Extension.create({
const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
const language = vsCodeMeta.mode;
- if (!content || (hasHTML && !hasVsCode) || (hasVsCode && language !== 'markdown')) {
- return false;
- }
-
// if a code block is active, paste as plain text
- if (CODE_BLOCK_NODE_TYPES.some((type) => this.editor.isActive(type))) {
+ if (!textContent || CODE_BLOCK_NODE_TYPES.some((type) => this.editor.isActive(type))) {
return false;
}
- this.editor.commands.pasteMarkdown(content);
+ if (hasVsCode) {
+ return this.editor.commands.pasteContent(
+ language === 'markdown' ? textContent : `\`\`\`${language}\n${textContent}\n\`\`\``,
+ true,
+ );
+ }
- return true;
+ return this.editor.commands.pasteContent(hasHTML ? htmlContent : textContent, !hasHTML);
},
},
}),