Commit ddb2c3ae authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch 'renovate/content-editor-packages' into 'master'

Update Content Editor Packages

See merge request gitlab-org/gitlab!72005
parents c1c86848 eee97b89
import { Blockquote } from '@tiptap/extension-blockquote'; import { Blockquote } from '@tiptap/extension-blockquote';
import { wrappingInputRule } from 'prosemirror-inputrules'; import { wrappingInputRule } from '@tiptap/core';
import { getParents } from '~/lib/utils/dom_utils'; import { getParents } from '~/lib/utils/dom_utils';
import { getMarkdownSource } from '../services/markdown_sourcemap'; import { getMarkdownSource } from '../services/markdown_sourcemap';
export const multilineInputRegex = /^\s*>>>\s$/gm;
export default Blockquote.extend({ export default Blockquote.extend({
addAttributes() { addAttributes() {
return { return {
...@@ -25,9 +23,15 @@ export default Blockquote.extend({ ...@@ -25,9 +23,15 @@ export default Blockquote.extend({
}, },
addInputRules() { addInputRules() {
const multilineInputRegex = /^\s*>>>\s$/gm;
return [ return [
...this.parent?.(), ...this.parent?.(),
wrappingInputRule(multilineInputRegex, this.type, () => ({ multiline: true })), wrappingInputRule({
find: multilineInputRegex,
type: this.type,
getAttributes: () => ({ multiline: true }),
}),
]; ];
}, },
}); });
import { Node, mergeAttributes } from '@tiptap/core'; import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { wrappingInputRule } from 'prosemirror-inputrules';
export const inputRegex = /^\s*(<dl>)$/;
export default Node.create({ export default Node.create({
name: 'descriptionList', name: 'descriptionList',
...@@ -18,6 +15,8 @@ export default Node.create({ ...@@ -18,6 +15,8 @@ export default Node.create({
}, },
addInputRules() { addInputRules() {
return [wrappingInputRule(inputRegex, this.type)]; const inputRegex = /^\s*(<dl>)$/;
return [wrappingInputRule({ find: inputRegex, type: this.type })];
}, },
}); });
import { Node } from '@tiptap/core'; import { Node, wrappingInputRule } from '@tiptap/core';
import { VueNodeViewRenderer } from '@tiptap/vue-2'; import { VueNodeViewRenderer } from '@tiptap/vue-2';
import { wrappingInputRule } from 'prosemirror-inputrules';
import DetailsWrapper from '../components/wrappers/details.vue'; import DetailsWrapper from '../components/wrappers/details.vue';
export const inputRegex = /^\s*(<details>)$/;
export default Node.create({ export default Node.create({
name: 'details', name: 'details',
content: 'detailsContent+', content: 'detailsContent+',
...@@ -24,7 +21,9 @@ export default Node.create({ ...@@ -24,7 +21,9 @@ export default Node.create({
}, },
addInputRules() { addInputRules() {
return [wrappingInputRule(inputRegex, this.type)]; const inputRegex = /^\s*(<details>)$/;
return [wrappingInputRule({ find: inputRegex, type: this.type })];
}, },
addCommands() { addCommands() {
......
import { Node } from '@tiptap/core'; import { Node, InputRule } from '@tiptap/core';
import { InputRule } from 'prosemirror-inputrules';
import { initEmojiMap, getAllEmoji } from '~/emoji'; import { initEmojiMap, getAllEmoji } from '~/emoji';
export const emojiInputRegex = /(?:^|\s)((?::)((?:\w+))(?::))$/;
export default Node.create({ export default Node.create({
name: 'emoji', name: 'emoji',
...@@ -54,15 +51,19 @@ export default Node.create({ ...@@ -54,15 +51,19 @@ export default Node.create({
}, },
addInputRules() { addInputRules() {
const emojiInputRegex = /(?:^|\s)(:(\w+):)$/;
return [ return [
new InputRule(emojiInputRegex, (state, match, start, end) => { new InputRule({
find: emojiInputRegex,
handler: ({ state, range: { from, to }, match }) => {
const [, , name] = match; const [, , name] = match;
const emojis = getAllEmoji(); const emojis = getAllEmoji();
const emoji = emojis[name]; const emoji = emojis[name];
const { tr } = state; const { tr } = state;
if (emoji) { if (emoji) {
tr.replaceWith(start, end, [ tr.replaceWith(from, to, [
state.schema.text(' '), state.schema.text(' '),
this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }), this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
]); ]);
...@@ -71,6 +72,7 @@ export default Node.create({ ...@@ -71,6 +72,7 @@ export default Node.create({
} }
return null; return null;
},
}), }),
]; ];
}, },
......
import { nodeInputRule } from '@tiptap/core'; import { nodeInputRule } from '@tiptap/core';
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'; import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
export const hrInputRuleRegExp = /^---$/;
export default HorizontalRule.extend({ export default HorizontalRule.extend({
addInputRules() { addInputRules() {
return [nodeInputRule(hrInputRuleRegExp, this.type)]; const hrInputRuleRegExp = /^---$/;
return [nodeInputRule({ find: hrInputRuleRegExp, type: this.type })];
}, },
}); });
...@@ -60,7 +60,13 @@ export default marks.map((name) => ...@@ -60,7 +60,13 @@ export default marks.map((name) =>
}, },
addInputRules() { addInputRules() {
return [markInputRule(markInputRegex(name), this.type, extractMarkAttributesFromMatch)]; return [
markInputRule({
find: markInputRegex(name),
type: this.type,
getAttributes: extractMarkAttributesFromMatch,
}),
];
}, },
}), }),
); );
import { Mark, markInputRule, mergeAttributes } from '@tiptap/core'; import { Mark, markInputRule, mergeAttributes } from '@tiptap/core';
export const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
export const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
export default Mark.create({ export default Mark.create({
name: 'inlineDiff', name: 'inlineDiff',
...@@ -38,9 +35,20 @@ export default Mark.create({ ...@@ -38,9 +35,20 @@ export default Mark.create({
}, },
addInputRules() { addInputRules() {
const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
return [ return [
markInputRule(inputRegexAddition, this.type, () => ({ type: 'addition' })), markInputRule({
markInputRule(inputRegexDeletion, this.type, () => ({ type: 'deletion' })), find: inputRegexAddition,
type: this.type,
getAttributes: () => ({ type: 'addition' }),
}),
markInputRule({
find: inputRegexDeletion,
type: this.type,
getAttributes: () => ({ type: 'deletion' }),
}),
]; ];
}, },
}); });
import { markInputRule } from '@tiptap/core'; import { markInputRule } from '@tiptap/core';
import { Link } from '@tiptap/extension-link'; import { Link } from '@tiptap/extension-link';
export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
const extractHrefFromMatch = (match) => { const extractHrefFromMatch = (match) => {
return { href: match.groups.href }; return { href: match.groups.href };
}; };
...@@ -26,9 +23,20 @@ export default Link.extend({ ...@@ -26,9 +23,20 @@ export default Link.extend({
openOnClick: false, openOnClick: false,
}, },
addInputRules() { addInputRules() {
const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
return [ return [
markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink), markInputRule({
markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch), find: markdownLinkSyntaxInputRuleRegExp,
type: this.type,
getAttributes: extractHrefFromMarkdownLink,
}),
markInputRule({
find: urlSyntaxRegExp,
type: this.type,
getAttributes: extractHrefFromMatch,
}),
]; ];
}, },
addAttributes() { addAttributes() {
......
...@@ -2,8 +2,6 @@ import { Mark, markInputRule } from '@tiptap/core'; ...@@ -2,8 +2,6 @@ import { Mark, markInputRule } from '@tiptap/core';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm;
export default Mark.create({ export default Mark.create({
name: 'mathInline', name: 'mathInline',
...@@ -30,6 +28,8 @@ export default Mark.create({ ...@@ -30,6 +28,8 @@ export default Mark.create({
}, },
addInputRules() { addInputRules() {
return [markInputRule(inputRegex, this.type)]; const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm;
return [markInputRule({ find: inputRegex, type: this.type })];
}, },
}); });
...@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark ...@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark
export default Subscript.extend({ export default Subscript.extend({
addInputRules() { addInputRules() {
return [markInputRule(markInputRegex('sub'), this.type, extractMarkAttributesFromMatch)]; return [
markInputRule({
find: markInputRegex('sub'),
type: this.type,
getAttributes: extractMarkAttributesFromMatch,
}),
];
}, },
}); });
...@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark ...@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark
export default Superscript.extend({ export default Superscript.extend({
addInputRules() { addInputRules() {
return [markInputRule(markInputRegex('sup'), this.type, extractMarkAttributesFromMatch)]; return [
markInputRule({
find: markInputRegex('sup'),
type: this.type,
getAttributes: extractMarkAttributesFromMatch,
}),
];
}, },
}); });
import { Node } from '@tiptap/core'; import { Node, InputRule } from '@tiptap/core';
import { InputRule } from 'prosemirror-inputrules';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/];
export default Node.create({ export default Node.create({
name: 'tableOfContents', name: 'tableOfContents',
...@@ -34,17 +31,21 @@ export default Node.create({ ...@@ -34,17 +31,21 @@ export default Node.create({
addInputRules() { addInputRules() {
const { type } = this; const { type } = this;
const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/];
return inputRuleRegExps.map( return inputRuleRegExps.map(
(regex) => (regex) =>
new InputRule(regex, (state, match, start, end) => { new InputRule({
find: regex,
handler: ({ state, range: { from, to }, match }) => {
const { tr } = state; const { tr } = state;
if (match) { if (match) {
tr.replaceWith(start - 1, end, type.create()); tr.replaceWith(from - 1, to, type.create());
} }
return tr; return tr;
},
}), }),
); );
}, },
......
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core'; import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
export const inputRegex = /^<wbr>$/;
export default Node.create({ export default Node.create({
name: 'wordBreak', name: 'wordBreak',
inline: true, inline: true,
...@@ -24,6 +22,8 @@ export default Node.create({ ...@@ -24,6 +22,8 @@ export default Node.create({
}, },
addInputRules() { addInputRules() {
return [nodeInputRule(inputRegex, this.type)]; const inputRegex = /^<wbr>$/;
return [nodeInputRule({ find: inputRegex, type: this.type })];
}, },
}); });
import { mapValues } from 'lodash'; import { mapValues } from 'lodash';
import { InputRule } from 'prosemirror-inputrules'; import { InputRule } from '@tiptap/core';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys'; import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import {
...@@ -17,10 +17,12 @@ const trackKeyboardShortcut = (contentType, commandFn, shortcut) => () => { ...@@ -17,10 +17,12 @@ const trackKeyboardShortcut = (contentType, commandFn, shortcut) => () => {
}; };
const trackInputRule = (contentType, inputRule) => { const trackInputRule = (contentType, inputRule) => {
return new InputRule(inputRule.match, (...args) => { return new InputRule({
find: inputRule.find,
handler: (...args) => {
const result = inputRule.handler(...args); const result = inputRule.handler(...args);
if (result) { if (result !== null) {
Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, { Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL, label: CONTENT_EDITOR_TRACKING_LABEL,
property: contentType, property: contentType,
...@@ -28,6 +30,7 @@ const trackInputRule = (contentType, inputRule) => { ...@@ -28,6 +30,7 @@ const trackInputRule = (contentType, inputRule) => {
} }
return result; return result;
},
}); });
}; };
......
...@@ -63,36 +63,36 @@ ...@@ -63,36 +63,36 @@
"@rails/ujs": "6.1.4-1", "@rails/ujs": "6.1.4-1",
"@sentry/browser": "5.30.0", "@sentry/browser": "5.30.0",
"@sourcegraph/code-host-integration": "0.0.60", "@sourcegraph/code-host-integration": "0.0.60",
"@tiptap/core": "^2.0.0-beta.118", "@tiptap/core": "^2.0.0-beta.125",
"@tiptap/extension-blockquote": "^2.0.0-beta.15", "@tiptap/extension-blockquote": "^2.0.0-beta.19",
"@tiptap/extension-bold": "^2.0.0-beta.15", "@tiptap/extension-bold": "^2.0.0-beta.19",
"@tiptap/extension-bullet-list": "^2.0.0-beta.15", "@tiptap/extension-bullet-list": "^2.0.0-beta.18",
"@tiptap/extension-code": "^2.0.0-beta.16", "@tiptap/extension-code": "^2.0.0-beta.20",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.41", "@tiptap/extension-code-block-lowlight": "2.0.0-beta.47",
"@tiptap/extension-document": "^2.0.0-beta.13", "@tiptap/extension-document": "^2.0.0-beta.13",
"@tiptap/extension-dropcursor": "^2.0.0-beta.19", "@tiptap/extension-dropcursor": "^2.0.0-beta.19",
"@tiptap/extension-gapcursor": "^2.0.0-beta.24", "@tiptap/extension-gapcursor": "^2.0.0-beta.27",
"@tiptap/extension-hard-break": "^2.0.0-beta.21", "@tiptap/extension-hard-break": "^2.0.0-beta.24",
"@tiptap/extension-heading": "^2.0.0-beta.15", "@tiptap/extension-heading": "^2.0.0-beta.18",
"@tiptap/extension-history": "^2.0.0-beta.16", "@tiptap/extension-history": "^2.0.0-beta.16",
"@tiptap/extension-horizontal-rule": "^2.0.0-beta.21", "@tiptap/extension-horizontal-rule": "^2.0.0-beta.24",
"@tiptap/extension-image": "^2.0.0-beta.15", "@tiptap/extension-image": "^2.0.0-beta.19",
"@tiptap/extension-italic": "^2.0.0-beta.15", "@tiptap/extension-italic": "^2.0.0-beta.19",
"@tiptap/extension-link": "^2.0.0-beta.20", "@tiptap/extension-link": "^2.0.0-beta.23",
"@tiptap/extension-list-item": "^2.0.0-beta.14", "@tiptap/extension-list-item": "^2.0.0-beta.14",
"@tiptap/extension-ordered-list": "^2.0.0-beta.16", "@tiptap/extension-ordered-list": "^2.0.0-beta.19",
"@tiptap/extension-paragraph": "^2.0.0-beta.17", "@tiptap/extension-paragraph": "^2.0.0-beta.17",
"@tiptap/extension-strike": "^2.0.0-beta.17", "@tiptap/extension-strike": "^2.0.0-beta.21",
"@tiptap/extension-subscript": "^2.0.0-beta.4", "@tiptap/extension-subscript": "^2.0.0-beta.4",
"@tiptap/extension-superscript": "^2.0.0-beta.4", "@tiptap/extension-superscript": "^2.0.0-beta.4",
"@tiptap/extension-table": "^2.0.0-beta.31", "@tiptap/extension-table": "^2.0.0-beta.34",
"@tiptap/extension-table-cell": "^2.0.0-beta.15", "@tiptap/extension-table-cell": "^2.0.0-beta.15",
"@tiptap/extension-table-header": "^2.0.0-beta.17", "@tiptap/extension-table-header": "^2.0.0-beta.17",
"@tiptap/extension-table-row": "^2.0.0-beta.14", "@tiptap/extension-table-row": "^2.0.0-beta.14",
"@tiptap/extension-task-item": "^2.0.0-beta.18", "@tiptap/extension-task-item": "^2.0.0-beta.21",
"@tiptap/extension-task-list": "^2.0.0-beta.17", "@tiptap/extension-task-list": "^2.0.0-beta.18",
"@tiptap/extension-text": "^2.0.0-beta.13", "@tiptap/extension-text": "^2.0.0-beta.13",
"@tiptap/vue-2": "^2.0.0-beta.57", "@tiptap/vue-2": "^2.0.0-beta.60",
"@toast-ui/editor": "^2.5.2", "@toast-ui/editor": "^2.5.2",
"@toast-ui/vue-editor": "^2.5.2", "@toast-ui/vue-editor": "^2.5.2",
"apollo-cache-inmemory": "^1.6.6", "apollo-cache-inmemory": "^1.6.6",
...@@ -159,12 +159,11 @@ ...@@ -159,12 +159,11 @@
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"prismjs": "^1.21.0", "prismjs": "^1.21.0",
"prosemirror-inputrules": "^1.1.3",
"prosemirror-markdown": "^1.6.0", "prosemirror-markdown": "^1.6.0",
"prosemirror-model": "^1.14.3", "prosemirror-model": "^1.14.3",
"prosemirror-state": "^1.3.4", "prosemirror-state": "^1.3.4",
"prosemirror-tables": "^1.1.1", "prosemirror-tables": "^1.1.1",
"prosemirror-view": "^1.20.2", "prosemirror-view": "^1.20.3",
"raphael": "^2.2.7", "raphael": "^2.2.7",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"scrollparent": "^2.0.1", "scrollparent": "^2.0.1",
...@@ -246,7 +245,7 @@ ...@@ -246,7 +245,7 @@
"prettier": "2.2.1", "prettier": "2.2.1",
"prosemirror-schema-basic": "^1.1.2", "prosemirror-schema-basic": "^1.1.2",
"prosemirror-schema-list": "^1.1.6", "prosemirror-schema-list": "^1.1.6",
"prosemirror-test-builder": "^1.0.4", "prosemirror-test-builder": "^1.0.5",
"purgecss": "^4.0.3", "purgecss": "^4.0.3",
"purgecss-from-html": "^4.0.3", "purgecss-from-html": "^4.0.3",
"readdir-enhanced": "^2.2.4", "readdir-enhanced": "^2.2.4",
......
...@@ -75,9 +75,9 @@ describe('content_editor/extensions/attachment', () => { ...@@ -75,9 +75,9 @@ describe('content_editor/extensions/attachment', () => {
it.each` it.each`
eventType | propName | eventData | output eventType | propName | eventData | output
${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [attachmentFile] } }} | ${true} ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [] } }} | ${undefined} ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [] } }} | ${undefined}
${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { files: [attachmentFile] } }} | ${true} ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
`('handles $eventType properly', ({ eventType, propName, eventData, output }) => { `('handles $eventType properly', ({ eventType, propName, eventData, output }) => {
const event = Object.assign(new Event(eventType), eventData); const event = Object.assign(new Event(eventType), eventData);
const handled = tiptapEditor.view.someProp(propName, (eventHandler) => { const handled = tiptapEditor.view.someProp(propName, (eventHandler) => {
......
import { multilineInputRegex } from '~/content_editor/extensions/blockquote'; import Blockquote from '~/content_editor/extensions/blockquote';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/blockquote', () => { describe('content_editor/extensions/blockquote', () => {
describe.each` let tiptapEditor;
input | matches let doc;
${'>>> '} | ${true} let p;
${' >>> '} | ${true} let blockquote;
${'\t>>> '} | ${true}
${'>> '} | ${false}
${'>>>x '} | ${false}
${'> '} | ${false}
`('multilineInputRegex', ({ input, matches }) => {
it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
const match = new RegExp(multilineInputRegex).test(input);
expect(match).toBe(matches); beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [Blockquote] });
({
builders: { doc, p, blockquote },
} = createDocBuilder({
tiptapEditor,
names: {
blockquote: { nodeType: Blockquote.name },
},
}));
}); });
it.each`
input | insertedNode
${'>>> '} | ${() => blockquote({ multiline: true }, p())}
${'> '} | ${() => blockquote(p())}
${' >>> '} | ${() => blockquote({ multiline: true }, p())}
${'>> '} | ${() => p()}
${'>>>x '} | ${() => p()}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const expectedDoc = doc(insertedNode());
triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
}); });
import { initEmojiMock } from 'helpers/emoji'; import { initEmojiMock } from 'helpers/emoji';
import Emoji from '~/content_editor/extensions/emoji'; import Emoji from '~/content_editor/extensions/emoji';
import { createTestEditor, createDocBuilder } from '../test_utils'; import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/emoji', () => { describe('content_editor/extensions/emoji', () => {
let tiptapEditor; let tiptapEditor;
...@@ -28,18 +28,16 @@ describe('content_editor/extensions/emoji', () => { ...@@ -28,18 +28,16 @@ describe('content_editor/extensions/emoji', () => {
describe('when typing a valid emoji input rule', () => { describe('when typing a valid emoji input rule', () => {
it('inserts an emoji node', () => { it('inserts an emoji node', () => {
const { view } = tiptapEditor;
const { selection } = view.state;
const expectedDoc = doc( const expectedDoc = doc(
p( p(
' ', ' ',
emoji({ moji: '', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }), emoji({ moji: '', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }),
), ),
); );
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, ':heart:'));
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); triggerNodeInputRule({ tiptapEditor, inputRuleText: ':heart:' });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
}); });
......
import { hrInputRuleRegExp } from '~/content_editor/extensions/horizontal_rule'; import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/horizontal_rule', () => { describe('content_editor/extensions/horizontal_rule', () => {
describe.each` let tiptapEditor;
input | matches let doc;
${'---'} | ${true} let p;
${'--'} | ${false} let horizontalRule;
${'---x'} | ${false}
${' ---x'} | ${false}
${' --- '} | ${false}
${'x---x'} | ${false}
${'x---'} | ${false}
`('hrInputRuleRegExp', ({ input, matches }) => {
it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
const match = new RegExp(hrInputRuleRegExp).test(input);
expect(match).toBe(matches); beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [HorizontalRule] });
({
builders: { doc, p, horizontalRule },
} = createDocBuilder({
tiptapEditor,
names: {
horizontalRule: { nodeType: HorizontalRule.name },
},
}));
}); });
it.each`
input | insertedNodes
${'---'} | ${() => [p(), horizontalRule()]}
${'--'} | ${() => [p()]}
${'---x'} | ${() => [p()]}
${' ---x'} | ${() => [p()]}
${' --- '} | ${() => [p()]}
${'x---x'} | ${() => [p()]}
${'x---'} | ${() => [p()]}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNodes }) => {
const expectedDoc = doc(...insertedNodes());
triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
}); });
import { inputRegexAddition, inputRegexDeletion } from '~/content_editor/extensions/inline_diff'; import InlineDiff from '~/content_editor/extensions/inline_diff';
import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/inline_diff', () => { describe('content_editor/extensions/inline_diff', () => {
describe.each` let tiptapEditor;
inputRegex | description | input | matches let doc;
${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+world+}'} | ${true} let p;
${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+ world +}'} | ${true} let inlineDiff;
${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello {+ world+}'} | ${true}
${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello world +}'} | ${true}
${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello with \nnewline+}'} | ${false}
${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+open only'} | ${false}
${inputRegexAddition} | ${'inputRegexAddition'} | ${'close only+}'} | ${false}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{-world-}'} | ${true}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{- world -}'} | ${true}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello {- world-}'} | ${true}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-hello world -}'} | ${true}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{+hello with \nnewline+}'} | ${false}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-open only'} | ${false}
${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'close only-}'} | ${false}
`('$description', ({ inputRegex, input, matches }) => {
it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
const match = new RegExp(inputRegex).test(input);
expect(match).toBe(matches); beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [InlineDiff] });
({
builders: { doc, p, inlineDiff },
} = createDocBuilder({
tiptapEditor,
names: {
inlineDiff: { markType: InlineDiff.name },
},
}));
}); });
it.each`
input | insertedNode
${'hello{+world+}'} | ${() => p('hello', inlineDiff('world'))}
${'hello{+ world +}'} | ${() => p('hello', inlineDiff(' world '))}
${'{+hello with \nnewline+}'} | ${() => p('{+hello with newline+}')}
${'{+open only'} | ${() => p('{+open only')}
${'close only+}'} | ${() => p('close only+}')}
${'hello{-world-}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, 'world'))}
${'hello{- world -}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, ' world '))}
${'hello {- world-}'} | ${() => p('hello ', inlineDiff({ type: 'deletion' }, ' world'))}
${'{-hello world -}'} | ${() => p(inlineDiff({ type: 'deletion' }, 'hello world '))}
${'{-hello with \nnewline-}'} | ${() => p('{-hello with newline-}')}
${'{-open only'} | ${() => p('{-open only')}
${'close only-}'} | ${() => p('close only-}')}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const expectedDoc = doc(insertedNode());
triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
}); });
import { import Link from '~/content_editor/extensions/link';
markdownLinkSyntaxInputRuleRegExp, import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
urlSyntaxRegExp,
extractHrefFromMarkdownLink,
} from '~/content_editor/extensions/link';
describe('content_editor/extensions/link', () => { describe('content_editor/extensions/link', () => {
describe.each` let tiptapEditor;
input | matches let doc;
${'[gitlab](https://gitlab.com)'} | ${true} let p;
${'[documentation](readme.md)'} | ${true} let link;
${'[link 123](readme.md)'} | ${true}
${'[link 123](read me.md)'} | ${true}
${'text'} | ${false}
${'documentation](readme.md'} | ${false}
${'https://www.google.com'} | ${false}
`('markdownLinkSyntaxInputRuleRegExp', ({ input, matches }) => {
it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
const match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
expect(Boolean(match?.groups.href)).toBe(matches);
});
});
describe.each`
input | matches
${'http://example.com '} | ${true}
${'https://example.com '} | ${true}
${'www.example.com '} | ${true}
${'example.com/ab.html '} | ${false}
${'text'} | ${false}
${' http://example.com '} | ${true}
${'https://www.google.com '} | ${true}
`('urlSyntaxRegExp', ({ input, matches }) => {
it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
const match = new RegExp(urlSyntaxRegExp).exec(input);
expect(Boolean(match?.groups.href)).toBe(matches);
});
});
describe('extractHrefFromMarkdownLink', () => {
const input = '[gitlab](https://gitlab.com)';
const href = 'https://gitlab.com';
let match;
let result;
beforeEach(() => { beforeEach(() => {
match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input); tiptapEditor = createTestEditor({ extensions: [Link] });
result = extractHrefFromMarkdownLink(match); ({
}); builders: { doc, p, link },
} = createDocBuilder({
it('extracts the url from a markdown link captured by markdownLinkSyntaxInputRuleRegExp', () => { tiptapEditor,
expect(result).toEqual({ href }); names: {
}); link: { markType: Link.name },
},
it('makes sure that url text is the last capture group', () => { }));
expect(match[match.length - 1]).toEqual('gitlab'); });
});
afterEach(() => {
tiptapEditor.destroy();
});
it.each`
input | insertedNode
${'[gitlab](https://gitlab.com)'} | ${() => p(link({ href: 'https://gitlab.com' }, 'gitlab'))}
${'[documentation](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'documentation'))}
${'[link 123](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'link 123'))}
${'[link 123](read me.md)'} | ${() => p(link({ href: 'read me.md' }, 'link 123'))}
${'text'} | ${() => p('text')}
${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const expectedDoc = doc(insertedNode());
triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
}); });
import MathInline from '~/content_editor/extensions/math_inline'; import MathInline from '~/content_editor/extensions/math_inline';
import { createTestEditor, createDocBuilder } from '../test_utils'; import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/math_inline', () => { describe('content_editor/extensions/math_inline', () => {
let tiptapEditor; let tiptapEditor;
...@@ -26,16 +26,9 @@ describe('content_editor/extensions/math_inline', () => { ...@@ -26,16 +26,9 @@ describe('content_editor/extensions/math_inline', () => {
${'$`a^2`'} | ${() => p('$`a^2`')} ${'$`a^2`'} | ${() => p('$`a^2`')}
${'`a^2`$'} | ${() => p('`a^2`$')} ${'`a^2`$'} | ${() => p('`a^2`$')}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const { view } = tiptapEditor;
const expectedDoc = doc(insertedNode()); const expectedDoc = doc(insertedNode());
tiptapEditor.chain().setContent(input).setTextSelection(0).run(); triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
const { state } = tiptapEditor;
const { selection } = state;
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) => f(view, selection.from, input.length + 1, input));
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
......
import TableOfContents from '~/content_editor/extensions/table_of_contents'; import TableOfContents from '~/content_editor/extensions/table_of_contents';
import { createTestEditor, createDocBuilder } from '../test_utils'; import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/emoji', () => { describe('content_editor/extensions/table_of_contents', () => {
let tiptapEditor; let tiptapEditor;
let builders; let doc;
let tableOfContents;
let p;
beforeEach(() => { beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [TableOfContents] }); tiptapEditor = createTestEditor({ extensions: [TableOfContents] });
({ builders } = createDocBuilder({ ({
builders: { doc, p, tableOfContents },
} = createDocBuilder({
tiptapEditor, tiptapEditor,
names: { tableOfContents: { nodeType: TableOfContents.name } }, names: { tableOfContents: { nodeType: TableOfContents.name } },
})); }));
...@@ -15,20 +19,16 @@ describe('content_editor/extensions/emoji', () => { ...@@ -15,20 +19,16 @@ describe('content_editor/extensions/emoji', () => {
it.each` it.each`
input | insertedNode input | insertedNode
${'[[_TOC_]]'} | ${'tableOfContents'} ${'[[_TOC_]]'} | ${() => tableOfContents()}
${'[TOC]'} | ${'tableOfContents'} ${'[TOC]'} | ${() => tableOfContents()}
${'[toc]'} | ${'p'} ${'[toc]'} | ${() => p()}
${'TOC'} | ${'p'} ${'TOC'} | ${() => p()}
${'[_TOC_]'} | ${'p'} ${'[_TOC_]'} | ${() => p()}
${'[[TOC]]'} | ${'p'} ${'[[TOC]]'} | ${() => p()}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const { doc } = builders; const expectedDoc = doc(insertedNode());
const { view } = tiptapEditor;
const { selection } = view.state;
const expectedDoc = doc(builders[insertedNode]());
// Triggers the event handler that input rules listen to triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
}); });
......
import WordBreak from '~/content_editor/extensions/word_break';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/word_break', () => {
let tiptapEditor;
let doc;
let p;
let wordBreak;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [WordBreak] });
({
builders: { doc, p, wordBreak },
} = createDocBuilder({
tiptapEditor,
names: {
wordBreak: { nodeType: WordBreak.name },
},
}));
});
it.each`
input | insertedNode
${'<wbr>'} | ${() => p(wordBreak())}
${'<wbr'} | ${() => p()}
${'wbr>'} | ${() => p()}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const expectedDoc = doc(insertedNode());
triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
...@@ -10,7 +10,7 @@ import Heading from '~/content_editor/extensions/heading'; ...@@ -10,7 +10,7 @@ import Heading from '~/content_editor/extensions/heading';
import 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, triggerNodeInputRule } from '../test_utils';
describe('content_editor/services/track_input_rules_and_shortcuts', () => { describe('content_editor/services/track_input_rules_and_shortcuts', () => {
let trackingSpy; let trackingSpy;
...@@ -70,14 +70,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => { ...@@ -70,14 +70,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('when creating a heading using an input rule', () => { describe('when creating a heading using an input rule', () => {
it('sends a tracking event indicating that a heading was created using an input rule', async () => { it('sends a tracking event indicating that a heading was created using an input rule', async () => {
const nodeName = Heading.name; const nodeName = Heading.name;
const { view } = editor; triggerNodeInputRule({ tiptapEditor: editor, inputRuleText: '## ' });
const { selection } = view.state;
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, '## '));
editor.chain().insertContent(HEADING_TEXT).run();
expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, { expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL, label: CONTENT_EDITOR_TRACKING_LABEL,
property: `${nodeName}`, property: `${nodeName}`,
......
...@@ -119,3 +119,26 @@ export const createTestContentEditorExtension = ({ commands = [] } = {}) => { ...@@ -119,3 +119,26 @@ export const createTestContentEditorExtension = ({ commands = [] } = {}) => {
}, },
}; };
}; };
export const triggerNodeInputRule = ({ tiptapEditor, inputRuleText }) => {
const { view } = tiptapEditor;
const { state } = tiptapEditor;
const { selection } = state;
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, inputRuleText));
};
export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
const { view } = tiptapEditor;
tiptapEditor.chain().setContent(inputRuleText).setTextSelection(0).run();
const { state } = tiptapEditor;
const { selection } = state;
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) =>
f(view, selection.from, inputRuleText.length + 1, inputRuleText),
);
};
This diff is collapsed.
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