diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-11 03:08:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-11 03:08:02 +0300 |
commit | cb2494484e33a0d3c750625908e8b4dda69ab7b4 (patch) | |
tree | d3a4748bdb44b5ad7c1952c508dbb18230c45a7b /app/assets/javascripts/content_editor/services | |
parent | 8cd9a013792172f8d64eba8f327e2dadd71efffb (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor/services')
4 files changed, 88 insertions, 32 deletions
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 6eac5a63439..52dacb84153 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -9,6 +9,7 @@ export class ContentEditor { this._deserializer = deserializer; this._eventHub = eventHub; this._assetResolver = assetResolver; + this._pristineDoc = null; } get tiptapEditor() { @@ -19,6 +20,10 @@ export class ContentEditor { return this._eventHub; } + get changed() { + return this._pristineDoc?.eq(this.tiptapEditor.state.doc); + } + get empty() { const doc = this.tiptapEditor?.state.doc; @@ -54,11 +59,12 @@ export class ContentEditor { try { eventHub.$emit(LOADING_CONTENT_EVENT); - const result = await this.deserialize(serializedContent); + const { document } = await this.deserialize(serializedContent); - if (Object.keys(result).length !== 0) { + if (document) { + this._pristineDoc = document; tr.setSelection(selection) - .replaceSelectionWith(result.document, false) + .replaceSelectionWith(document, false) .setMeta('preventUpdate', true); editor.view.dispatch(tr); } @@ -71,8 +77,9 @@ export class ContentEditor { } getSerializedContent() { - const { _tiptapEditor: editor, _serializer: serializer } = this; + const { _tiptapEditor: editor, _serializer: serializer, _pristineDoc: pristineDoc } = this; + const { doc } = editor.state; - return serializer.serialize({ schema: editor.schema, content: editor.getJSON() }); + return serializer.serialize({ doc, pristineDoc }); } } diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index db76073510e..15aac3d86e5 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -43,6 +43,7 @@ import OrderedList from '../extensions/ordered_list'; import Paragraph from '../extensions/paragraph'; import PasteMarkdown from '../extensions/paste_markdown'; import Reference from '../extensions/reference'; +import Sourcemap from '../extensions/sourcemap'; import Strike from '../extensions/strike'; import Subscript from '../extensions/subscript'; import Superscript from '../extensions/superscript'; @@ -128,6 +129,7 @@ export const createContentEditor = ({ Paragraph, PasteMarkdown.configure({ renderMarkdown, eventHub }), Reference, + Sourcemap, Strike, Subscript, Superscript, diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index c2be7bc9195..6c8c562af8e 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -61,6 +61,7 @@ import { renderPlayable, renderHTMLNode, renderContent, + preserveUnchanged, } from './serialization_helpers'; const defaultSerializerConfig = { @@ -119,7 +120,7 @@ const defaultSerializerConfig = { nodes: { [Audio.name]: renderPlayable, - [Blockquote.name]: (state, node) => { + [Blockquote.name]: preserveUnchanged((state, node) => { if (node.attrs.multiline) { state.write('>>>'); state.ensureNewLine(); @@ -130,9 +131,9 @@ const defaultSerializerConfig = { } else { state.wrapBlock('> ', null, node, () => state.renderContent(node)); } - }, - [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list, - [CodeBlockHighlight.name]: renderCodeBlock, + }), + [BulletList.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.bullet_list), + [CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock), [Diagram.name]: renderCodeBlock, [Division.name]: (state, node) => { if (node.attrs.className?.includes('js-markdown-code')) { @@ -189,13 +190,13 @@ const defaultSerializerConfig = { }, [Figure.name]: renderHTMLNode('figure'), [FigureCaption.name]: renderHTMLNode('figcaption'), - [HardBreak.name]: renderHardBreak, - [Heading.name]: defaultMarkdownSerializer.nodes.heading, - [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule, - [Image.name]: renderImage, - [ListItem.name]: defaultMarkdownSerializer.nodes.list_item, - [OrderedList.name]: renderOrderedList, - [Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph, + [HardBreak.name]: preserveUnchanged(renderHardBreak), + [Heading.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.heading), + [HorizontalRule.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.horizontal_rule), + [Image.name]: preserveUnchanged(renderImage), + [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item), + [OrderedList.name]: preserveUnchanged(renderOrderedList), + [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph), [Reference.name]: (state, node) => { state.write(node.attrs.originalText || node.attrs.text); }, @@ -221,29 +222,60 @@ const defaultSerializerConfig = { }, }; +const createChangeTracker = (doc, pristineDoc) => { + const changeTracker = new WeakMap(); + const pristineSourceMarkdownMap = new Map(); + + if (doc && pristineDoc) { + pristineDoc.descendants((node) => { + if (node.attrs.sourceMapKey) { + pristineSourceMarkdownMap.set(`${node.attrs.sourceMapKey}${node.type.name}`, node); + } + }); + doc.descendants((node) => { + const pristineNode = pristineSourceMarkdownMap.get( + `${node.attrs.sourceMapKey}${node.type.name}`, + ); + + if (pristineNode) { + changeTracker.set(node, node.eq(pristineNode)); + } + }); + } + + return changeTracker; +}; + /** - * A markdown serializer converts arbitrary Markdown content - * into a ProseMirror document and viceversa. To convert Markdown - * into a ProseMirror document, the Markdown should be rendered. + * Converts a ProseMirror document to Markdown. See the + * following documentation to learn how to implement + * custom node and mark serializer functions. + * + * https://github.com/prosemirror/prosemirror-markdown * - * The client should provide a render function to allow flexibility - * on the desired rendering approach. + * @param {Object} params.nodes ProseMirror node serializer functions + * @param {Object} params.marks ProseMirror marks serializer config * - * @param {Function} params.render Render function - * that parses the Markdown and converts it into HTML. * @returns a markdown serializer */ export default ({ serializerConfig = {} } = {}) => ({ /** - * Converts a ProseMirror JSONDocument based - * on a ProseMirror schema into Markdown - * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines - * the types of content supported in the document - * @param {String} params.content A ProseMirror JSONDocument - * @returns A Markdown string + * Serializes a ProseMirror document as Markdown. If a node contains + * sourcemap metadata, the serializer is capable of restoring the + * Markdown from which the node was generated using a Markdown + * deserializer. + * + * See the Sourcemap metadata extension and the remark_markdown_deserializer + * service for more information. + * + * @param {ProseMirror.Node} params.doc ProseMirror document to convert into Markdown + * @param {ProseMirror.Node} params.pristineDoc Pristine version of the document that + * should be converted into Markdown. This is used to detect which nodes in the document + * changed. + * @returns A String that represents the serialized document as Markdown */ - serialize: ({ schema, content }) => { - const proseMirrorDocument = schema.nodeFromJSON(content); + serialize: ({ doc, pristineDoc }) => { + const changeTracker = createChangeTracker(doc, pristineDoc); const serializer = new ProseMirrorMarkdownSerializer( { ...defaultSerializerConfig.nodes, @@ -255,8 +287,9 @@ export default ({ serializerConfig = {} } = {}) => ({ }, ); - return serializer.serialize(proseMirrorDocument, { + return serializer.serialize(doc, { tightLists: true, + changeTracker, }); }, }); diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 3e48434c6f9..99aaee8f312 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -349,3 +349,17 @@ export function renderCodeBlock(state, node) { state.write('```'); state.closeBlock(node); } + +export function preserveUnchanged(render) { + return (state, node, parent, index) => { + const { sourceMarkdown } = node.attrs; + const same = state.options.changeTracker.get(node); + + if (same) { + state.write(sourceMarkdown); + state.closeBlock(node); + } else { + render(state, node, parent, index); + } + }; +} |