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
@@ -3,10 +3,20 @@ import { Editor, Transforms } from 'slate';
3
3
  import defaultEmptyBlock from '../blocks/defaultEmptyBlock';
4
4
 
5
5
  function withShortcodes(editor) {
6
- const { isVoid, normalizeNode } = editor;
6
+ const { isVoid, isInline, normalizeNode } = editor;
7
7
 
8
8
  editor.isVoid = element => {
9
- return element.type === 'shortcode' ? true : isVoid(element);
9
+ if (element.type === 'shortcode') {
10
+ return true;
11
+ }
12
+ if (element.type === 'inline-shortcode') {
13
+ return true;
14
+ }
15
+ return isVoid(element);
16
+ };
17
+
18
+ editor.isInline = element => {
19
+ return element.type === 'inline-shortcode' ? true : isInline(element);
10
20
  };
11
21
 
12
22
  // Prevent empty editor after deleting shortcode theat was only child
@@ -6,6 +6,7 @@ import { useSelected } from 'slate-react';
6
6
 
7
7
  import VoidBlock from './components/VoidBlock';
8
8
  import Shortcode from './components/Shortcode';
9
+ import InlineShortcode from './components/InlineShortcode';
9
10
 
10
11
  const bottomMargin = '16px';
11
12
 
@@ -350,6 +351,8 @@ export function Element(props) {
350
351
  <Shortcode {...props}>{children}</Shortcode>
351
352
  </VoidBlock>
352
353
  );
354
+ case 'inline-shortcode':
355
+ return <InlineShortcode {...props} />;
353
356
  default:
354
357
  return <Paragraph style={style}>{children}</Paragraph>;
355
358
  }
@@ -1,8 +1,9 @@
1
1
  import { Map, OrderedMap } from 'immutable';
2
2
  import unified from 'unified';
3
3
  import markdownToRemarkPlugin from 'remark-parse';
4
+ import remarkToMarkdownPlugin from 'remark-stringify';
4
5
 
5
- import { remarkParseShortcodes } from '../remarkShortcodes';
6
+ import { remarkParseShortcodes, createRemarkShortcodeStringifier } from '../remarkShortcodes';
6
7
 
7
8
  function process(value, plugins) {
8
9
  return unified()
@@ -11,6 +12,14 @@ function process(value, plugins) {
11
12
  .parse(value);
12
13
  }
13
14
 
15
+ function stringify(mdast, plugins) {
16
+ return unified()
17
+ .use(remarkToMarkdownPlugin, { commonmark: true })
18
+ .use(createRemarkShortcodeStringifier({ plugins }))
19
+ .stringify(mdast)
20
+ .trim();
21
+ }
22
+
14
23
  function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) {
15
24
  return {
16
25
  id,
@@ -103,6 +112,206 @@ describe('remarkParseShortcodes', () => {
103
112
  expect(removePositions(mdast)).toMatchSnapshot();
104
113
  });
105
114
  });
