Commit ca64c46a authored by Frédéric Caplette's avatar Frédéric Caplette

Merge branch 'return-dom-fragment-markdown-deserializer' into 'master'

Return DOM fragment in Markdown Serializer

See merge request gitlab-org/gitlab!82733
parents 077339d3 3107464d
...@@ -34,15 +34,15 @@ export default Extension.create({ ...@@ -34,15 +34,15 @@ export default Extension.create({
deserializer deserializer
.deserialize({ schema: editor.schema, content: markdown }) .deserialize({ schema: editor.schema, content: markdown })
.then((doc) => { .then(({ document }) => {
if (!doc) { if (!document) {
return; return;
} }
const { state, view } = editor; const { state, view } = editor;
const { tr, selection } = state; const { tr, selection } = state;
tr.replaceWith(selection.from - 1, selection.to, doc.content); tr.replaceWith(selection.from - 1, selection.to, document.content);
view.dispatch(tr); view.dispatch(tr);
eventHub.$emit(LOADING_SUCCESS_EVENT); eventHub.$emit(LOADING_SUCCESS_EVENT);
}) })
......
...@@ -40,13 +40,14 @@ export class ContentEditor { ...@@ -40,13 +40,14 @@ export class ContentEditor {
try { try {
eventHub.$emit(LOADING_CONTENT_EVENT); eventHub.$emit(LOADING_CONTENT_EVENT);
const newDoc = await deserializer.deserialize({ const { document } = await deserializer.deserialize({
schema: editor.schema, schema: editor.schema,
content: serializedContent, content: serializedContent,
}); });
if (newDoc) {
if (document) {
tr.setSelection(selection) tr.setSelection(selection)
.replaceSelectionWith(newDoc, false) .replaceSelectionWith(document, false)
.setMeta('preventUpdate', true); .setMeta('preventUpdate', true);
editor.view.dispatch(tr); editor.view.dispatch(tr);
} }
......
...@@ -4,16 +4,22 @@ export default ({ render }) => { ...@@ -4,16 +4,22 @@ export default ({ render }) => {
/** /**
* Converts a Markdown string into a ProseMirror JSONDocument based * Converts a Markdown string into a ProseMirror JSONDocument based
* on a ProseMirror schema. * on a ProseMirror schema.
*
* @param {Object} options — The schema and content for deserialization
* @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
* the types of content supported in the document * the types of content supported in the document
* @param {String} params.content An arbitrary markdown string * @param {String} params.content An arbitrary markdown string
* @returns A ProseMirror JSONDocument *
* @returns An object with the following properties:
* - document: A ProseMirror document object generated from the deserialized Markdown
* - dom: The Markdown Deserializer renders Markdown as HTML to generate the ProseMirror
* document. The dom property contains the HTML generated from the Markdown Source.
*/ */
return { return {
deserialize: async ({ schema, content }) => { deserialize: async ({ schema, content }) => {
const html = await render(content); const html = await render(content);
if (!html) return null; if (!html) return {};
const parser = new DOMParser(); const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html'); const { body } = parser.parseFromString(html, 'text/html');
...@@ -21,7 +27,7 @@ export default ({ render }) => { ...@@ -21,7 +27,7 @@ export default ({ render }) => {
// append original source as a comment that nodes can access // append original source as a comment that nodes can access
body.append(document.createComment(content)); body.append(document.createComment(content));
return ProseMirrorDOMParser.fromSchema(schema).parse(body); return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), dom: body };
}, },
}; };
}; };
...@@ -5,18 +5,26 @@ import { ...@@ -5,18 +5,26 @@ import {
} from '~/content_editor/constants'; } from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor'; import { ContentEditor } from '~/content_editor/services/content_editor';
import eventHubFactory from '~/helpers/event_hub_factory'; import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor } from '../test_utils'; import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/content_editor', () => { describe('content_editor/services/content_editor', () => {
let contentEditor; let contentEditor;
let serializer; let serializer;
let deserializer; let deserializer;
let eventHub; let eventHub;
let doc;
let p;
beforeEach(() => { beforeEach(() => {
const tiptapEditor = createTestEditor(); const tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'destroy'); jest.spyOn(tiptapEditor, 'destroy');
({
builders: { doc, p },
} = createDocBuilder({
tiptapEditor,
}));
serializer = { deserialize: jest.fn() }; serializer = { deserialize: jest.fn() };
deserializer = { deserialize: jest.fn() }; deserializer = { deserialize: jest.fn() };
eventHub = eventHubFactory(); eventHub = eventHubFactory();
...@@ -34,8 +42,11 @@ describe('content_editor/services/content_editor', () => { ...@@ -34,8 +42,11 @@ describe('content_editor/services/content_editor', () => {
}); });
describe('when setSerializedContent succeeds', () => { describe('when setSerializedContent succeeds', () => {
let document;
beforeEach(() => { beforeEach(() => {
deserializer.deserialize.mockResolvedValueOnce(''); document = doc(p('document'));
deserializer.deserialize.mockResolvedValueOnce({ document });
}); });
it('emits loadingContent and loadingSuccess event in the eventHub', () => { it('emits loadingContent and loadingSuccess event in the eventHub', () => {
...@@ -50,6 +61,12 @@ describe('content_editor/services/content_editor', () => { ...@@ -50,6 +61,12 @@ describe('content_editor/services/content_editor', () => {
contentEditor.setSerializedContent('**bold text**'); contentEditor.setSerializedContent('**bold text**');
}); });
it('sets the deserialized document in the tiptap editor object', async () => {
await contentEditor.setSerializedContent('**bold text**');
expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
});
}); });
describe('when setSerializedContent fails', () => { describe('when setSerializedContent fails', () => {
......
...@@ -25,27 +25,38 @@ describe('content_editor/services/markdown_deserializer', () => { ...@@ -25,27 +25,38 @@ describe('content_editor/services/markdown_deserializer', () => {
renderMarkdown = jest.fn(); renderMarkdown = jest.fn();
}); });
it('transforms HTML returned by render function to a ProseMirror document', async () => { describe('when deserializing', () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); let result;
const expectedDoc = doc(p(bold('Bold text'))); const text = 'Bold text';
renderMarkdown.mockResolvedValueOnce('<p><strong>Bold text</strong></p>'); beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`);
const result = await deserializer.deserialize({ result = await deserializer.deserialize({
content: 'content', content: 'content',
schema: tiptapEditor.schema, schema: tiptapEditor.schema,
});
}); });
it('transforms HTML returned by render function to a ProseMirror document', async () => {
const expectedDoc = doc(p(bold(text)));
expect(result.toJSON()).toEqual(expectedDoc.toJSON()); expect(result.document.toJSON()).toEqual(expectedDoc.toJSON());
});
it('returns parsed HTML as a DOM object', () => {
expect(result.dom.innerHTML).toEqual(`<p><strong>${text}</strong></p><!--content-->`);
});
}); });
describe('when the render function returns an empty value', () => { describe('when the render function returns an empty value', () => {
it('also returns null', async () => { it('returns an empty object', async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
renderMarkdown.mockResolvedValueOnce(null); renderMarkdown.mockResolvedValueOnce(null);
expect(await deserializer.deserialize({ content: 'content' })).toBe(null); expect(await deserializer.deserialize({ content: 'content' })).toEqual({});
}); });
}); });
}); });
...@@ -73,7 +73,7 @@ describe('content_editor/services/markdown_sourcemap', () => { ...@@ -73,7 +73,7 @@ describe('content_editor/services/markdown_sourcemap', () => {
}); });
it('gets markdown source for a rendered HTML element', async () => { it('gets markdown source for a rendered HTML element', async () => {
const deserialized = await markdownDeserializer({ const { document } = await markdownDeserializer({
render: () => BULLET_LIST_HTML, render: () => BULLET_LIST_HTML,
}).deserialize({ }).deserialize({
schema: tiptapEditor.schema, schema: tiptapEditor.schema,
...@@ -95,6 +95,6 @@ describe('content_editor/services/markdown_sourcemap', () => { ...@@ -95,6 +95,6 @@ describe('content_editor/services/markdown_sourcemap', () => {
), ),
); );
expect(deserialized.toJSON()).toEqual(expected.toJSON()); expect(document.toJSON()).toEqual(expected.toJSON());
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment