Commit 3107464d authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Frédéric Caplette

Return DOM fragment in Markdown Serializer

Return DOMFragment along with ProseMirror document
in the MarkdownDeserializer
parent 4ba8a6f5
...@@ -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