115
+ describe('inline shortcodes', () => {
116
+ it('should parse inline shortcode inside paragraph without breaking paragraph into blocks', () => {
117
+ const inlineComponent = {
118
+ id: 'ref',
119
+ type: 'inline',
120
+ pattern: /\{\{<\s*ref\s+"(?<target>[^"]+)"\s*>\}\}/,
121
+ fromInline: match => ({ target: match.groups.target }),
122
+ toInline: data => `{{< ref "${data.target}" >}}`,
123
+ };
124
+
125
+ const mdast = process(
126
+ 'Hello {{< ref "about" >}} world',
127
+ Map({ [inlineComponent.id]: inlineComponent }),
128
+ );
129
+
130
+ const stripped = removePositions(mdast);
131
+ expect(stripped).toEqual({
132
+ type: 'root',
133
+ children: [
134
+ {
135
+ type: 'paragraph',
136
+ children: [
137
+ { type: 'text', value: 'Hello ' },
138
+ {
139
+ type: 'inline-shortcode',
140
+ data: {
141
+ shortcode: 'ref',
142
+ shortcodeData: { target: 'about' },
143
+ isVoid: true,
144
+ },
145
+ },
146
+ { type: 'text', value: ' world' },
147
+ ],
148
+ },
149
+ ],
150
+ });
151
+ });
152
+
153
+ it('should parse adjacent CJK characters and punctuation correctly', () => {
154
+ const wikilinkComponent = {
155
+ id: 'wikilink',
156
+ type: 'inline',
157
+ trigger: '[',
158
+ pattern: /\[\[(?<target>[^\]]+)\]\]/,
159
+ fromInline: match => ({ target: match.groups.target }),
160
+ toInline: data => `[[${data.target}]]`,
161
+ };
162
+
163
+ const mdast = process(
164
+ '這是一個[[測試頁面]],請點擊!',
165
+ Map({ [wikilinkComponent.id]: wikilinkComponent }),
166
+ );
167
+
168
+ const stripped = removePositions(mdast);
169
+ expect(stripped).toEqual({
170
+ type: 'root',
171
+ children: [
172
+ {
173
+ type: 'paragraph',
174
+ children: [
175
+ { type: 'text', value: '這是一個' },
176
+ {
177
+ type: 'inline-shortcode',
178
+ data: {
179
+ shortcode: 'wikilink',
180
+ shortcodeData: { target: '測試頁面' },
181
+ isVoid: true,
182
+ },
183
+ },
184
+ { type: 'text', value: ',請點擊!' },
185
+ ],
186
+ },
187
+ ],
188
+ });
189
+ });
190
+
191
+ it('should parse multiple inline shortcodes within single paragraph', () => {
192
+ const tagComponent = {
193
+ id: 'tag',
194
+ type: 'inline',
195
+ trigger: '#',
196
+ pattern: /#(?<name>[a-zA-Z0-9_-]+)/,
197
+ fromInline: match => ({ name: match.groups.name }),
198
+ toInline: data => `#${data.name}`,
199
+ };
200
+
201
+ const mdast = process(
202
+ 'Tags: #react and #decap are cool',
203
+ Map({ [tagComponent.id]: tagComponent }),
204
+ );
205
+
206
+ const stripped = removePositions(mdast);
207
+ expect(stripped.children[0].children).toHaveLength(5);
208
+ expect(stripped.children[0].children[1]).toEqual({
209
+ type: 'inline-shortcode',
210
+ data: {
211
+ shortcode: 'tag',
212
+ shortcodeData: { name: 'react' },
213
+ isVoid: true,
214
+ },
215
+ });
216
+ expect(stripped.children[0].children[3]).toEqual({
217
+ type: 'inline-shortcode',
218
+ data: {
219
+ shortcode: 'tag',
220
+ shortcodeData: { name: 'decap' },
221
+ isVoid: true,
222
+ },
223
+ });
224
+ });
225
+
226
+ it.each([undefined, '['])(
227
+ 'should preserve captures for global patterns with trigger %s',
228
+ trigger => {
229
+ const wikilinkComponent = {
230
+ id: 'wikilink',
231
+ type: 'inline',
232
+ trigger,
233
+ pattern: /\[\[(?<target>[^\]]+)\]\]/g,
234
+ fromInline: match => ({ target: match.groups.target }),
235
+ };
236
+
237
+ const mdast = process(
238
+ 'Before [[target]] after',
239
+ Map({ [wikilinkComponent.id]: wikilinkComponent }),
240
+ );
241
+
242
+ expect(removePositions(mdast).children[0].children[1]).toEqual({
243
+ type: 'inline-shortcode',
244
+ data: {
245
+ shortcode: 'wikilink',
246
+ shortcodeData: { target: 'target' },
247
+ isVoid: true,
248
+ },
249
+ });
250
+ },
251
+ );
252
+
253
+ it('should stringify inline shortcodes correctly in round-trip', () => {
254
+ const inlineComponent = {
255
+ id: 'ref',
256
+ type: 'inline',
257
+ pattern: /\{\{<\s*ref\s+"(?<target>[^"]+)"\s*>\}\}/,
258
+ fromInline: match => ({ target: match.groups.target }),
259
+ toInline: data => `{{< ref "${data.target}" >}}`,
260
+ };
261
+
262
+ const input = 'Hello {{< ref "about" >}} world';
263
+ const plugins = Map({ [inlineComponent.id]: inlineComponent });
264
+ const mdast = process(input, plugins);
265
+ const output = stringify(mdast, plugins);
266
+
267
+ expect(output).toEqual(input);
268
+ });
269
+
270
+ it('should warn when inline component pattern has greedy quantifier', () => {
271
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
272
+ const inlineComponent = {
273
+ id: 'greedy-ref',
274
+ type: 'inline',
275
+ pattern: /\{\{< ref (.+) >\}\}/,
276
+ fromInline: match => ({ target: match[1] }),
277
+ toInline: data => `{{< ref ${data.target} >}}`,
278
+ };
279
+
280
+ process('text', Map({ [inlineComponent.id]: inlineComponent }));
281
+ expect(warnSpy).toHaveBeenCalledWith(
282
+ expect.stringContaining('Potentially greedy RegExp in inline component'),
283
+ );
284
+ warnSpy.mockRestore();
285
+ });
286
+
287
+ it('should handle inline shortcodes nested inside bold text', () => {
288
+ const badgeComponent = {
289
+ id: 'badge',
290
+ type: 'inline',
291
+ pattern: /\[badge:(?<text>[^\]]+)\]/,
292
+ fromInline: match => ({ text: match.groups.text }),
293
+ toInline: data => `[badge:${data.text}]`,
294
+ };
295
+
296
+ const input = '**Important [badge:NEW] Note**';
297
+ const plugins = Map({ [badgeComponent.id]: badgeComponent });
298
+ const mdast = process(input, plugins);
299
+ const stripped = removePositions(mdast);
300
+
301
+ expect(stripped.children[0].children[0].type).toBe('strong');
302
+ const strongChildren = stripped.children[0].children[0].children;
303
+ expect(strongChildren[0]).toEqual({ type: 'text', value: 'Important ' });
304
+ expect(strongChildren[1]).toEqual({
305
+ type: 'inline-shortcode',
306
+ data: {
307
+ shortcode: 'badge',
308
+ shortcodeData: { text: 'NEW' },
309
+ isVoid: true,
310
+ },
311
+ });
312
+ expect(strongChildren[2]).toEqual({ type: 'text', value: ' Note' });
313
+ });
314
+ });
106
315
  });
