decap-cms-widget-markdown 3.12.0 → 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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/decap-cms-widget-markdown.js +6 -6
  3. package/dist/decap-cms-widget-markdown.js.map +1 -1
  4. package/dist/esm/MarkdownControl/Toolbar.js +6 -6
  5. package/dist/esm/MarkdownControl/VisualEditor.js +7 -3
  6. package/dist/esm/MarkdownControl/components/InlineShortcode.js +67 -0
  7. package/dist/esm/MarkdownControl/index.js +2 -2
  8. package/dist/esm/MarkdownControl/plugins/shortcodes/insertShortcode.js +53 -4
  9. package/dist/esm/MarkdownControl/plugins/shortcodes/withShortcodes.js +11 -1
  10. package/dist/esm/MarkdownControl/renderers.js +21 -16
  11. package/dist/esm/MarkdownPreview.js +37 -3
  12. package/dist/esm/serializers/remarkRehypeShortcodes.js +18 -15
  13. package/dist/esm/serializers/remarkShortcodes.js +118 -12
  14. package/dist/esm/serializers/remarkSlate.js +14 -1
  15. package/dist/esm/serializers/slateRemark.js +18 -2
  16. package/package.json +23 -13
  17. package/src/MarkdownControl/Toolbar.js +2 -2
  18. package/src/MarkdownControl/VisualEditor.js +5 -1
  19. package/src/MarkdownControl/components/InlineShortcode.js +77 -0
  20. package/src/MarkdownControl/components/__tests__/InlineShortcode.spec.js +81 -0
  21. package/src/MarkdownControl/index.js +2 -2
  22. package/src/MarkdownControl/plugins/shortcodes/__tests__/insertShortcode.spec.js +106 -0
  23. package/src/MarkdownControl/plugins/shortcodes/insertShortcode.js +59 -6
  24. package/src/MarkdownControl/plugins/shortcodes/withShortcodes.js +12 -2
  25. package/src/MarkdownControl/renderers.js +3 -0
  26. package/src/MarkdownPreview.js +38 -3
  27. package/src/__tests__/renderer.spec.js +68 -0
  28. package/src/serializers/__tests__/remarkShortcodes.spec.js +210 -1
  29. package/src/serializers/__tests__/slate.spec.js +80 -0
  30. package/src/serializers/remarkRehypeShortcodes.js +19 -9
  31. package/src/serializers/remarkShortcodes.js +140 -12
  32. package/src/serializers/remarkSlate.js +7 -0
  33. package/src/serializers/slateRemark.js +13 -2
  34. package/LICENSE +0 -22
  35. package/dist/esm/MarkdownControl/plugins/BreakToDefaultBlock.js +0 -26
  36. package/dist/esm/MarkdownControl/plugins/CloseBlock.js +0 -28
  37. package/dist/esm/MarkdownControl/plugins/CommandsAndQueries.js +0 -180
  38. package/dist/esm/MarkdownControl/plugins/ForceInsert.js +0 -42
  39. package/dist/esm/MarkdownControl/plugins/Hotkey.js +0 -26
  40. package/dist/esm/MarkdownControl/plugins/LineBreak.js +0 -13
  41. package/dist/esm/MarkdownControl/plugins/Link.js +0 -49
  42. package/dist/esm/MarkdownControl/plugins/lists/locations/isCursorInItemContainingNestedList.js +0 -6
  43. package/dist/esm/MarkdownControl/plugins/util.js +0 -11
  44. package/dist/esm/serializers/remarkImagesToText.js +0 -31
  45. package/dist/esm/types.js +0 -2
@@ -1,10 +1,44 @@
1
- import React from 'react';
1
+ import { Component } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { WidgetPreviewContainer } from 'decap-cms-ui-default';
4
4
  import DOMPurify from 'dompurify';
5
5
  import { markdownToHtml } from './serializers';
