diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 02:50:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 02:50:22 +0300 |
commit | 9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch) | |
tree | 70467ae3692a0e35e5ea56bcb803eb512a10bedb /app/assets/javascripts/content_editor | |
parent | 4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff) |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'app/assets/javascripts/content_editor')
6 files changed, 196 insertions, 0 deletions
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue new file mode 100644 index 00000000000..839d4de912d --- /dev/null +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -0,0 +1,18 @@ +<script> +import { EditorContent } from 'tiptap'; +import createEditor from '../services/create_editor'; + +export default { + components: { + EditorContent, + }, + data() { + return { + editor: createEditor(), + }; + }, +}; +</script> +<template> + <editor-content :editor="editor" /> +</template> diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js new file mode 100644 index 00000000000..eb6deff434d --- /dev/null +++ b/app/assets/javascripts/content_editor/constants.js @@ -0,0 +1,5 @@ +import { s__ } from '~/locale'; + +export const PROVIDE_SERIALIZER_OR_RENDERER_ERROR = s__( + 'ContentEditor|You have to provide a renderMarkdown function or a custom serializer', +); diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js new file mode 100644 index 00000000000..1d050ed208b --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js @@ -0,0 +1,38 @@ +import { CodeBlockHighlight as BaseCodeBlockHighlight } from 'tiptap-extensions'; + +export default class GlCodeBlockHighlight extends BaseCodeBlockHighlight { + get schema() { + const baseSchema = super.schema; + + return { + ...baseSchema, + attrs: { + params: { + default: null, + }, + }, + parseDOM: [ + { + tag: 'pre', + preserveWhitespace: 'full', + getAttrs: (node) => { + const code = node.querySelector('code'); + + if (!code) { + return null; + } + + return { + /* `params` is the name of the attribute that + prosemirror-markdown uses to extract the language + of a codeblock. + https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.js#L62 + */ + params: code.getAttribute('lang'), + }; + }, + }, + ], + }; + } +} diff --git a/app/assets/javascripts/content_editor/index.js b/app/assets/javascripts/content_editor/index.js new file mode 100644 index 00000000000..e6ef3965da1 --- /dev/null +++ b/app/assets/javascripts/content_editor/index.js @@ -0,0 +1,2 @@ +export { default as createEditor } from './services/create_editor'; +export { default as ContentEditor } from './components/content_editor.vue'; diff --git a/app/assets/javascripts/content_editor/services/create_editor.js b/app/assets/javascripts/content_editor/services/create_editor.js new file mode 100644 index 00000000000..128d332b0a2 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/create_editor.js @@ -0,0 +1,60 @@ +import { isFunction, isString } from 'lodash'; +import { Editor } from 'tiptap'; +import { + Bold, + Italic, + Code, + Link, + Image, + Heading, + Blockquote, + HorizontalRule, + BulletList, + OrderedList, + ListItem, +} from 'tiptap-extensions'; +import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants'; +import CodeBlockHighlight from '../extensions/code_block_highlight'; +import createMarkdownSerializer from './markdown_serializer'; + +const createEditor = async ({ content, renderMarkdown, serializer: customSerializer } = {}) => { + if (!customSerializer && !isFunction(renderMarkdown)) { + throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR); + } + + const editor = new Editor({ + extensions: [ + new Bold(), + new Italic(), + new Code(), + new Link(), + new Image(), + new Heading({ levels: [1, 2, 3, 4, 5, 6] }), + new Blockquote(), + new HorizontalRule(), + new BulletList(), + new ListItem(), + new OrderedList(), + new CodeBlockHighlight(), + ], + }); + 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; diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js new file mode 100644 index 00000000000..e3b5775e320 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -0,0 +1,73 @@ +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, + }, + italic: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true }, + }); + + return serializer.serialize(document, { + tightLists: true, + }); + }, + }; +}; + +export default create; |