Commit af408f37 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'split-markdown-serializer-and-markdown-deserializer' into 'master'

Separate Markdown Serializer and Deserializer in the Content Editor

See merge request gitlab-org/gitlab!81034
parents 31ab4415 9354eee3
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