6
+
7
+ // Editors preview a selected-but-not-yet-committed image via URL.createObjectURL(), which
8
+ // produces a blob: URL - DOMPurify's default ALLOWED_URI_REGEXP doesn't include that scheme,
9
+ // so it strips `src` here even though the image is entirely local and safe.
10
+ //
11
+ // Widening ALLOWED_URI_REGEXP itself (as an earlier version of this fix did) allows blob: on
12
+ // every URI-bearing attribute DOMPurify checks - not just <img src>, but also <a href>,
13
+ // <form action>, etc. - which is a materially larger relaxation than this bug needs. A
14
+ // uponSanitizeAttribute hook scopes the exception to exactly <img src>; every other
15
+ // attribute/tag keeps DOMPurify's default (blob:-excluding) behaviour. The hook is added and
16
+ // removed around a single sanitize() call so it can never affect any other consumer of the
17
+ // shared `dompurify` module import elsewhere in the app.
18
+ //
19
+ // The allowed value is further restricted to this document's own origin: a blob: URL embeds
20
+ // the origin of the tab that created it (`blob:<origin>/<uuid>`) and a browser already refuses
21
+ // to dereference one minted by a different origin, so this check cannot relax anything a
22
+ // browser wouldn't already block - it only means a value that could not possibly be one of
23
+ // *this* CMS instance's own not-yet-committed asset previews is rejected before ever reaching
24
+ // the DOM, rather than being handed to the browser to fail on.
6
25
  import { jsx as _jsx } from "@emotion/react/jsx-runtime";
