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.
Files changed (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/decap-cms-widget-markdown.js +5 -5
  3. package/dist/decap-cms-widget-markdown.js.map +1 -1
  4. package/dist/esm/MarkdownControl/VisualEditor.js +7 -3
  5. package/dist/esm/MarkdownControl/components/InlineShortcode.js +67 -0
  6. package/dist/esm/MarkdownControl/plugins/shortcodes/insertShortcode.js +53 -4
  7. package/dist/esm/MarkdownControl/plugins/shortcodes/withShortcodes.js +11 -1
  8. package/dist/esm/MarkdownControl/renderers.js +21 -16
  9. package/dist/esm/serializers/remarkRehypeShortcodes.js +16 -12
  10. package/dist/esm/serializers/remarkShortcodes.js +118 -12
  11. package/dist/esm/serializers/remarkSlate.js +14 -1
  12. package/dist/esm/serializers/slateRemark.js +18 -2
  13. package/package.json +2 -2
  14. package/src/MarkdownControl/VisualEditor.js +5 -1
  15. package/src/MarkdownControl/components/InlineShortcode.js +77 -0
  16. package/src/MarkdownControl/components/__tests__/InlineShortcode.spec.js +81 -0
  17. package/src/MarkdownControl/plugins/shortcodes/__tests__/insertShortcode.spec.js +106 -0
  18. package/src/MarkdownControl/plugins/shortcodes/insertShortcode.js +59 -6
  19. package/src/MarkdownControl/plugins/shortcodes/withShortcodes.js +12 -2
  20. package/src/MarkdownControl/renderers.js +3 -0
  21. package/src/serializers/__tests__/remarkShortcodes.spec.js +210 -1
  22. package/src/serializers/__tests__/slate.spec.js +80 -0
  23. package/src/serializers/remarkRehypeShortcodes.js +17 -6
  24. package/src/serializers/remarkShortcodes.js +140 -12
  25. package/src/serializers/remarkSlate.js +7 -0
  26. 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
- const transformedChildren = map(root.children, processShortcodes);
22
- return {
23
- ...root,
24
- children: transformedChildren
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
- const textNode = u('html', valueHtml);
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 tokenizers = Parser.prototype.blockTokenizers;
6
- const methods = Parser.prototype.blockMethods;
7
- tokenizers.shortcode = createShortcodeTokenizer({
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
- methods.unshift('shortcode');
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
- if (!pattern.source.startsWith('^')) {
31
- pattern = new RegExp(`^${pattern.source}`, pattern.flags);
32
- }
33
- match = value.match(pattern);
50
+ pattern = createPattern(pattern, {
51
+ anchored: true
52
+ });
53
+ match = pattern.exec(value);
34
54
  if (!match) {
35
- match = potentialMatchValue.match(pattern);
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
- return plugin.toBlock(data.shortcodeData);
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.12.1",
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.1"
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
- .toMap()
8
- .mapKeys((_, field) => field.get('name'))
9
- .map(field => field.get('default', ''));
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.toJS(),
70
+ shortcodeData: defaultValues,
18
71
  },
19
72
  children: [{ text: '' }],
20
73
  };