decap-cms-widget-markdown 3.12.1 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/decap-cms-widget-markdown.js +5 -5
- package/dist/decap-cms-widget-markdown.js.map +1 -1
- package/dist/esm/MarkdownControl/VisualEditor.js +7 -3
- package/dist/esm/MarkdownControl/components/InlineShortcode.js +67 -0
- package/dist/esm/MarkdownControl/plugins/shortcodes/insertShortcode.js +53 -4
- package/dist/esm/MarkdownControl/plugins/shortcodes/withShortcodes.js +11 -1
- package/dist/esm/MarkdownControl/renderers.js +21 -16
- package/dist/esm/serializers/remarkRehypeShortcodes.js +16 -12
- package/dist/esm/serializers/remarkShortcodes.js +118 -12
- package/dist/esm/serializers/remarkSlate.js +14 -1
- package/dist/esm/serializers/slateRemark.js +18 -2
- package/package.json +2 -2
- package/src/MarkdownControl/VisualEditor.js +5 -1
- package/src/MarkdownControl/components/InlineShortcode.js +77 -0
- package/src/MarkdownControl/components/__tests__/InlineShortcode.spec.js +81 -0
- package/src/MarkdownControl/plugins/shortcodes/__tests__/insertShortcode.spec.js +106 -0
- package/src/MarkdownControl/plugins/shortcodes/insertShortcode.js +59 -6
- package/src/MarkdownControl/plugins/shortcodes/withShortcodes.js +12 -2
- package/src/MarkdownControl/renderers.js +3 -0
- package/src/serializers/__tests__/remarkShortcodes.spec.js +210 -1
- package/src/serializers/__tests__/slate.spec.js +80 -0
- package/src/serializers/remarkRehypeShortcodes.js +17 -6
- package/src/serializers/remarkShortcodes.js +140 -12
- package/src/serializers/remarkSlate.js +7 -0
- package/src/serializers/slateRemark.js +13 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import map from 'lodash/map';
|
|
2
1
|
import has from 'lodash/has';
|
|
3
2
|
import { renderToString } from 'react-dom/server';
|
|
4
3
|
import u from 'unist-builder';
|
|
@@ -18,11 +17,20 @@ export default function remarkToRehypeShortcodes({
|
|
|
18
17
|
}) {
|
|
19
18
|
return transform;
|
|
20
19
|
function transform(root) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
function walk(node) {
|
|
21
|
+
if (!node) return node;
|
|
22
|
+
if (has(node, ['data', 'shortcode'])) {
|
|
23
|
+
return processShortcodes(node);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(node.children)) {
|
|
26
|
+
return {
|
|
27
|
+
...node,
|
|
28
|
+
children: node.children.map(walk)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
return walk(root);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -43,6 +51,7 @@ export default function remarkToRehypeShortcodes({
|
|
|
43
51
|
shortcodeData
|
|
44
52
|
} = node.data;
|
|
45
53
|
const plugin = plugins.get(shortcode);
|
|
54
|
+
if (!plugin) return node;
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
57
|
* Run the shortcode plugin's `toPreview` method, which will return either
|
|
@@ -55,12 +64,7 @@ export default function remarkToRehypeShortcodes({
|
|
|
55
64
|
/**
|
|
56
65
|
* Return a new 'html' type node containing the shortcode preview markup.
|
|
57
66
|
*/
|
|
58
|
-
|
|
59
|
-
const children = [textNode];
|
|
60
|
-
return {
|
|
61
|
-
...node,
|
|
62
|
-
children
|
|
63
|
-
};
|
|
67
|
+
return u('html', valueHtml);
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
/**
|
|
@@ -2,18 +2,35 @@ export function remarkParseShortcodes({
|
|
|
2
2
|
plugins
|
|
3
3
|
}) {
|
|
4
4
|
const Parser = this.Parser;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
5
|
+
const blockTokenizers = Parser.prototype.blockTokenizers;
|
|
6
|
+
const blockMethods = Parser.prototype.blockMethods;
|
|
7
|
+
const inlineTokenizers = Parser.prototype.inlineTokenizers;
|
|
8
|
+
const inlineMethods = Parser.prototype.inlineMethods;
|
|
9
|
+
blockTokenizers.shortcode = createShortcodeTokenizer({
|
|
8
10
|
plugins
|
|
9
11
|
});
|
|
10
|
-
|
|
12
|
+
blockMethods.unshift('shortcode');
|
|
13
|
+
inlineTokenizers.inlineShortcode = createInlineShortcodeTokenizer({
|
|
14
|
+
plugins
|
|
15
|
+
});
|
|
16
|
+
inlineMethods.unshift('inlineShortcode');
|
|
17
|
+
}
|
|
18
|
+
function createPattern(pattern, {
|
|
19
|
+
anchored = false
|
|
20
|
+
} = {}) {
|
|
21
|
+
let source = pattern.source;
|
|
22
|
+
if (anchored && !source.startsWith('^')) {
|
|
23
|
+
source = `^${source}`;
|
|
24
|
+
} else if (!anchored && source.startsWith('^')) {
|
|
25
|
+
source = source.slice(1);
|
|
26
|
+
}
|
|
27
|
+
return new RegExp(source, pattern.flags.replace(/[gy]/g, ''));
|
|
11
28
|
}
|
|
12
29
|
function createShortcodeTokenizer({
|
|
13
30
|
plugins
|
|
14
31
|
}) {
|
|
15
32
|
plugins.forEach(plugin => {
|
|
16
|
-
if (plugin.pattern.flags.includes('m')) {
|
|
33
|
+
if (plugin.pattern && plugin.pattern.flags.includes('m')) {
|
|
17
34
|
console.warn(`Invalid RegExp: editor component '${plugin.id}' must not use the multiline flag in its pattern.`);
|
|
18
35
|
}
|
|
19
36
|
});
|
|
@@ -21,18 +38,21 @@ function createShortcodeTokenizer({
|
|
|
21
38
|
let match;
|
|
22
39
|
const potentialMatchValue = value.split('\n\n')[0].trimEnd();
|
|
23
40
|
const plugin = plugins.find(plugin => {
|
|
41
|
+
if (plugin.type === 'inline') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
24
44
|
let {
|
|
25
45
|
pattern
|
|
26
46
|
} = plugin;
|
|
27
47
|
// Plugin patterns must start with a caret (^) to match the beginning of the block.
|
|
28
48
|
// If the pattern does not start with a caret, we add it
|
|
29
49
|
// to ensure that remark consumes only the shortcode, without any leading text.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
match =
|
|
50
|
+
pattern = createPattern(pattern, {
|
|
51
|
+
anchored: true
|
|
52
|
+
});
|
|
53
|
+
match = pattern.exec(value);
|
|
34
54
|
if (!match) {
|
|
35
|
-
match =
|
|
55
|
+
match = pattern.exec(potentialMatchValue);
|
|
36
56
|
}
|
|
37
57
|
return !!match;
|
|
38
58
|
});
|
|
@@ -43,7 +63,7 @@ function createShortcodeTokenizer({
|
|
|
43
63
|
if (silent) {
|
|
44
64
|
return true;
|
|
45
65
|
}
|
|
46
|
-
const shortcodeData = plugin.fromBlock(match);
|
|
66
|
+
const shortcodeData = plugin.fromBlock ? plugin.fromBlock(match) : plugin.fromInline ? plugin.fromInline(match) : match;
|
|
47
67
|
try {
|
|
48
68
|
return eat(match[0])({
|
|
49
69
|
type: 'shortcode',
|
|
@@ -59,6 +79,82 @@ function createShortcodeTokenizer({
|
|
|
59
79
|
}
|
|
60
80
|
};
|
|
61
81
|
}
|
|
82
|
+
function createInlineShortcodeTokenizer({
|
|
83
|
+
plugins
|
|
84
|
+
}) {
|
|
85
|
+
plugins.forEach(plugin => {
|
|
86
|
+
if (plugin.type === 'inline' && plugin.pattern) {
|
|
87
|
+
if (plugin.pattern.flags.includes('m')) {
|
|
88
|
+
console.warn(`Invalid RegExp: inline editor component '${plugin.id}' must not use the multiline flag in its pattern.`);
|
|
89
|
+
}
|
|
90
|
+
if (/(\.\*|\.\+)(?!\?)/.test(plugin.pattern.source)) {
|
|
91
|
+
console.warn(`Potentially greedy RegExp in inline component '${plugin.id}': consider using non-greedy quantifier (e.g. .*? or .+?) or specific character classes to prevent overmatching within paragraphs.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
function tokenizeInlineShortcode(eat, value, silent) {
|
|
96
|
+
let match;
|
|
97
|
+
const plugin = plugins.find(plugin => {
|
|
98
|
+
if (plugin.type !== 'inline') {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let {
|
|
102
|
+
pattern
|
|
103
|
+
} = plugin;
|
|
104
|
+
// Inline patterns must match at the current offset (leading ^)
|
|
105
|
+
pattern = createPattern(pattern, {
|
|
106
|
+
anchored: true
|
|
107
|
+
});
|
|
108
|
+
match = pattern.exec(value);
|
|
109
|
+
return !!match;
|
|
110
|
+
});
|
|
111
|
+
if (match) {
|
|
112
|
+
if (silent) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const shortcodeData = plugin.fromInline ? plugin.fromInline(match) : plugin.fromBlock ? plugin.fromBlock(match) : match;
|
|
116
|
+
try {
|
|
117
|
+
return eat(match[0])({
|
|
118
|
+
type: 'inline-shortcode',
|
|
119
|
+
data: {
|
|
120
|
+
shortcode: plugin.id,
|
|
121
|
+
shortcodeData,
|
|
122
|
+
isVoid: true
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.warn(`Sent invalid data to remark. Inline plugin: ${plugin.id}. Value: ${match[0]}. Data: ${JSON.stringify(shortcodeData)}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
tokenizeInlineShortcode.locator = function locateInlineShortcode(value, fromIndex) {
|
|
132
|
+
let minIndex = -1;
|
|
133
|
+
plugins.forEach(plugin => {
|
|
134
|
+
if (plugin.type !== 'inline') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (plugin.trigger) {
|
|
138
|
+
const triggerIndex = value.indexOf(plugin.trigger, fromIndex);
|
|
139
|
+
if (triggerIndex !== -1 && (minIndex === -1 || triggerIndex < minIndex)) {
|
|
140
|
+
minIndex = triggerIndex;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
const searchPattern = createPattern(plugin.pattern);
|
|
144
|
+
const slice = value.slice(fromIndex);
|
|
145
|
+
const match = searchPattern.exec(slice);
|
|
146
|
+
if (match && typeof match.index === 'number') {
|
|
147
|
+
const foundIndex = fromIndex + match.index;
|
|
148
|
+
if (minIndex === -1 || foundIndex < minIndex) {
|
|
149
|
+
minIndex = foundIndex;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return minIndex;
|
|
155
|
+
};
|
|
156
|
+
return tokenizeInlineShortcode;
|
|
157
|
+
}
|
|
62
158
|
export function createRemarkShortcodeStringifier({
|
|
63
159
|
plugins
|
|
64
160
|
}) {
|
|
@@ -66,12 +162,22 @@ export function createRemarkShortcodeStringifier({
|
|
|
66
162
|
const Compiler = this.Compiler;
|
|
67
163
|
const visitors = Compiler.prototype.visitors;
|
|
68
164
|
visitors.shortcode = shortcode;
|
|
165
|
+
visitors['inline-shortcode'] = inlineShortcode;
|
|
69
166
|
function shortcode(node) {
|
|
70
167
|
const {
|
|
71
168
|
data
|
|
72
169
|
} = node;
|
|
73
170
|
const plugin = plugins.find(plugin => data.shortcode === plugin.id);
|
|
74
|
-
|
|
171
|
+
if (!plugin) return '';
|
|
172
|
+
return plugin.toBlock ? plugin.toBlock(data.shortcodeData) : plugin.toInline ? plugin.toInline(data.shortcodeData) : '';
|
|
173
|
+
}
|
|
174
|
+
function inlineShortcode(node) {
|
|
175
|
+
const {
|
|
176
|
+
data
|
|
177
|
+
} = node;
|
|
178
|
+
const plugin = plugins.find(plugin => data.shortcode === plugin.id);
|
|
179
|
+
if (!plugin) return '';
|
|
180
|
+
return plugin.toInline ? plugin.toInline(data.shortcodeData) : plugin.toBlock ? plugin.toBlock(data.shortcodeData) : '';
|
|
75
181
|
}
|
|
76
182
|
};
|
|
77
183
|
}
|
|
@@ -20,7 +20,8 @@ const typeMap = {
|
|
|
20
20
|
thematicBreak: 'thematic-break',
|
|
21
21
|
link: 'link',
|
|
22
22
|
image: 'image',
|
|
23
|
-
shortcode: 'shortcode'
|
|
23
|
+
shortcode: 'shortcode',
|
|
24
|
+
'inline-shortcode': 'inline-shortcode'
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -306,6 +307,18 @@ export default function remarkToSlate({
|
|
|
306
307
|
data
|
|
307
308
|
});
|
|
308
309
|
}
|
|
310
|
+
case 'inline-shortcode':
|
|
311
|
+
{
|
|
312
|
+
const nodes = [createText('')];
|
|
313
|
+
const data = {
|
|
314
|
+
...node.data,
|
|
315
|
+
id: node.data.shortcode,
|
|
316
|
+
shortcodeNew: true
|
|
317
|
+
};
|
|
318
|
+
return createInline(typeMap[node.type], {
|
|
319
|
+
data
|
|
320
|
+
}, nodes);
|
|
321
|
+
}
|
|
309
322
|
case 'text':
|
|
310
323
|
{
|
|
311
324
|
const text = node.value;
|
|
@@ -31,7 +31,8 @@ const typeMap = {
|
|
|
31
31
|
'thematic-break': 'thematicBreak',
|
|
32
32
|
link: 'link',
|
|
33
33
|
image: 'image',
|
|
34
|
-
shortcode: 'shortcode'
|
|
34
|
+
shortcode: 'shortcode',
|
|
35
|
+
'inline-shortcode': 'inline-shortcode'
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -44,7 +45,7 @@ const markMap = {
|
|
|
44
45
|
code: 'inlineCode'
|
|
45
46
|
};
|
|
46
47
|
const blockTypes = ['paragraph', 'quote', 'heading-one', 'heading-two', 'heading-three', 'heading-four', 'heading-five', 'heading-six', 'bulleted-list', 'numbered-list', 'list-item', 'shortcode', 'table', 'table-row', 'table-cell'];
|
|
47
|
-
const inlineTypes = ['link', 'image', 'break'];
|
|
48
|
+
const inlineTypes = ['link', 'image', 'break', 'inline-shortcode'];
|
|
48
49
|
const leadingWhitespaceExp = /^\s+\S/;
|
|
49
50
|
const trailingWhitespaceExp = /(?!\S)\s+$/;
|
|
50
51
|
export default function slateToRemark(value, {
|
|
@@ -93,6 +94,7 @@ export default function slateToRemark(value, {
|
|
|
93
94
|
}
|
|
94
95
|
case 'image':
|
|
95
96
|
case 'break':
|
|
97
|
+
case 'inline-shortcode':
|
|
96
98
|
{
|
|
97
99
|
const data = omit(node.data, 'marks');
|
|
98
100
|
return {
|
|
@@ -135,6 +137,7 @@ export default function slateToRemark(value, {
|
|
|
135
137
|
}
|
|
136
138
|
case 'break':
|
|
137
139
|
case 'image':
|
|
140
|
+
case 'inline-shortcode':
|
|
138
141
|
return map(get(node, ['data', 'marks']), mark => mark.type);
|
|
139
142
|
default:
|
|
140
143
|
return getNodeMarkArray(node);
|
|
@@ -501,6 +504,19 @@ export default function slateToRemark(value, {
|
|
|
501
504
|
data
|
|
502
505
|
});
|
|
503
506
|
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Inline Shortcodes
|
|
510
|
+
*/
|
|
511
|
+
case 'inline-shortcode':
|
|
512
|
+
{
|
|
513
|
+
const {
|
|
514
|
+
data
|
|
515
|
+
} = node;
|
|
516
|
+
return u(typeMap[node.type], {
|
|
517
|
+
data
|
|
518
|
+
});
|
|
519
|
+
}
|
|
504
520
|
}
|
|
505
521
|
}
|
|
506
522
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decap-cms-widget-markdown",
|
|
3
3
|
"description": "Widget for editing markdown in Decap CMS.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.13.0",
|
|
5
5
|
"homepage": "https://www.decapcms.org/docs/widgets/#markdown",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"react": "^19.1.0",
|
|
57
57
|
"react-dom": "^19.1.0",
|
|
58
58
|
"react-immutable-proptypes": "^2.1.0",
|
|
59
|
-
"decap-cms-ui-default": "3.9.
|
|
59
|
+
"decap-cms-ui-default": "3.9.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"commonmark": "^0.30.0",
|
|
@@ -164,7 +164,11 @@ function Editor(props) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
function handleInsertShortcode(pluginConfig) {
|
|
167
|
-
insertShortcode(editor, pluginConfig
|
|
167
|
+
insertShortcode(editor, pluginConfig, {
|
|
168
|
+
getAsset: props.getAsset,
|
|
169
|
+
resolveWidget: props.resolveWidget,
|
|
170
|
+
t: props.t,
|
|
171
|
+
});
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
function handleKeyDown(event) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import { css } from '@emotion/react';
|
|
3
|
+
import { useSelected, ReactEditor, useSlate } from 'slate-react';
|
|
4
|
+
import { Editor, Transforms } from 'slate';
|
|
5
|
+
import { colors, lengths } from 'decap-cms-ui-default';
|
|
6
|
+
|
|
7
|
+
import { getEditorComponents } from '../index';
|
|
8
|
+
|
|
9
|
+
function InlineShortcode(props) {
|
|
10
|
+
const { attributes, children, element } = props;
|
|
11
|
+
const editor = useSlate();
|
|
12
|
+
const isSelected = useSelected();
|
|
13
|
+
const plugin = getEditorComponents().get(element.data?.shortcode);
|
|
14
|
+
const shortcodeData = element.data?.shortcodeData || {};
|
|
15
|
+
|
|
16
|
+
async function handleClick(e) {
|
|
17
|
+
if (plugin && typeof plugin.onEdit === 'function') {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
const pathRef = Editor.pathRef(editor, ReactEditor.findPath(editor, element));
|
|
21
|
+
try {
|
|
22
|
+
const updatedData = await plugin.onEdit({ data: shortcodeData });
|
|
23
|
+
const path = pathRef.current;
|
|
24
|
+
if (updatedData && path) {
|
|
25
|
+
Transforms.setNodes(
|
|
26
|
+
editor,
|
|
27
|
+
{
|
|
28
|
+
data: {
|
|
29
|
+
...element.data,
|
|
30
|
+
shortcodeData: updatedData,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{ at: path },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(
|
|
38
|
+
`Error executing onEdit for inline component '${element.data?.shortcode}':`,
|
|
39
|
+
err,
|
|
40
|
+
);
|
|
41
|
+
} finally {
|
|
42
|
+
pathRef.unref();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let previewContent;
|
|
48
|
+
if (plugin && typeof plugin.toPreview === 'function') {
|
|
49
|
+
previewContent = plugin.toPreview(shortcodeData);
|
|
50
|
+
} else if (plugin && typeof plugin.toInline === 'function') {
|
|
51
|
+
previewContent = plugin.toInline(shortcodeData);
|
|
52
|
+
} else {
|
|
53
|
+
previewContent = `[${element.data?.shortcode || 'inline'}]`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inlineStyles = css`
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
vertical-align: baseline;
|
|
60
|
+
cursor: ${plugin?.onEdit ? 'pointer' : 'default'};
|
|
61
|
+
border-radius: ${lengths.borderRadius || '3px'};
|
|
62
|
+
padding: 0 2px;
|
|
63
|
+
background-color: ${isSelected ? 'rgba(30, 144, 255, 0.15)' : 'transparent'};
|
|
64
|
+
box-shadow: ${isSelected ? `0 0 0 1px ${colors.active || '#3a69c7'}` : 'none'};
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<span {...attributes} css={inlineStyles} onClick={handleClick}>
|
|
69
|
+
<span contentEditable={false} style={{ userSelect: 'none' }}>
|
|
70
|
+
{previewContent}
|
|
71
|
+
</span>
|
|
72
|
+
{children}
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default InlineShortcode;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { Map } from 'immutable';
|
|
3
|
+
import { createEditor, Transforms } from 'slate';
|
|
4
|
+
import { Editable, Slate, withReact } from 'slate-react';
|
|
5
|
+
|
|
6
|
+
import { getEditorComponents } from '../../index';
|
|
7
|
+
import withShortcodes from '../../plugins/shortcodes/withShortcodes';
|
|
8
|
+
import InlineShortcode from '../InlineShortcode';
|
|
9
|
+
|
|
10
|
+
jest.mock('../../index', () => ({
|
|
11
|
+
getEditorComponents: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('InlineShortcode', () => {
|
|
15
|
+
it('should not update another node when the edited node is deleted', async () => {
|
|
16
|
+
let resolveEdit;
|
|
17
|
+
const onEdit = jest.fn(
|
|
18
|
+
() =>
|
|
19
|
+
new Promise(resolve => {
|
|
20
|
+
resolveEdit = resolve;
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
getEditorComponents.mockReturnValue(
|
|
24
|
+
Map({
|
|
25
|
+
wikilink: {
|
|
26
|
+
id: 'wikilink',
|
|
27
|
+
onEdit,
|
|
28
|
+
toPreview: data => data.target,
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const editor = withReact(withShortcodes(createEditor()));
|
|
34
|
+
const initialValue = [
|
|
35
|
+
{
|
|
36
|
+
type: 'paragraph',
|
|
37
|
+
children: [
|
|
38
|
+
{ text: '' },
|
|
39
|
+
{
|
|
40
|
+
type: 'inline-shortcode',
|
|
41
|
+
data: { shortcode: 'wikilink', shortcodeData: { target: 'first' } },
|
|
42
|
+
children: [{ text: '' }],
|
|
43
|
+
},
|
|
44
|
+
{ text: ' ' },
|
|
45
|
+
{
|
|
46
|
+
type: 'inline-shortcode',
|
|
47
|
+
data: { shortcode: 'wikilink', shortcodeData: { target: 'second' } },
|
|
48
|
+
children: [{ text: '' }],
|
|
49
|
+
},
|
|
50
|
+
{ text: '' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
render(
|
|
56
|
+
<Slate editor={editor} initialValue={initialValue}>
|
|
57
|
+
<Editable
|
|
58
|
+
renderElement={props =>
|
|
59
|
+
props.element.type === 'inline-shortcode' ? (
|
|
60
|
+
<InlineShortcode {...props} />
|
|
61
|
+
) : (
|
|
62
|
+
<p {...props.attributes}>{props.children}</p>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
</Slate>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
fireEvent.click(screen.getByText('first'));
|
|
70
|
+
await act(async () => {
|
|
71
|
+
Transforms.removeNodes(editor, { at: [0, 1] });
|
|
72
|
+
resolveEdit({ target: 'updated' });
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const remainingInline = editor.children[0].children.find(
|
|
77
|
+
child => child.type === 'inline-shortcode',
|
|
78
|
+
);
|
|
79
|
+
expect(remainingInline.data.shortcodeData).toEqual({ target: 'second' });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createEditor, Editor, Transforms } from 'slate';
|
|
2
|
+
import { withReact } from 'slate-react';
|
|
3
|
+
|
|
4
|
+
import withShortcodes from '../withShortcodes';
|
|
5
|
+
import insertShortcode from '../insertShortcode';
|
|
6
|
+
|
|
7
|
+
function makeEditor(
|
|
8
|
+
initialChildren = [{ type: 'paragraph', children: [{ text: 'Sample text' }] }],
|
|
9
|
+
) {
|
|
10
|
+
const editor = withReact(withShortcodes(createEditor()));
|
|
11
|
+
editor.children = initialChildren;
|
|
12
|
+
return editor;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('insertShortcode', () => {
|
|
16
|
+
it('should insert inline shortcode with onInsert resolving data', async () => {
|
|
17
|
+
const editor = makeEditor();
|
|
18
|
+
editor.selection = {
|
|
19
|
+
anchor: { path: [0, 0], offset: 0 },
|
|
20
|
+
focus: { path: [0, 0], offset: 6 },
|
|
21
|
+
}; // Selected "Sample"
|
|
22
|
+
|
|
23
|
+
const onInsertMock = jest.fn().mockResolvedValue({ target: 'doc-page', label: 'Sample' });
|
|
24
|
+
|
|
25
|
+
const pluginConfig = {
|
|
26
|
+
id: 'wikilink',
|
|
27
|
+
type: 'inline',
|
|
28
|
+
onInsert: onInsertMock,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await insertShortcode(editor, pluginConfig, { contextKey: 'val' });
|
|
32
|
+
|
|
33
|
+
expect(onInsertMock).toHaveBeenCalledWith({
|
|
34
|
+
selectedText: 'Sample',
|
|
35
|
+
cmsContext: { contextKey: 'val' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const insertedNode = editor.children[0].children.find(
|
|
39
|
+
child => child.type === 'inline-shortcode',
|
|
40
|
+
);
|
|
41
|
+
expect(insertedNode).toBeDefined();
|
|
42
|
+
expect(insertedNode.data).toEqual({
|
|
43
|
+
shortcode: 'wikilink',
|
|
44
|
+
shortcodeNew: true,
|
|
45
|
+
shortcodeData: { target: 'doc-page', label: 'Sample' },
|
|
46
|
+
isVoid: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should cancel inline shortcode insertion when onInsert resolves null', async () => {
|
|
51
|
+
const editor = makeEditor();
|
|
52
|
+
editor.selection = {
|
|
53
|
+
anchor: { path: [0, 0], offset: 0 },
|
|
54
|
+
focus: { path: [0, 0], offset: 6 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onInsertMock = jest.fn().mockResolvedValue(null);
|
|
58
|
+
|
|
59
|
+
const pluginConfig = {
|
|
60
|
+
id: 'wikilink',
|
|
61
|
+
type: 'inline',
|
|
62
|
+
onInsert: onInsertMock,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await insertShortcode(editor, pluginConfig);
|
|
66
|
+
|
|
67
|
+
expect(onInsertMock).toHaveBeenCalled();
|
|
68
|
+
const insertedNode = editor.children[0].children.find(
|
|
69
|
+
child => child.type === 'inline-shortcode',
|
|
70
|
+
);
|
|
71
|
+
expect(insertedNode).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should insert at the captured selection after onInsert changes focus', async () => {
|
|
75
|
+
const editor = makeEditor();
|
|
76
|
+
Transforms.select(editor, {
|
|
77
|
+
anchor: { path: [0, 0], offset: 0 },
|
|
78
|
+
focus: { path: [0, 0], offset: 6 },
|
|
79
|
+
});
|
|
80
|
+
let resolveInsert;
|
|
81
|
+
const onInsert = jest.fn(
|
|
82
|
+
() =>
|
|
83
|
+
new Promise(resolve => {
|
|
84
|
+
resolveInsert = resolve;
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const insertion = insertShortcode(editor, {
|
|
89
|
+
id: 'wikilink',
|
|
90
|
+
type: 'inline',
|
|
91
|
+
onInsert,
|
|
92
|
+
});
|
|
93
|
+
Transforms.select(editor, { path: [0, 0], offset: 11 });
|
|
94
|
+
resolveInsert({ target: 'doc-page' });
|
|
95
|
+
await insertion;
|
|
96
|
+
|
|
97
|
+
expect(editor.children[0].children[1].type).toBe('inline-shortcode');
|
|
98
|
+
expect(Editor.string(editor, [0])).toBe(' text');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should keep inline shortcodes atomic when isVoid is false', () => {
|
|
102
|
+
const editor = makeEditor();
|
|
103
|
+
|
|
104
|
+
expect(editor.isVoid({ type: 'inline-shortcode', data: { isVoid: false } })).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -1,12 +1,65 @@
|
|
|
1
|
-
import { Transforms } from 'slate';
|
|
1
|
+
import { Editor, Range, Transforms } from 'slate';
|
|
2
2
|
|
|
3
3
|
import isCursorInEmptyParagraph from './locations/isCursorInEmptyParagraph';
|
|
4
4
|
|
|
5
|
-
function insertShortcode(editor, pluginConfig) {
|
|
5
|
+
async function insertShortcode(editor, pluginConfig, cmsContext = {}) {
|
|
6
|
+
if (pluginConfig.type === 'inline') {
|
|
7
|
+
const selectionRef = editor.selection ? Editor.rangeRef(editor, editor.selection) : null;
|
|
8
|
+
let selectedText = '';
|
|
9
|
+
if (editor.selection && Range.isRange(editor.selection)) {
|
|
10
|
+
selectedText = Editor.string(editor, editor.selection);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let shortcodeData = {};
|
|
14
|
+
|
|
15
|
+
if (typeof pluginConfig.onInsert === 'function') {
|
|
16
|
+
try {
|
|
17
|
+
const result = await pluginConfig.onInsert({ selectedText, cmsContext });
|
|
18
|
+
if (result === null || result === undefined) {
|
|
19
|
+
selectionRef?.unref();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
shortcodeData = result;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
selectionRef?.unref();
|
|
25
|
+
console.error(`Error in onInsert for inline component '${pluginConfig.id}':`, err);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
} else if (pluginConfig.fields) {
|
|
29
|
+
const defaultValues = pluginConfig.fields
|
|
30
|
+
.toMap()
|
|
31
|
+
.mapKeys((_, field) => field.get('name'))
|
|
32
|
+
.map(field => field.get('default', ''));
|
|
33
|
+
shortcodeData = defaultValues.toJS();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const nodeData = {
|
|
37
|
+
type: 'inline-shortcode',
|
|
38
|
+
id: pluginConfig.id,
|
|
39
|
+
data: {
|
|
40
|
+
shortcode: pluginConfig.id,
|
|
41
|
+
shortcodeNew: true,
|
|
42
|
+
shortcodeData,
|
|
43
|
+
isVoid: true,
|
|
44
|
+
},
|
|
45
|
+
children: [{ text: '' }],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const at = selectionRef?.unref();
|
|
49
|
+
if (selectionRef && !at) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
Transforms.insertNodes(editor, nodeData, at ? { at } : undefined);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
6
56
|
const defaultValues = pluginConfig.fields
|
|
7
|
-
.
|
|
8
|
-
|
|
9
|
-
|
|
57
|
+
? pluginConfig.fields
|
|
58
|
+
.toMap()
|
|
59
|
+
.mapKeys((_, field) => field.get('name'))
|
|
60
|
+
.map(field => field.get('default', ''))
|
|
61
|
+
.toJS()
|
|
62
|
+
: {};
|
|
10
63
|
|
|
11
64
|
const nodeData = {
|
|
12
65
|
type: 'shortcode',
|
|
@@ -14,7 +67,7 @@ function insertShortcode(editor, pluginConfig) {
|
|
|
14
67
|
data: {
|
|
15
68
|
shortcode: pluginConfig.id,
|
|
16
69
|
shortcodeNew: true,
|
|
17
|
-
shortcodeData: defaultValues
|
|
70
|
+
shortcodeData: defaultValues,
|
|
18
71
|
},
|
|
19
72
|
children: [{ text: '' }],
|
|
20
73
|
};
|