7
- class MarkdownPreview extends React.Component {
26
+ function isSameOriginBlobUrl(value) {
27
+ return value.startsWith(`blob:${window.location.origin}/`);
28
+ }
29
+ function sanitizePreviewHtml(html) {
30
+ DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
31
+ if (node.nodeName === 'IMG' && data.attrName === 'src' && isSameOriginBlobUrl(data.attrValue)) {
32
+ data.forceKeepAttr = true;
33
+ }
34
+ });
35
+ try {
36
+ return DOMPurify.sanitize(html);
37
+ } finally {
38
+ DOMPurify.removeHook('uponSanitizeAttribute');
39
+ }
40
+ }
41
+ class MarkdownPreview extends Component {
8
42
  static propTypes = {
9
43
  getAsset: PropTypes.func.isRequired,
10
44
  resolveWidget: PropTypes.func.isRequired,
@@ -30,7 +64,7 @@ class MarkdownPreview extends React.Component {
30
64
  resolveWidget
31
65
  }, getRemarkPlugins?.());
32
66
  const shouldSanitizePreview = field?.get('sanitize_preview') ?? true;
33
- const toRender = shouldSanitizePreview ? DOMPurify.sanitize(html) : html;
67
+ const toRender = shouldSanitizePreview ? sanitizePreviewHtml(html) : html;
34
68
  return _jsx(WidgetPreviewContainer, {
35
69
  dangerouslySetInnerHTML: {
36
70
  __html: toRender
@@ -1,8 +1,7 @@
1
- import React from 'react';
2
- import map from 'lodash/map';
3
1
  import has from 'lodash/has';
4
2
  import { renderToString } from 'react-dom/server';
5
3
  import u from 'unist-builder';
4
+ import { createElement } from 'react';
6
5
 
7
6
  /**
8
7
  * This plugin doesn't actually transform Remark (MDAST) nodes to Rehype
@@ -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
  /**
@@ -74,7 +78,6 @@ export default function remarkToRehypeShortcodes({
74
78
  if (toPreview) {
75
79
  return toPreview(shortcodeData, getAsset, fields);
76
80
  }
77
-
78
81
  /**
79
82
  * For editor components without a custom `toPreview` (e.g. container
80
83
  * components with nested markdown/richtext fields), render each sub-field
@@ -98,7 +101,7 @@ export default function remarkToRehypeShortcodes({
98
101
  * Last resort fallback: try resolving the widget and rendering its preview.
99
102
  */
100
103
  const preview = resolveWidget(plugin.widget);
101
- return /*#__PURE__*/React.createElement(preview.preview, {
104
+ return /*#__PURE__*/createElement(preview.preview, {
102
105
  value: shortcodeData,
103
106
  field: plugin,
104
107
  getAsset
@@ -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,9 +1,12 @@
1
1
  {
2
2
  "name": "decap-cms-widget-markdown",
3
3
  "description": "Widget for editing markdown in Decap CMS.",
4
- "version": "3.12.0",
4
+ "version": "3.13.0",
5
5
  "homepage": "https://www.decapcms.org/docs/widgets/#markdown",
6
- "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown"
9
+ },
7
10
  "bugs": "https://github.com/decaporg/decap-cms/issues",
8
11
  "module": "dist/esm/index.js",
9
12
  "main": "dist/decap-cms-widget-markdown.js",
@@ -15,14 +18,11 @@
15
18
  "editor"
16
19
  ],
17
20
  "sideEffects": false,
18
- "scripts": {
19
- "develop": "npm run build:esm -- --watch",
20
- "build": "cross-env NODE_ENV=production webpack",
21
- "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
22
- },
23
21
  "dependencies": {
22
+ "detab": "^2.0.4",
24
23
  "dompurify": "^3.4.13",
25
24
  "is-hotkey": "^0.2.0",
25
+ "is-url": "^1.2.4",
26
26
  "mdast-util-definitions": "^1.2.3",
27
27
  "mdast-util-to-string": "^1.0.5",
28
28
  "rehype-parse": "^6.0.0",
@@ -31,30 +31,40 @@
31
31
  "rehype-stringify": "^7.0.0",
32
32
  "remark-parse": "^6.0.3",
33
33
  "remark-rehype": "^4.0.0",
34
+ "remark-slate": "^1.8.6",
35
+ "remark-slate-transformer": "^0.7.4",
34
36
  "remark-stringify": "^6.0.4",
35
37
  "slate": "^0.118.1",
38
+ "slate-base64-serializer": "^0.2.107",
36
39
  "slate-dom": "^0.118.1",
37
40
  "slate-history": "^0.113.1",
38
41
  "slate-hyperscript": "^0.100.0",
42
+ "slate-plain-serializer": "^0.7.3",
39
43
  "slate-react": "^0.117.4",
44
+ "slate-soft-break": "^0.9.0",
40
45
  "unified": "^9.2.0",
41
46
  "unist-builder": "^1.0.3",
42
- "unist-util-visit-parents": "^2.0.1"
47
+ "unist-util-visit-parents": "^2.0.1",
48
+ "vfile-location": "^2.0.6"
43
49
  },
44
50
  "peerDependencies": {
45
51
  "@emotion/react": "^11.11.1",
46
52
  "@emotion/styled": "^11.11.0",
47
- "decap-cms-ui-default": "^3.0.0",
48
- "immutable": "^3.7.6",
53
+ "immutable": "^4.3.9",
49
54
  "lodash": "^4.17.11",
50
55
  "prop-types": "^15.7.2",
51
56
  "react": "^19.1.0",
52
57
  "react-dom": "^19.1.0",
53
- "react-immutable-proptypes": "^2.1.0"
58
+ "react-immutable-proptypes": "^2.1.0",
59
+ "decap-cms-ui-default": "3.9.2"
54
60
  },
55
61
  "devDependencies": {
56
62
  "commonmark": "^0.30.0",
57
63
  "commonmark-spec": "^0.30.0"
58
64
  },
59
- "gitHead": "63c8abdbd1f5c530d6f3d78dccca3379d0370c3e"
60
- }
65
+ "scripts": {
66
+ "develop": "pnpm run build:esm --watch",
67
+ "build": "cross-env NODE_ENV=production webpack",
68
+ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
69
+ }
70
+ }
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import { Component } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import ImmutablePropTypes from 'react-immutable-proptypes';
4
4
  import styled from '@emotion/styled';
@@ -59,7 +59,7 @@ const ToolbarToggleLabel = styled.span`
59
59
  `};
60
60
  `;
61
61
 
62
- export default class Toolbar extends React.Component {
62
+ export default class Toolbar extends Component {
63
63
  static propTypes = {
64
64
  buttons: ImmutablePropTypes.list,
65
65
  editorComponents: ImmutablePropTypes.list,
@@ -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
+ });
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import { Component } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import ImmutablePropTypes from 'react-immutable-proptypes';
4
4
  import { List, Map } from 'immutable';
@@ -22,7 +22,7 @@ export function getEditorComponents() {
22
22
  return _getEditorComponents();
23
23
  }
24
24
 
25
- export default class MarkdownControl extends React.Component {
25
+ export default class MarkdownControl extends Component {
26
26
  static propTypes = {
27
27
  onChange: PropTypes.func.isRequired,
28
28
  onAddAsset: PropTypes.func.isRequired,