Commit e61d74af authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'content-editor-markdown-serializer' into 'master'

Pass a Markdown serializer to the Content Editor

See merge request gitlab-org/gitlab!58678
parents fb0738b3 e5ce78cf
import { s__ } from '~/locale';
export const PROVIDE_SERIALIZER_OR_RENDERER_ERROR = s__(
'ContentEditor|You have to provide a renderMarkdown function or a custom serializer',
);
export { default as createEditor } from './services/create_editor';
export { default as ContentEditor } from './components/content_editor.vue';
import { isFunction, isString } from 'lodash';
import { Editor } from 'tiptap'; import { Editor } from 'tiptap';
import { Bold, Code } from 'tiptap-extensions'; import { Bold, Code } from 'tiptap-extensions';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import createMarkdownSerializer from './markdown_serializer';
const createEditor = ({ content } = {}) => { const createEditor = async ({ content, renderMarkdown, serializer: customSerializer } = {}) => {
return new Editor({ if (!customSerializer && !isFunction(renderMarkdown)) {
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}
const editor = new Editor({
extensions: [new Bold(), new Code()], extensions: [new Bold(), new Code()],
content,
}); });
const serializer = customSerializer || createMarkdownSerializer({ render: renderMarkdown });
editor.setSerializedContent = async (serializedContent) => {
editor.setContent(
await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
);
};
editor.getSerializedContent = () => {
return serializer.serialize({ schema: editor.schema, content: editor.getJSON() });
};
if (isString(content)) {
await editor.setSerializedContent(content);
}
return editor;
}; };
export default createEditor; export default createEditor;
import {
MarkdownSerializer as ProseMirrorMarkdownSerializer,
defaultMarkdownSerializer,
} from 'prosemirror-markdown';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
/**
* 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.
*
* The client should provide a render function to allow flexibility
* on the desired rendering approach.
*
* @param {Function} params.render Render function
* that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
const create = ({ render = () => null }) => {
return {
/**
* 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: { firstElementChild },
} = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
return state.toJSON();
},
/**
* 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
*/
serialize: ({ schema, content }) => {
const document = schema.nodeFromJSON(content);
const serializer = new ProseMirrorMarkdownSerializer(defaultMarkdownSerializer.nodes, {
...defaultMarkdownSerializer.marks,
bold: {
// creates a bold alias for the strong mark converter
...defaultMarkdownSerializer.marks.strong,
},
});
return serializer.serialize(document);
},
};
};
export default create;
import { MarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown';
const toMarkdown = (document) => {
const serializer = new MarkdownSerializer(defaultMarkdownSerializer.nodes, {
...defaultMarkdownSerializer.marks,
bold: {
// creates a bold alias for the strong mark converter
...defaultMarkdownSerializer.marks.strong,
},
});
return serializer.serialize(document);
};
export default toMarkdown;
...@@ -8564,6 +8564,9 @@ msgstr "" ...@@ -8564,6 +8564,9 @@ msgstr ""
msgid "Contains %{count} blobs of images (%{size})" msgid "Contains %{count} blobs of images (%{size})"
msgstr "" msgstr ""
msgid "ContentEditor|You have to provide a renderMarkdown function or a custom serializer"
msgstr ""
msgid "Contents of .gitlab-ci.yml" msgid "Contents of .gitlab-ci.yml"
msgstr "" msgstr ""
......
import createEditor from '~/content_editor/services/create_editor'; import { createEditor } from '~/content_editor';
import toMarkdown from '~/content_editor/services/to_markdown';
import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples'; import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples';
describe('markdown processing', () => { describe('markdown processing', () => {
// Ensure we generate same markdown that was provided to Markdown API. // Ensure we generate same markdown that was provided to Markdown API.
it.each(loadMarkdownApiExamples())('correctly handles %s', async (testName, markdown) => { it.each(loadMarkdownApiExamples())('correctly handles %s', async (testName, markdown) => {
const { html } = loadMarkdownApiResult(testName); const { html } = loadMarkdownApiResult(testName);
const editor = await createEditor({ content: html }); const editor = await createEditor({ content: markdown, renderMarkdown: () => html });
expect(toMarkdown(editor.state.doc)).toBe(markdown); expect(editor.getSerializedContent()).toBe(markdown);
}); });
}); });
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants';
import createEditor from '~/content_editor/services/create_editor';
import createMarkdownSerializer from '~/content_editor/services/markdown_serializer';
jest.mock('~/content_editor/services/markdown_serializer');
describe('content_editor/services/create_editor', () => {
const buildMockSerializer = () => ({
serialize: jest.fn(),
deserialize: jest.fn(),
});
describe('creating an editor', () => {
it('uses markdown serializer when a renderMarkdown function is provided', async () => {
const renderMarkdown = () => true;
const mockSerializer = buildMockSerializer();
createMarkdownSerializer.mockReturnValueOnce(mockSerializer);
await createEditor({ renderMarkdown });
expect(createMarkdownSerializer).toHaveBeenCalledWith({ render: renderMarkdown });
});
it('uses custom serializer when it is provided', async () => {
const mockSerializer = buildMockSerializer();
const serializedContent = '**bold**';
mockSerializer.serialize.mockReturnValueOnce(serializedContent);
const editor = await createEditor({ serializer: mockSerializer });
expect(editor.getSerializedContent()).toBe(serializedContent);
});
it('throws an error when neither a serializer or renderMarkdown fn are provided', async () => {
await expect(createEditor()).rejects.toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
});
});
});
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