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
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
const
|
|
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
|
-
|
|
8
|
+
blockTokenizers.shortcode = createShortcodeTokenizer({ plugins });
|
|
9
|
+
blockMethods.unshift('shortcode');
|
|
7
10
|
|
|
8
|
-
|
|
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
|
-
|
|
28
|
-
pattern = new RegExp(`^${pattern.source}`, pattern.flags);
|
|
29
|
-
}
|
|
45
|
+
pattern = createPattern(pattern, { anchored: true });
|
|
30
46
|
|
|
31
|
-
match =
|
|
47
|
+
match = pattern.exec(value);
|
|
32
48
|
if (!match) {
|
|
33
|
-
match =
|
|
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
|
|
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
|
-
|
|
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
|
}
|