Commit 0653e298 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'refactor/put-all-serializers-in-one-place' into 'master'

Refactor: Declare all Content Editor Markdown serializers in a single module

See merge request gitlab-org/gitlab!66867
parents f344fa1d 30776c80
...@@ -9,10 +9,9 @@ import { ...@@ -9,10 +9,9 @@ import {
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2'; import { Editor as TiptapEditor } from '@tiptap/vue-2';
import Link from '../extensions/link';
import { hasSelection } from '../services/utils'; import { hasSelection } from '../services/utils';
export const linkContentType = 'link';
export default { export default {
components: { components: {
GlDropdown, GlDropdown,
...@@ -38,12 +37,12 @@ export default { ...@@ -38,12 +37,12 @@ export default {
}, },
computed: { computed: {
isActive() { isActive() {
return this.tiptapEditor.isActive(linkContentType); return this.tiptapEditor.isActive(Link.name);
}, },
}, },
mounted() { mounted() {
this.tiptapEditor.on('selectionUpdate', ({ editor }) => { this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
const { canonicalSrc, href } = editor.getAttributes(linkContentType); const { canonicalSrc, href } = editor.getAttributes(Link.name);
this.linkHref = canonicalSrc || href; this.linkHref = canonicalSrc || href;
}); });
...@@ -60,20 +59,20 @@ export default { ...@@ -60,20 +59,20 @@ export default {
}) })
.run(); .run();
this.$emit('execute', { contentType: linkContentType }); this.$emit('execute', { contentType: Link.name });
}, },
selectLink() { selectLink() {
const { tiptapEditor } = this; const { tiptapEditor } = this;
// a selection has already been made by the user, so do nothing // a selection has already been made by the user, so do nothing
if (!hasSelection(tiptapEditor)) { if (!hasSelection(tiptapEditor)) {
tiptapEditor.chain().focus().extendMarkRange(linkContentType).run(); tiptapEditor.chain().focus().extendMarkRange(Link.name).run();
} }
}, },
removeLink() { removeLink() {
this.tiptapEditor.chain().focus().unsetLink().run(); this.tiptapEditor.chain().focus().unsetLink().run();
this.$emit('execute', { contentType: linkContentType }); this.$emit('execute', { contentType: Link.name });
}, },
}, },
}; };
......
import { Blockquote } from '@tiptap/extension-blockquote'; export { Blockquote as default } from '@tiptap/extension-blockquote';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = Blockquote;
export const serializer = defaultMarkdownSerializer.nodes.blockquote;
import { Bold } from '@tiptap/extension-bold'; export { Bold as default } from '@tiptap/extension-bold';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = Bold;
export const serializer = defaultMarkdownSerializer.marks.strong;
import { BulletList } from '@tiptap/extension-bullet-list'; export { BulletList as default } from '@tiptap/extension-bullet-list';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = BulletList;
export const serializer = defaultMarkdownSerializer.nodes.bullet_list;
import { Code } from '@tiptap/extension-code'; export { Code as default } from '@tiptap/extension-code';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = Code;
export const serializer = defaultMarkdownSerializer.marks.code;
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'; import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import * as lowlight from 'lowlight'; import * as lowlight from 'lowlight';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
const extractLanguage = (element) => element.getAttribute('lang'); const extractLanguage = (element) => element.getAttribute('lang');
const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({ export default CodeBlockLowlight.extend({
addAttributes() { addAttributes() {
return { return {
language: { language: {
...@@ -38,6 +37,3 @@ const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({ ...@@ -38,6 +37,3 @@ const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({
}).configure({ }).configure({
lowlight, lowlight,
}); });
export const tiptapExtension = ExtendedCodeBlockLowlight;
export const serializer = defaultMarkdownSerializer.nodes.code_block;
import Document from '@tiptap/extension-document'; export { Document as default } from '@tiptap/extension-document';
export const tiptapExtension = Document;
import Dropcursor from '@tiptap/extension-dropcursor'; export { Dropcursor as default } from '@tiptap/extension-dropcursor';
export const tiptapExtension = Dropcursor;
import Gapcursor from '@tiptap/extension-gapcursor'; export { Gapcursor as default } from '@tiptap/extension-gapcursor';
export const tiptapExtension = Gapcursor;
import { HardBreak } from '@tiptap/extension-hard-break'; import { HardBreak } from '@tiptap/extension-hard-break';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
const ExtendedHardBreak = HardBreak.extend({ export default HardBreak.extend({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
'Shift-Enter': () => this.editor.commands.setHardBreak(), 'Shift-Enter': () => this.editor.commands.setHardBreak(),
}; };
}, },
}); });
export const tiptapExtension = ExtendedHardBreak;
export const serializer = defaultMarkdownSerializer.nodes.hard_break;
import { Heading } from '@tiptap/extension-heading'; export { Heading as default } from '@tiptap/extension-heading';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = Heading;
export const serializer = defaultMarkdownSerializer.nodes.heading;
import History from '@tiptap/extension-history'; export { History as default } from '@tiptap/extension-history';
export const tiptapExtension = History;
import { nodeInputRule } from '@tiptap/core'; import { nodeInputRule } from '@tiptap/core';
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'; import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const hrInputRuleRegExp = /^---$/; export const hrInputRuleRegExp = /^---$/;
export const tiptapExtension = HorizontalRule.extend({ export default HorizontalRule.extend({
addInputRules() { addInputRules() {
return [nodeInputRule(hrInputRuleRegExp, this.type)]; return [nodeInputRule(hrInputRuleRegExp, this.type)];
}, },
}); });
export const serializer = defaultMarkdownSerializer.nodes.horizontal_rule;
...@@ -48,11 +48,12 @@ const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => { ...@@ -48,11 +48,12 @@ const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
return false; return false;
}; };
const ExtendedImage = Image.extend({ export default Image.extend({
defaultOptions: { defaultOptions: {
...Image.options, ...Image.options,
uploadsPath: null, uploadsPath: null,
renderMarkdown: null, renderMarkdown: null,
inline: true,
}, },
addAttributes() { addAttributes() {
return { return {
...@@ -152,17 +153,3 @@ const ExtendedImage = Image.extend({ ...@@ -152,17 +153,3 @@ const ExtendedImage = Image.extend({
return VueNodeViewRenderer(ImageWrapper); return VueNodeViewRenderer(ImageWrapper);
}, },
}); });
const serializer = (state, node) => {
const { alt, canonicalSrc, src, title } = node.attrs;
const quotedTitle = title ? ` ${state.quote(title)}` : '';
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
};
export const configure = ({ renderMarkdown, uploadsPath }) => {
return {
tiptapExtension: ExtendedImage.configure({ inline: true, renderMarkdown, uploadsPath }),
serializer,
};
};
import { Italic } from '@tiptap/extension-italic'; export { Italic as default } from '@tiptap/extension-italic';
export const tiptapExtension = Italic;
export const serializer = { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true };
...@@ -20,7 +20,7 @@ export const extractHrefFromMarkdownLink = (match) => { ...@@ -20,7 +20,7 @@ export const extractHrefFromMarkdownLink = (match) => {
return extractHrefFromMatch(match); return extractHrefFromMatch(match);
}; };
export const tiptapExtension = Link.extend({ export default Link.extend({
addInputRules() { addInputRules() {
return [ return [
markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink), markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink),
...@@ -51,13 +51,3 @@ export const tiptapExtension = Link.extend({ ...@@ -51,13 +51,3 @@ export const tiptapExtension = Link.extend({
}).configure({ }).configure({
openOnClick: false, openOnClick: false,
}); });
export const serializer = {
open() {
return '[';
},
close(state, mark) {
const href = mark.attrs.canonicalSrc || mark.attrs.href;
return `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
},
};
import { ListItem } from '@tiptap/extension-list-item'; export { ListItem as default } from '@tiptap/extension-list-item';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = ListItem;
export const serializer = defaultMarkdownSerializer.nodes.list_item;
import { OrderedList } from '@tiptap/extension-ordered-list'; export { OrderedList as default } from '@tiptap/extension-ordered-list';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = OrderedList;
export const serializer = defaultMarkdownSerializer.nodes.ordered_list;
import { Paragraph } from '@tiptap/extension-paragraph'; export { Paragraph as default } from '@tiptap/extension-paragraph';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = Paragraph;
export const serializer = defaultMarkdownSerializer.nodes.paragraph;
import { Strike } from '@tiptap/extension-strike'; export { Strike as default } from '@tiptap/extension-strike';
export const tiptapExtension = Strike;
export const serializer = {
open: '~~',
close: '~~',
mixable: true,
expelEnclosingWhitespace: true,
};
import { Table } from '@tiptap/extension-table'; export { Table as default } from '@tiptap/extension-table';
export const tiptapExtension = Table;
export function serializer(state, node) {
state.renderContent(node);
}
import { TableCell } from '@tiptap/extension-table-cell'; import { TableCell } from '@tiptap/extension-table-cell';
export const tiptapExtension = TableCell.extend({ export default TableCell.extend({
content: 'inline*', content: 'inline*',
}); });
export function serializer(state, node) {
state.renderInline(node);
}
import { TableHeader } from '@tiptap/extension-table-header'; import { TableHeader } from '@tiptap/extension-table-header';
export const tiptapExtension = TableHeader.extend({ export default TableHeader.extend({
content: 'inline*', content: 'inline*',
}); });
export function serializer(state, node) {
state.renderInline(node);
}
import { TableRow } from '@tiptap/extension-table-row'; import { TableRow } from '@tiptap/extension-table-row';
export const tiptapExtension = TableRow.extend({ export default TableRow.extend({
allowGapCursor: false, allowGapCursor: false,
}); });
export function serializer(state, node) {
const isHeaderRow = node.child(0).type.name === 'tableHeader';
const renderRow = () => {
const cellWidths = [];
state.flushClose(1);
state.write('| ');
node.forEach((cell, _, i) => {
if (i) state.write(' | ');
const { length } = state.out;
state.render(cell, node, i);
cellWidths.push(state.out.length - length);
});
state.write(' |');
state.closeBlock(node);
return cellWidths;
};
const renderHeaderRow = (cellWidths) => {
state.flushClose(1);
state.write('|');
node.forEach((cell, _, i) => {
if (i) state.write('|');
state.write(cell.attrs.align === 'center' ? ':' : '-');
state.write(state.repeat('-', cellWidths[i]));
state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
});
state.write('|');
state.closeBlock(node);
};
if (isHeaderRow) {
renderHeaderRow(renderRow());
} else {
renderRow();
}
}
import { Text } from '@tiptap/extension-text'; export { Text as default } from '@tiptap/extension-text';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const tiptapExtension = Text;
export const serializer = defaultMarkdownSerializer.nodes.text;
const buildSerializerConfig = (extensions = []) =>
extensions
.filter(({ serializer }) => serializer)
.reduce(
(serializers, { serializer, tiptapExtension: { name, type } }) => {
const collection = `${type}s`;
return {
...serializers,
[collection]: {
...serializers[collection],
[name]: serializer,
},
};
},
{
nodes: {},
marks: {},
},
);
export default buildSerializerConfig;
import { Editor } from '@tiptap/vue-2'; import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash'; import { isFunction } from 'lodash';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants'; import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import * as Blockquote from '../extensions/blockquote'; import Blockquote from '../extensions/blockquote';
import * as Bold from '../extensions/bold'; import Bold from '../extensions/bold';
import * as BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
import * as Code from '../extensions/code'; import Code from '../extensions/code';
import * as CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import * as Document from '../extensions/document'; import Document from '../extensions/document';
import * as Dropcursor from '../extensions/dropcursor'; import Dropcursor from '../extensions/dropcursor';
import * as Gapcursor from '../extensions/gapcursor'; import Gapcursor from '../extensions/gapcursor';
import * as HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
import * as Heading from '../extensions/heading'; import Heading from '../extensions/heading';
import * as History from '../extensions/history'; import History from '../extensions/history';
import * as HorizontalRule from '../extensions/horizontal_rule'; import HorizontalRule from '../extensions/horizontal_rule';
import * as Image from '../extensions/image'; import Image from '../extensions/image';
import * as Italic from '../extensions/italic'; import Italic from '../extensions/italic';
import * as Link from '../extensions/link'; import Link from '../extensions/link';
import * as ListItem from '../extensions/list_item'; import ListItem from '../extensions/list_item';
import * as OrderedList from '../extensions/ordered_list'; import OrderedList from '../extensions/ordered_list';
import * as Paragraph from '../extensions/paragraph'; import Paragraph from '../extensions/paragraph';
import * as Strike from '../extensions/strike'; import Strike from '../extensions/strike';
import * as Table from '../extensions/table'; import Table from '../extensions/table';
import * as TableCell from '../extensions/table_cell'; import TableCell from '../extensions/table_cell';
import * as TableHeader from '../extensions/table_header'; import TableHeader from '../extensions/table_header';
import * as TableRow from '../extensions/table_row'; import TableRow from '../extensions/table_row';
import * as Text from '../extensions/text'; import Text from '../extensions/text';
import buildSerializerConfig from './build_serializer_config';
import { ContentEditor } from './content_editor'; import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer'; import createMarkdownSerializer from './markdown_serializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts'; import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
const collectTiptapExtensions = (extensions = []) =>
extensions.map(({ tiptapExtension }) => tiptapExtension);
const createTiptapEditor = ({ extensions = [], ...options } = {}) => const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({ new Editor({
extensions: [...extensions], extensions: [...extensions],
...@@ -48,6 +44,7 @@ export const createContentEditor = ({ ...@@ -48,6 +44,7 @@ export const createContentEditor = ({
renderMarkdown, renderMarkdown,
uploadsPath, uploadsPath,
extensions = [], extensions = [],
serializerConfig = { marks: {}, nodes: {} },
tiptapOptions, tiptapOptions,
} = {}) => { } = {}) => {
if (!isFunction(renderMarkdown)) { if (!isFunction(renderMarkdown)) {
...@@ -82,9 +79,8 @@ export const createContentEditor = ({ ...@@ -82,9 +79,8 @@ export const createContentEditor = ({
]; ];
const allExtensions = [...builtInContentEditorExtensions, ...extensions]; const allExtensions = [...builtInContentEditorExtensions, ...extensions];
const tiptapExtensions = collectTiptapExtensions(allExtensions).map(trackInputRulesAndShortcuts); const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
const tiptapEditor = createTiptapEditor({ extensions: tiptapExtensions, ...tiptapOptions }); const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializerConfig = buildSerializerConfig(allExtensions);
const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig }); const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
return new ContentEditor({ tiptapEditor, serializer }); return new ContentEditor({ tiptapEditor, serializer });
......
import { MarkdownSerializer as ProseMirrorMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown'; import {
MarkdownSerializer as ProseMirrorMarkdownSerializer,
defaultMarkdownSerializer,
} from 'prosemirror-markdown/src/to_markdown';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import Blockquote from '../extensions/blockquote';
import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule';
import Image from '../extensions/image';
import Italic from '../extensions/italic';
import Link from '../extensions/link';
import ListItem from '../extensions/list_item';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import Strike from '../extensions/strike';
import Table from '../extensions/table';
import TableCell from '../extensions/table_cell';
import TableHeader from '../extensions/table_header';
import TableRow from '../extensions/table_row';
import Text from '../extensions/text';
const defaultSerializerConfig = {
marks: {
[Bold.name]: defaultMarkdownSerializer.marks.strong,
[Code.name]: defaultMarkdownSerializer.marks.code,
[Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
[Link.name]: {
open() {
return '[';
},
close(state, mark) {
const href = mark.attrs.canonicalSrc || mark.attrs.href;
return `](${state.esc(href)}${
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''
})`;
},
},
[Strike.name]: {
open: '~~',
close: '~~',
mixable: true,
expelEnclosingWhitespace: true,
},
},
nodes: {
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
[CodeBlockHighlight.name]: defaultMarkdownSerializer.nodes.code_block,
[HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
[Image.name]: (state, node) => {
const { alt, canonicalSrc, src, title } = node.attrs;
const quotedTitle = title ? ` ${state.quote(title)}` : '';
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
},
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
[OrderedList.name]: defaultMarkdownSerializer.nodes.ordered_list,
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
[Table.name]: (state, node) => {
state.renderContent(node);
},
[TableCell.name]: (state, node) => {
state.renderInline(node);
},
[TableHeader.name]: (state, node) => {
state.renderInline(node);
},
[TableRow.name]: (state, node) => {
const isHeaderRow = node.child(0).type.name === 'tableHeader';
const renderRow = () => {
const cellWidths = [];
state.flushClose(1);
state.write('| ');
node.forEach((cell, _, i) => {
if (i) state.write(' | ');
const { length } = state.out;
state.render(cell, node, i);
cellWidths.push(state.out.length - length);
});
state.write(' |');
state.closeBlock(node);
return cellWidths;
};
const renderHeaderRow = (cellWidths) => {
state.flushClose(1);
state.write('|');
node.forEach((cell, _, i) => {
if (i) state.write('|');
state.write(cell.attrs.align === 'center' ? ':' : '-');
state.write(state.repeat('-', cellWidths[i]));
state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
});
state.write('|');
state.closeBlock(node);
};
if (isHeaderRow) {
renderHeaderRow(renderRow());
} else {
renderRow();
}
},
[Text.name]: defaultMarkdownSerializer.nodes.text,
},
};
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`; const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
...@@ -50,8 +170,16 @@ export default ({ render = () => null, serializerConfig }) => ({ ...@@ -50,8 +170,16 @@ export default ({ render = () => null, serializerConfig }) => ({
*/ */
serialize: ({ schema, content }) => { serialize: ({ schema, content }) => {
const proseMirrorDocument = schema.nodeFromJSON(content); const proseMirrorDocument = schema.nodeFromJSON(content);
const { nodes, marks } = serializerConfig; const serializer = new ProseMirrorMarkdownSerializer(
const serializer = new ProseMirrorMarkdownSerializer(nodes, marks); {
...defaultSerializerConfig.nodes,
...serializerConfig.nodes,
},
{
...defaultSerializerConfig.marks,
...serializerConfig.marks,
},
);
return serializer.serialize(proseMirrorDocument, { return serializer.serialize(proseMirrorDocument, {
tightLists: true, tightLists: true,
......
import { GlButton, GlFormInputGroup } from '@gitlab/ui'; import { GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue'; import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
import { configure as configureImageExtension } from '~/content_editor/extensions/image'; import Image from '~/content_editor/extensions/image';
import { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_image_button', () => { describe('content_editor/components/toolbar_image_button', () => {
...@@ -29,13 +29,13 @@ describe('content_editor/components/toolbar_image_button', () => { ...@@ -29,13 +29,13 @@ describe('content_editor/components/toolbar_image_button', () => {
}; };
beforeEach(() => { beforeEach(() => {
const { tiptapExtension: Image } = configureImageExtension({ editor = createTestEditor({
extensions: [
Image.configure({
renderMarkdown: jest.fn(), renderMarkdown: jest.fn(),
uploadsPath: '/uploads/', uploadsPath: '/uploads/',
}); }),
],
editor = createTestEditor({
extensions: [Image],
}); });
buildWrapper(); buildWrapper();
......
import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui'; import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue'; import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
import { tiptapExtension as Link } from '~/content_editor/extensions/link'; import Link from '~/content_editor/extensions/link';
import { hasSelection } from '~/content_editor/services/utils'; import { hasSelection } from '~/content_editor/services/utils';
import { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands } from '../test_utils';
...@@ -25,9 +25,7 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -25,9 +25,7 @@ describe('content_editor/components/toolbar_link_button', () => {
const findRemoveLinkButton = () => wrapper.findByText('Remove link'); const findRemoveLinkButton = () => wrapper.findByText('Remove link');
beforeEach(() => { beforeEach(() => {
editor = createTestEditor({ editor = createTestEditor();
extensions: [Link],
});
}); });
afterEach(() => { afterEach(() => {
......
import { GlDropdown, GlButton } from '@gitlab/ui'; import { GlDropdown, GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue'; import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue';
import { tiptapExtension as Table } from '~/content_editor/extensions/table';
import { tiptapExtension as TableCell } from '~/content_editor/extensions/table_cell';
import { tiptapExtension as TableHeader } from '~/content_editor/extensions/table_header';
import { tiptapExtension as TableRow } from '~/content_editor/extensions/table_row';
import { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_table_button', () => { describe('content_editor/components/toolbar_table_button', () => {
...@@ -23,9 +19,7 @@ describe('content_editor/components/toolbar_table_button', () => { ...@@ -23,9 +19,7 @@ describe('content_editor/components/toolbar_table_button', () => {
const getNumButtons = () => findDropdown().findAllComponents(GlButton).length; const getNumButtons = () => findDropdown().findAllComponents(GlButton).length;
beforeEach(() => { beforeEach(() => {
editor = createTestEditor({ editor = createTestEditor();
extensions: [Table, TableCell, TableRow, TableHeader],
});
buildWrapper(); buildWrapper();
}); });
......
...@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; ...@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue'; import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants'; import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
import { tiptapExtension as Heading } from '~/content_editor/extensions/heading'; import Heading from '~/content_editor/extensions/heading';
import { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_headings_dropdown', () => { describe('content_editor/components/toolbar_headings_dropdown', () => {
......
import { tiptapExtension as CodeBlockHighlight } from '~/content_editor/extensions/code_block_highlight'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import { loadMarkdownApiResult } from '../markdown_processing_examples'; import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor } from '../test_utils'; import { createTestEditor } from '../test_utils';
......
import { tiptapExtension as HardBreak } from '~/content_editor/extensions/hard_break'; import HardBreak from '~/content_editor/extensions/hard_break';
import { createTestEditor, createDocBuilder } from '../test_utils'; import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/extensions/hard_break', () => { describe('content_editor/extensions/hard_break', () => {
......
...@@ -2,7 +2,7 @@ import axios from 'axios'; ...@@ -2,7 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { once } from 'lodash'; import { once } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import * as Image from '~/content_editor/extensions/image'; import Image from '~/content_editor/extensions/image';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { loadMarkdownApiResult } from '../markdown_processing_examples'; import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils'; import { createTestEditor, createDocBuilder } from '../test_utils';
...@@ -24,16 +24,16 @@ describe('content_editor/extensions/image', () => { ...@@ -24,16 +24,16 @@ describe('content_editor/extensions/image', () => {
.fn() .fn()
.mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image').body); .mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image').body);
const { tiptapExtension } = Image.configure({ renderMarkdown, uploadsPath }); tiptapEditor = createTestEditor({
extensions: [Image.configure({ renderMarkdown, uploadsPath })],
tiptapEditor = createTestEditor({ extensions: [tiptapExtension] }); });
({ ({
builders: { doc, p, image }, builders: { doc, p, image },
eq, eq,
} = createDocBuilder({ } = createDocBuilder({
tiptapEditor, tiptapEditor,
names: { image: { nodeType: tiptapExtension.name } }, names: { image: { nodeType: Image.name } },
})); }));
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
......
import * as Blockquote from '~/content_editor/extensions/blockquote';
import * as Bold from '~/content_editor/extensions/bold';
import * as Dropcursor from '~/content_editor/extensions/dropcursor';
import * as Paragraph from '~/content_editor/extensions/paragraph';
import buildSerializerConfig from '~/content_editor/services/build_serializer_config';
describe('content_editor/services/build_serializer_config', () => {
describe('given one or more content editor extensions', () => {
it('creates a serializer config that collects all extension serializers by type', () => {
const extensions = [Bold, Blockquote, Paragraph];
const serializerConfig = buildSerializerConfig(extensions);
extensions.forEach(({ tiptapExtension, serializer }) => {
const { name, type } = tiptapExtension;
expect(serializerConfig[`${type}s`][name]).toBe(serializer);
});
});
});
describe('given an extension without serializer', () => {
it('does not include the extension in the serializer config', () => {
const serializerConfig = buildSerializerConfig([Dropcursor]);
expect(serializerConfig.marks[Dropcursor.tiptapExtension.name]).toBe(undefined);
expect(serializerConfig.nodes[Dropcursor.tiptapExtension.name]).toBe(undefined);
});
});
describe('given no extensions', () => {
it('creates an empty serializer config', () => {
expect(buildSerializerConfig()).toStrictEqual({
marks: {},
nodes: {},
});
});
});
});
...@@ -32,13 +32,15 @@ describe('content_editor/services/create_editor', () => { ...@@ -32,13 +32,15 @@ describe('content_editor/services/create_editor', () => {
it('allows providing external content editor extensions', async () => { it('allows providing external content editor extensions', async () => {
const labelReference = 'this is a ~group::editor'; const labelReference = 'this is a ~group::editor';
const { tiptapExtension, serializer } = createTestContentEditorExtension();
renderMarkdown.mockReturnValueOnce( renderMarkdown.mockReturnValueOnce(
'<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>', '<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>',
); );
editor = createContentEditor({ editor = createContentEditor({
renderMarkdown, renderMarkdown,
extensions: [createTestContentEditorExtension()], extensions: [tiptapExtension],
serializerConfig: { nodes: { [tiptapExtension.name]: serializer } },
}); });
await editor.setSerializedContent(labelReference); await editor.setSerializedContent(labelReference);
......
...@@ -4,10 +4,10 @@ import { ...@@ -4,10 +4,10 @@ import {
INPUT_RULE_TRACKING_ACTION, INPUT_RULE_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL, CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants'; } from '~/content_editor/constants';
import { tiptapExtension as BulletList } from '~/content_editor/extensions/bullet_list'; import BulletList from '~/content_editor/extensions/bullet_list';
import { tiptapExtension as CodeBlockLowlight } from '~/content_editor/extensions/code_block_highlight'; import CodeBlockLowlight from '~/content_editor/extensions/code_block_highlight';
import { tiptapExtension as Heading } from '~/content_editor/extensions/heading'; import Heading from '~/content_editor/extensions/heading';
import { tiptapExtension as ListItem } from '~/content_editor/extensions/list_item'; import ListItem from '~/content_editor/extensions/list_item';
import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts'; import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys'; import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
import { createTestEditor } from '../test_utils'; import { createTestEditor } from '../test_utils';
......
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