Commit 9354eee3 authored by Enrique Alcantara's avatar Enrique Alcantara

Separate Markdown Serializer and Deserializers

Split the Markdown Serializer and Deserializer
concerns in different modules. This allows us
to reuse any of these concerns in other parts
of the Content Editor without having to provide
the dependencies expected by both modules
at the same time
parent 84d6de14
import { TextSelection } from 'prosemirror-state';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer, eventHub }) {
constructor({ tiptapEditor, serializer, deserializer, eventHub }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._deserializer = deserializer;
this._eventHub = eventHub;
}
......@@ -31,15 +34,22 @@ export class ContentEditor {
}
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _serializer: serializer, _eventHub: eventHub } = this;
const { _tiptapEditor: editor, _deserializer: deserializer, _eventHub: eventHub } = this;
const { doc, tr } = editor.state;
const selection = TextSelection.create(doc, 0, doc.content.size);
try {
eventHub.$emit(LOADING_CONTENT_EVENT);
const document = await serializer.deserialize({
const newDoc = await deserializer.deserialize({
schema: editor.schema,
content: serializedContent,
});
editor.commands.setContent(document);
if (newDoc) {
tr.setSelection(selection)
.replaceSelectionWith(newDoc, false)
.setMeta('preventUpdate', true);
editor.view.dispatch(tr);
}
eventHub.$emit(LOADING_SUCCESS_EVENT);
} catch (e) {
eventHub.$emit(LOADING_ERROR_EVENT, e);
......
......@@ -55,6 +55,7 @@ import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import createMarkdownDeserializer from './markdown_deserializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
......@@ -138,7 +139,8 @@ export const createContentEditor = ({
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
const serializer = createMarkdownSerializer({ serializerConfig });
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
return new ContentEditor({ tiptapEditor, serializer, eventHub });
return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer });
};
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
export default ({ render }) => {
/**
* Converts a Markdown string into a ProseMirror JSONDocument based
* on a ProseMirror schema.
* @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
* the types of content supported in the document
* @param {String} params.content An arbitrary markdown string
* @returns A ProseMirror JSONDocument
*/
return {
deserialize: async ({ schema, content }) => {
const html = await render(content);
if (!html) return null;
const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html');
// append original source as a comment that nodes can access
body.append(document.createComment(content));
return ProseMirrorDOMParser.fromSchema(schema).parse(body);
},
};
};
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import {
MarkdownSerializer as ProseMirrorMarkdownSerializer,
defaultMarkdownSerializer,
......@@ -237,31 +236,7 @@ const defaultSerializerConfig = {
* that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
export default ({ render = () => null, serializerConfig = {} } = {}) => ({
/**
* Converts a Markdown string into a ProseMirror JSONDocument based
* on a ProseMirror schema.
* @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
* the types of content supported in the document
* @param {String} params.content An arbitrary markdown string
* @returns A ProseMirror JSONDocument
*/
deserialize: async ({ schema, content }) => {
const html = await render(content);
if (!html) return null;
const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html');
// append original source as a comment that nodes can access
body.append(document.createComment(content));
const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
return state.toJSON();
},
export default ({ serializerConfig = {} } = {}) => ({
/**
* Converts a ProseMirror JSONDocument based
* on a ProseMirror schema into Markdown
......
......@@ -10,6 +10,7 @@ import { createTestEditor } from '../test_utils';
describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
let deserializer;
let eventHub;
beforeEach(() => {
......@@ -17,8 +18,9 @@ describe('content_editor/services/content_editor', () => {
jest.spyOn(tiptapEditor, 'destroy');
serializer = { deserialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
eventHub = eventHubFactory();
contentEditor = new ContentEditor({ tiptapEditor, serializer, eventHub });
contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub });
});
describe('.dispose', () => {
......@@ -33,7 +35,7 @@ describe('content_editor/services/content_editor', () => {
describe('when setSerializedContent succeeds', () => {
beforeEach(() => {
serializer.deserialize.mockResolvedValueOnce('');
deserializer.deserialize.mockResolvedValueOnce('');
});
it('emits loadingContent and loadingSuccess event in the eventHub', () => {
......@@ -54,7 +56,7 @@ describe('content_editor/services/content_editor', () => {
const error = 'error';
beforeEach(() => {
serializer.deserialize.mockRejectedValueOnce(error);
deserializer.deserialize.mockRejectedValueOnce(error);
});
it('emits loadingError event', async () => {
......
import createMarkdownDeserializer from '~/content_editor/services/markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/markdown_deserializer', () => {
let renderMarkdown;
let doc;
let p;
let bold;
let tiptapEditor;
beforeEach(() => {
tiptapEditor = createTestEditor({
extensions: [Bold],
});
({
builders: { doc, p, bold },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
},
}));
renderMarkdown = jest.fn();
});
it('transforms HTML returned by render function to a ProseMirror document', async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
const expectedDoc = doc(p(bold('Bold text')));
renderMarkdown.mockResolvedValueOnce('<p><strong>Bold text</strong></p>');
const result = await deserializer.deserialize({
content: 'content',
schema: tiptapEditor.schema,
});
expect(result.toJSON()).toEqual(expectedDoc.toJSON());
});
describe('when the render function returns an empty value', () => {
it('also returns null', async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
renderMarkdown.mockResolvedValueOnce(null);
expect(await deserializer.deserialize({ content: 'content' })).toBe(null);
});
});
});
......@@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
import Paragraph from '~/content_editor/extensions/paragraph';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import markdownDeserializer from '~/content_editor/services/markdown_deserializer';
import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
import { createTestEditor, createDocBuilder } from '../test_utils';
......@@ -53,9 +53,8 @@ const {
describe('content_editor/services/markdown_sourcemap', () => {
it('gets markdown source for a rendered HTML element', async () => {
const deserialized = await markdownSerializer({
const deserialized = await markdownDeserializer({
render: () => BULLET_LIST_HTML,
serializerConfig: {},
}).deserialize({
schema: tiptapEditor.schema,
content: BULLET_LIST_MARKDOWN,
......@@ -76,6 +75,6 @@ describe('content_editor/services/markdown_sourcemap', () => {
),
);
expect(deserialized).toEqual(expected.toJSON());
expect(deserialized.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