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