107
316
 
108
317
  function removePositions(obj) {
@@ -6,6 +6,7 @@ import flow from 'lodash/flow';
6
6
  // eslint-disable-next-line no-unused-vars
7
7
  import h from '../../../test-helpers/h';
8
8
  import { markdownToSlate, slateToMarkdown } from '../index';
9
+ import slateToRemark from '../slateRemark';
9
10
 
10
11
  const process = flow([markdownToSlate, slateToMarkdown]);
11
12
 
@@ -55,6 +56,44 @@ describe('slate', () => {
55
56
  expect(process('*a \nb*')).toEqual('*a\\\nb*');
56
57
  });
57
58
 
59
+ it('should preserve marks around inline shortcodes', () => {
60
+ const marks = [{ type: 'bold' }];
61
+ const mdast = slateToRemark(
62
+ [
63
+ {
64
+ type: 'paragraph',
65
+ children: [
66
+ { text: 'Important ', bold: true, marks },
67
+ {
68
+ type: 'inline-shortcode',
69
+ data: {
70
+ shortcode: 'badge',
71
+ shortcodeData: { text: 'NEW' },
72
+ marks,
73
+ },
74
+ children: [{ text: '' }],
75
+ },
76
+ { text: ' Note', bold: true, marks },
77
+ ],
78
+ },
79
+ ],
80
+ {},
81
+ );
82
+
83
+ expect(mdast.children[0].children).toHaveLength(1);
84
+ expect(mdast.children[0].children[0]).toMatchObject({
85
+ type: 'strong',
86
+ children: [
87
+ { type: 'html', value: 'Important ' },
88
+ {
89
+ type: 'inline-shortcode',
90
+ data: { shortcode: 'badge', shortcodeData: { text: 'NEW' } },
91
+ },
92
+ { type: 'html', value: ' Note' },
93
+ ],
94
+ });
95
+ });
96
+
58
97
  // slateAst no longer valid
59
98
 
60
99
  it('should not output empty headers in markdown', () => {
@@ -299,4 +338,45 @@ describe('slate', () => {
299
338
  expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h~~e**l**l~~o*"`);
300
339
  });
301
340
  });
341
+
342
+ describe('inline-shortcode', () => {
343
+ it('should convert inline-shortcode between Slate and MDAST', () => {
344
+ const slateAst = (
345
+ <editor>
346
+ <element type="paragraph">
347
+ <text>Hello </text>
348
+ <element
349
+ type="inline-shortcode"
350
+ data={{
351
+ shortcode: 'ref',
352
+ shortcodeData: { target: 'about' },
353
+ }}
354
+ >
355
+ <text></text>
356
+ </element>
357
+ <text> world</text>
358
+ </element>
359
+ </editor>
360
+ );
361
+
362
+ const refPlugin = {
363
+ id: 'ref',
364
+ type: 'inline',
365
+ pattern: /\{\{<\s*ref\s+"(?<target>[^"]+)"\s*>\}\}/,
366
+ fromInline: match => ({ target: match.groups.target }),
367
+ toInline: data => `{{< ref "${data.target}" >}}`,
368
+ };
369
+
370
+ const markdown = slateToMarkdown(slateAst.children, {
371
+ remarkPlugins: [
372
+ function () {
373
+ this.Compiler.prototype.visitors['inline-shortcode'] = node =>
374
+ refPlugin.toInline(node.data.shortcodeData);
375
+ },
376
+ ],
377
+ });
378
+
379
+ expect(markdown).toEqual('Hello {{< ref "about" >}} world');
380
+ });
381
+ });
302
382
  });
@@ -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';
@@ -14,8 +13,21 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid
14
13
  return transform;
15
14
 
16
15
  function transform(root) {
17
- const transformedChildren = map(root.children, processShortcodes);
18
- return { ...root, children: transformedChildren };
16
+ function walk(node) {
17
+ if (!node) return node;
18
+ if (has(node, ['data', 'shortcode'])) {
19
+ return processShortcodes(node);
20
+ }
21
+ if (Array.isArray(node.children)) {
22
+ return {
23
+ ...node,
24
+ children: node.children.map(walk),
25
+ };
26
+ }
27
+ return node;
28
+ }
29
+
30
+ return walk(root);
19
31
  }
20
32
 
21
33
  /**
@@ -33,6 +45,7 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid
33
45
  */
34
46
  const { shortcode, shortcodeData } = node.data;
35
47
  const plugin = plugins.get(shortcode);
48
+ if (!plugin) return node;
36
49
 
37
50
  /**
38
51
  * Run the shortcode plugin's `toPreview` method, which will return either
@@ -45,9 +58,7 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid
45
58
  /**
46
59
  * Return a new 'html' type node containing the shortcode preview markup.
47
60
  */
48
- const textNode = u('html', valueHtml);
49
- const children = [textNode];
50
- return { ...node, children };
61
+ return u('html', valueHtml);
51
62
  }
52
63
 
53
64
  /**
@@ -1,16 +1,31 @@
1
1
  export function remarkParseShortcodes({ plugins }) {
2
2
  const Parser = this.Parser;
3
- const tokenizers = Parser.prototype.blockTokenizers;
4
- const methods = Parser.prototype.blockMethods;
3
+ const blockTokenizers = Parser.prototype.blockTokenizers;
4
+ const blockMethods = Parser.prototype.blockMethods;
5
+ const inlineTokenizers = Parser.prototype.inlineTokenizers;
6
+ const inlineMethods = Parser.prototype.inlineMethods;
5
7
 
6
- tokenizers.shortcode = createShortcodeTokenizer({ plugins });
8
+ blockTokenizers.shortcode = createShortcodeTokenizer({ plugins });
9
+ blockMethods.unshift('shortcode');
7
10
 
8
- methods.unshift('shortcode');
11
+ inlineTokenizers.inlineShortcode = createInlineShortcodeTokenizer({ plugins });
12
+ inlineMethods.unshift('inlineShortcode');
13
+ }
14
+
15
+ function createPattern(pattern, { anchored = false } = {}) {
16
+ let source = pattern.source;
17
+ if (anchored && !source.startsWith('^')) {
18
+ source = `^${source}`;
19
+ } else if (!anchored && source.startsWith('^')) {
20
+ source = source.slice(1);
21
+ }
22
+
23
+ return new RegExp(source, pattern.flags.replace(/[gy]/g, ''));
9
24
  }
10
25
 
11
26
  function createShortcodeTokenizer({ plugins }) {
12
27
  plugins.forEach(plugin => {
13
- if (plugin.pattern.flags.includes('m')) {
28
+ if (plugin.pattern && plugin.pattern.flags.includes('m')) {
14
29
  console.warn(
15
30
  `Invalid RegExp: editor component '${plugin.id}' must not use the multiline flag in its pattern.`,
16
31
  );
@@ -20,17 +35,18 @@ function createShortcodeTokenizer({ plugins }) {
20
35
  let match;
21
36
  const potentialMatchValue = value.split('\n\n')[0].trimEnd();
22
37
  const plugin = plugins.find(plugin => {
38
+ if (plugin.type === 'inline') {
39
+ return false;
40
+ }
23
41
  let { pattern } = plugin;
24
42
  // Plugin patterns must start with a caret (^) to match the beginning of the block.
25
43
  // If the pattern does not start with a caret, we add it
26
44
  // to ensure that remark consumes only the shortcode, without any leading text.
27
- if (!pattern.source.startsWith('^')) {
28
- pattern = new RegExp(`^${pattern.source}`, pattern.flags);
29
- }
45
+ pattern = createPattern(pattern, { anchored: true });
30
46
 
31
- match = value.match(pattern);
47
+ match = pattern.exec(value);
32
48
  if (!match) {
33
- match = potentialMatchValue.match(pattern);
49
+ match = pattern.exec(potentialMatchValue);
34
50
  }
35
51
 
36
52
  return !!match;
@@ -46,7 +62,11 @@ function createShortcodeTokenizer({ plugins }) {
46
62
  return true;
47
63
  }
48
64
 
49
- const shortcodeData = plugin.fromBlock(match);
65
+ const shortcodeData = plugin.fromBlock
66
+ ? plugin.fromBlock(match)
67
+ : plugin.fromInline
68
+ ? plugin.fromInline(match)
69
+ : match;
50
70
 
51
71
  try {
52
72
  return eat(match[0])({
@@ -65,17 +85,125 @@ function createShortcodeTokenizer({ plugins }) {
65
85
  };
66
86
  }
67
87
 
88
+ function createInlineShortcodeTokenizer({ plugins }) {
89
+ plugins.forEach(plugin => {
90
+ if (plugin.type === 'inline' && plugin.pattern) {
91
+ if (plugin.pattern.flags.includes('m')) {
92
+ console.warn(
93
+ `Invalid RegExp: inline editor component '${plugin.id}' must not use the multiline flag in its pattern.`,
94
+ );
95
+ }
96
+ if (/(\.\*|\.\+)(?!\?)/.test(plugin.pattern.source)) {
97
+ console.warn(
98
+ `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.`,
99
+ );
100
+ }
101
+ }
102
+ });
103
+
104
+ function tokenizeInlineShortcode(eat, value, silent) {
105
+ let match;
106
+ const plugin = plugins.find(plugin => {
107
+ if (plugin.type !== 'inline') {
108
+ return false;
109
+ }
110
+ let { pattern } = plugin;
111
+ // Inline patterns must match at the current offset (leading ^)
112
+ pattern = createPattern(pattern, { anchored: true });
113
+
114
+ match = pattern.exec(value);
115
+ return !!match;
116
+ });
117
+
118
+ if (match) {
119
+ if (silent) {
120
+ return true;
121
+ }
122
+
123
+ const shortcodeData = plugin.fromInline
124
+ ? plugin.fromInline(match)
125
+ : plugin.fromBlock
126
+ ? plugin.fromBlock(match)
127
+ : match;
128
+
129
+ try {
130
+ return eat(match[0])({
131
+ type: 'inline-shortcode',
132
+ data: {
133
+ shortcode: plugin.id,
134
+ shortcodeData,
135
+ isVoid: true,
136
+ },
137
+ });
138
+ } catch (e) {
139
+ console.warn(
140
+ `Sent invalid data to remark. Inline plugin: ${plugin.id}. Value: ${
141
+ match[0]
142
+ }. Data: ${JSON.stringify(shortcodeData)}`,
143
+ );
144
+ return false;
145
+ }
146
+ }
147
+ }
148
+
149
+ tokenizeInlineShortcode.locator = function locateInlineShortcode(value, fromIndex) {
150
+ let minIndex = -1;
151
+ plugins.forEach(plugin => {
152
+ if (plugin.type !== 'inline') {
153
+ return;
154
+ }
155
+
156
+ if (plugin.trigger) {
157
+ const triggerIndex = value.indexOf(plugin.trigger, fromIndex);
158
+ if (triggerIndex !== -1 && (minIndex === -1 || triggerIndex < minIndex)) {
159
+ minIndex = triggerIndex;
160
+ }
161
+ } else {
162
+ const searchPattern = createPattern(plugin.pattern);
163
+ const slice = value.slice(fromIndex);
164
+ const match = searchPattern.exec(slice);
165
+ if (match && typeof match.index === 'number') {
166
+ const foundIndex = fromIndex + match.index;
167
+ if (minIndex === -1 || foundIndex < minIndex) {
168
+ minIndex = foundIndex;
169
+ }
170
+ }
171
+ }
172
+ });
173
+ return minIndex;
174
+ };
175
+
176
+ return tokenizeInlineShortcode;
177
+ }
178
+
68
179
  export function createRemarkShortcodeStringifier({ plugins }) {
69
180
  return function remarkStringifyShortcodes() {
70
181
  const Compiler = this.Compiler;
71
182
  const visitors = Compiler.prototype.visitors;
72
183
 
73
184
  visitors.shortcode = shortcode;
185
+ visitors['inline-shortcode'] = inlineShortcode;
74
186
 
75
187
  function shortcode(node) {
76
188
  const { data } = node;
77
189
  const plugin = plugins.find(plugin => data.shortcode === plugin.id);
78
- return plugin.toBlock(data.shortcodeData);
190
+ if (!plugin) return '';
191
+ return plugin.toBlock
192
+ ? plugin.toBlock(data.shortcodeData)
193
+ : plugin.toInline
194
+ ? plugin.toInline(data.shortcodeData)
195
+ : '';
196
+ }
197
+
198
+ function inlineShortcode(node) {
199
+ const { data } = node;
200
+ const plugin = plugins.find(plugin => data.shortcode === plugin.id);
201
+ if (!plugin) return '';
202
+ return plugin.toInline
203
+ ? plugin.toInline(data.shortcodeData)
204
+ : plugin.toBlock
205
+ ? plugin.toBlock(data.shortcodeData)
206
+ : '';
79
207
  }
80
208
  };
81
209
  }
@@ -21,6 +21,7 @@ const typeMap = {
21
21
  link: 'link',
22
22
  image: 'image',
23
23
  shortcode: 'shortcode',
24
+ 'inline-shortcode': 'inline-shortcode',
24
25
  };
25
26
 
26
27
  /**
@@ -279,6 +280,12 @@ export default function remarkToSlate({ voidCodeBlock } = {}) {
279
280
  return createBlock(typeMap[node.type], nodes, { data });
280
281
  }
281
282
 
283
+ case 'inline-shortcode': {
284
+ const nodes = [createText('')];
285
+ const data = { ...node.data, id: node.data.shortcode, shortcodeNew: true };
286
+ return createInline(typeMap[node.type], { data }, nodes);
287
+ }
288
+
282
289
  case 'text': {
283
290
  const text = node.value;
284
291
  return createText(text);
@@ -32,6 +32,7 @@ const typeMap = {
32
32
  link: 'link',
33
33
  image: 'image',
34
34
  shortcode: 'shortcode',
35
+ 'inline-shortcode': 'inline-shortcode',
35
36
  };
36
37
 
37
38
  /**
@@ -62,7 +63,7 @@ const blockTypes = [
62
63
  'table-cell',
63
64
  ];
64
65
 
65
- const inlineTypes = ['link', 'image', 'break'];
66
+ const inlineTypes = ['link', 'image', 'break', 'inline-shortcode'];
66
67
 
67
68
  const leadingWhitespaceExp = /^\s+\S/;
68
69
  const trailingWhitespaceExp = /(?!\S)\s+$/;
@@ -114,7 +115,8 @@ export default function slateToRemark(value, { voidCodeBlock }) {
114
115
  }
115
116
 
116
117
  case 'image':
117
- case 'break': {
118
+ case 'break':
119
+ case 'inline-shortcode': {
118
120
  const data = omit(node.data, 'marks');
119
121
  return { ...node, data };
120
122
  }
@@ -153,6 +155,7 @@ export default function slateToRemark(value, { voidCodeBlock }) {
153
155
 
154
156
  case 'break':
155
157
  case 'image':
158
+ case 'inline-shortcode':
156
159
  return map(get(node, ['data', 'marks']), mark => mark.type);
157
160
 
158
161
  default:
@@ -468,6 +471,14 @@ export default function slateToRemark(value, { voidCodeBlock }) {
468
471
  const { url, title, alt, ...data } = get(node, 'data', {});
469
472
  return u(typeMap[node.type], { url, title, alt, data });
470
473
  }
474
+
475
+ /**
476
+ * Inline Shortcodes
477
+ */
478
+ case 'inline-shortcode': {
479
+ const { data } = node;
480
+ return u(typeMap[node.type], { data });
481
+ }
471
482
  }
472
483
  }
473
484
  }