decap-cms-widget-markdown 3.4.1 → 3.5.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 +4 -0
- package/dist/decap-cms-widget-markdown.js +4 -4
- package/dist/decap-cms-widget-markdown.js.LICENSE.txt +1 -1
- package/dist/decap-cms-widget-markdown.js.map +1 -1
- package/dist/esm/serializers/remarkShortcodes.js +14 -29
- package/package.json +3 -3
- package/src/serializers/__tests__/__snapshots__/remarkShortcodes.spec.js.snap +132 -0
- package/src/serializers/__tests__/remarkShortcodes.spec.js +68 -29
- package/src/serializers/remarkShortcodes.js +14 -22
|
@@ -34,33 +34,10 @@ export function getLinesWithOffsets(value) {
|
|
|
34
34
|
}));
|
|
35
35
|
return trimmedLines;
|
|
36
36
|
}
|
|
37
|
-
function matchFromLines({
|
|
38
|
-
trimmedLines,
|
|
39
|
-
plugin
|
|
40
|
-
}) {
|
|
41
|
-
for (const {
|
|
42
|
-
line,
|
|
43
|
-
start
|
|
44
|
-
} of trimmedLines) {
|
|
45
|
-
const match = line.match(plugin.pattern);
|
|
46
|
-
if (match) {
|
|
47
|
-
match.index += start;
|
|
48
|
-
return match;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
37
|
function createShortcodeTokenizer({
|
|
53
38
|
plugins
|
|
54
39
|
}) {
|
|
55
40
|
return function tokenizeShortcode(eat, value, silent) {
|
|
56
|
-
// Plugin patterns may rely on `^` and `$` tokens, even if they don't
|
|
57
|
-
// use the multiline flag. To support this, we fall back to searching
|
|
58
|
-
// through each line individually, trimming trailing whitespace and
|
|
59
|
-
// newlines, if we don't initially match on a pattern. We keep track of
|
|
60
|
-
// the starting position of each line so that we can sort correctly
|
|
61
|
-
// across the full multiline matches.
|
|
62
|
-
const trimmedLines = getLinesWithOffsets(value);
|
|
63
|
-
|
|
64
41
|
// Attempt to find a regex match for each plugin's pattern, and then
|
|
65
42
|
// select the first by its occurrence in `value`. This ensures we won't
|
|
66
43
|
// skip a plugin that occurs later in the plugin registry, but earlier
|
|
@@ -68,13 +45,21 @@ function createShortcodeTokenizer({
|
|
|
68
45
|
const [{
|
|
69
46
|
plugin,
|
|
70
47
|
match
|
|
71
|
-
} = {}] = plugins.toArray().map(plugin =>
|
|
72
|
-
|
|
73
|
-
|
|
48
|
+
} = {}] = plugins.toArray().map(plugin => {
|
|
49
|
+
let {
|
|
50
|
+
pattern
|
|
51
|
+
} = plugin;
|
|
52
|
+
// Plugin patterns must start with a caret (^) to match the beginning of the line.
|
|
53
|
+
// If the pattern does not start with a caret, we add it
|
|
54
|
+
// to ensure that remark consumes only the shortcode, without any leading text.
|
|
55
|
+
if (!pattern.source.startsWith('^')) {
|
|
56
|
+
pattern = new RegExp(`^${pattern.source}`, pattern.flags);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
match: value.match(pattern),
|
|
74
60
|
plugin
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
})).filter(({
|
|
61
|
+
};
|
|
62
|
+
}).filter(({
|
|
78
63
|
match
|
|
79
64
|
}) => !!match).sort((a, b) => a.match.index - b.match.index);
|
|
80
65
|
if (match) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decap-cms-widget-markdown",
|
|
3
3
|
"description": "Widget for editing markdown in Decap CMS.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.5.0",
|
|
5
5
|
"homepage": "https://www.decapcms.org/docs/widgets/#markdown",
|
|
6
6
|
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown",
|
|
7
7
|
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"detab": "^2.0.4",
|
|
25
|
-
"dompurify": "^
|
|
25
|
+
"dompurify": "^3.2.6",
|
|
26
26
|
"is-hotkey": "^0.2.0",
|
|
27
27
|
"is-url": "^1.2.4",
|
|
28
28
|
"mdast-util-definitions": "^1.2.3",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"commonmark": "^0.30.0",
|
|
63
63
|
"commonmark-spec": "^0.30.0"
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "d3465f53b7f056ad5d872948a07eaa8e4ae63315"
|
|
66
66
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`remarkParseShortcodes parse pattern with leading caret should be a remark shortcode node 1`] = `
|
|
4
|
+
Object {
|
|
5
|
+
"children": Array [
|
|
6
|
+
Object {
|
|
7
|
+
"data": Object {
|
|
8
|
+
"shortcode": "foo",
|
|
9
|
+
"shortcodeData": Object {
|
|
10
|
+
"bar": "baz",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"type": "shortcode",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
"type": "root",
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`remarkParseShortcodes parse pattern with leading caret should parse multiple shortcodes 1`] = `
|
|
21
|
+
Object {
|
|
22
|
+
"children": Array [
|
|
23
|
+
Object {
|
|
24
|
+
"children": Array [
|
|
25
|
+
Object {
|
|
26
|
+
"type": "text",
|
|
27
|
+
"value": "paragraph",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
"type": "paragraph",
|
|
31
|
+
},
|
|
32
|
+
Object {
|
|
33
|
+
"data": Object {
|
|
34
|
+
"shortcode": "foo",
|
|
35
|
+
"shortcodeData": Object {
|
|
36
|
+
"bar": "bar",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"type": "shortcode",
|
|
40
|
+
},
|
|
41
|
+
Object {
|
|
42
|
+
"data": Object {
|
|
43
|
+
"shortcode": "foo",
|
|
44
|
+
"shortcodeData": Object {
|
|
45
|
+
"bar": "baz",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"type": "shortcode",
|
|
49
|
+
},
|
|
50
|
+
Object {
|
|
51
|
+
"children": Array [
|
|
52
|
+
Object {
|
|
53
|
+
"type": "text",
|
|
54
|
+
"value": "next para",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
"type": "paragraph",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
"type": "root",
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
exports[`remarkParseShortcodes parse pattern without leading caret should handle pattern without leading caret 1`] = `
|
|
65
|
+
Object {
|
|
66
|
+
"children": Array [
|
|
67
|
+
Object {
|
|
68
|
+
"children": Array [
|
|
69
|
+
Object {
|
|
70
|
+
"type": "text",
|
|
71
|
+
"value": "paragraph",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
"type": "paragraph",
|
|
75
|
+
},
|
|
76
|
+
Object {
|
|
77
|
+
"data": Object {
|
|
78
|
+
"shortcode": "foo",
|
|
79
|
+
"shortcodeData": Object {
|
|
80
|
+
"bar": "baz",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
"type": "shortcode",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
"type": "root",
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
exports[`remarkParseShortcodes parse pattern without leading caret should parse multiple shortcodes 1`] = `
|
|
91
|
+
Object {
|
|
92
|
+
"children": Array [
|
|
93
|
+
Object {
|
|
94
|
+
"children": Array [
|
|
95
|
+
Object {
|
|
96
|
+
"type": "text",
|
|
97
|
+
"value": "paragraph",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
"type": "paragraph",
|
|
101
|
+
},
|
|
102
|
+
Object {
|
|
103
|
+
"data": Object {
|
|
104
|
+
"shortcode": "foo",
|
|
105
|
+
"shortcodeData": Object {
|
|
106
|
+
"bar": "bar",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
"type": "shortcode",
|
|
110
|
+
},
|
|
111
|
+
Object {
|
|
112
|
+
"data": Object {
|
|
113
|
+
"shortcode": "foo",
|
|
114
|
+
"shortcodeData": Object {
|
|
115
|
+
"bar": "baz",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
"type": "shortcode",
|
|
119
|
+
},
|
|
120
|
+
Object {
|
|
121
|
+
"children": Array [
|
|
122
|
+
Object {
|
|
123
|
+
"type": "text",
|
|
124
|
+
"value": "next para",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
"type": "paragraph",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
"type": "root",
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { Map, OrderedMap } from 'immutable';
|
|
2
|
+
import unified from 'unified';
|
|
3
|
+
import markdownToRemarkPlugin from 'remark-parse';
|
|
2
4
|
|
|
3
5
|
import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function Parser() {}
|
|
12
|
-
Parser.prototype.blockTokenizers = {};
|
|
13
|
-
Parser.prototype.blockMethods = [];
|
|
14
|
-
remarkParseShortcodes.call({ Parser }, { plugins });
|
|
15
|
-
Parser.prototype.blockTokenizers.shortcode(eat, value);
|
|
7
|
+
function process(value, plugins) {
|
|
8
|
+
return unified()
|
|
9
|
+
.use(markdownToRemarkPlugin, { fences: true, commonmark: true })
|
|
10
|
+
.use(remarkParseShortcodes, { plugins })
|
|
11
|
+
.parse(value);
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) {
|
|
@@ -25,16 +21,6 @@ function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) {
|
|
|
25
21
|
|
|
26
22
|
describe('remarkParseShortcodes', () => {
|
|
27
23
|
describe('pattern matching', () => {
|
|
28
|
-
it('should work', () => {
|
|
29
|
-
const editorComponent = EditorComponent({ pattern: /bar/ });
|
|
30
|
-
process('foo bar', Map({ [editorComponent.id]: editorComponent }));
|
|
31
|
-
expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
|
|
32
|
-
});
|
|
33
|
-
it('should match value surrounded in newlines', () => {
|
|
34
|
-
const editorComponent = EditorComponent({ pattern: /^bar$/ });
|
|
35
|
-
process('foo\n\nbar\n', Map({ [editorComponent.id]: editorComponent }));
|
|
36
|
-
expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
|
|
37
|
-
});
|
|
38
24
|
it('should match multiline shortcodes', () => {
|
|
39
25
|
const editorComponent = EditorComponent({ pattern: /^foo\nbar$/ });
|
|
40
26
|
process('foo\nbar', Map({ [editorComponent.id]: editorComponent }));
|
|
@@ -72,16 +58,69 @@ describe('remarkParseShortcodes', () => {
|
|
|
72
58
|
expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
|
|
73
59
|
});
|
|
74
60
|
});
|
|
75
|
-
describe('
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
61
|
+
describe('parse', () => {
|
|
62
|
+
describe('pattern with leading caret', () => {
|
|
63
|
+
it('should be a remark shortcode node', () => {
|
|
64
|
+
const editorComponent = EditorComponent({
|
|
65
|
+
pattern: /^foo (?<bar>.+)$/,
|
|
66
|
+
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
|
67
|
+
});
|
|
68
|
+
const mdast = process('foo baz', Map({ [editorComponent.id]: editorComponent }));
|
|
69
|
+
expect(removePositions(mdast)).toMatchSnapshot();
|
|
70
|
+
});
|
|
71
|
+
it('should parse multiple shortcodes', () => {
|
|
72
|
+
const editorComponent = EditorComponent({
|
|
73
|
+
pattern: /foo (?<bar>.+)/,
|
|
74
|
+
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
|
75
|
+
});
|
|
76
|
+
const mdast = process(
|
|
77
|
+
'paragraph\n\nfoo bar\n\nfoo baz\n\nnext para',
|
|
78
|
+
Map({ [editorComponent.id]: editorComponent }),
|
|
79
|
+
);
|
|
80
|
+
expect(removePositions(mdast)).toMatchSnapshot();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('pattern without leading caret', () => {
|
|
84
|
+
it('should handle pattern without leading caret', () => {
|
|
85
|
+
const editorComponent = EditorComponent({
|
|
86
|
+
pattern: /foo (?<bar>.+)/,
|
|
87
|
+
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
|
88
|
+
});
|
|
89
|
+
const mdast = process(
|
|
90
|
+
'paragraph\n\nfoo baz',
|
|
91
|
+
Map({ [editorComponent.id]: editorComponent }),
|
|
92
|
+
);
|
|
93
|
+
expect(removePositions(mdast)).toMatchSnapshot();
|
|
94
|
+
});
|
|
95
|
+
it('should parse multiple shortcodes', () => {
|
|
96
|
+
const editorComponent = EditorComponent({
|
|
97
|
+
pattern: /foo (?<bar>.+)/,
|
|
98
|
+
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
|
99
|
+
});
|
|
100
|
+
const mdast = process(
|
|
101
|
+
'paragraph\n\nfoo bar\n\nfoo baz\n\nnext para',
|
|
102
|
+
Map({ [editorComponent.id]: editorComponent }),
|
|
103
|
+
);
|
|
104
|
+
expect(removePositions(mdast)).toMatchSnapshot();
|
|
105
|
+
});
|
|
83
106
|
});
|
|
84
107
|
});
|
|
108
|
+
|
|
109
|
+
function removePositions(obj) {
|
|
110
|
+
if (Array.isArray(obj)) {
|
|
111
|
+
return obj.map(removePositions);
|
|
112
|
+
}
|
|
113
|
+
if (obj && typeof obj === 'object') {
|
|
114
|
+
// eslint-disable-next-line no-unused-vars
|
|
115
|
+
const { position, ...rest } = obj;
|
|
116
|
+
const result = {};
|
|
117
|
+
for (const key in rest) {
|
|
118
|
+
result[key] = removePositions(rest[key]);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
return obj;
|
|
123
|
+
}
|
|
85
124
|
});
|
|
86
125
|
|
|
87
126
|
describe('getLinesWithOffsets', () => {
|
|
@@ -33,36 +33,28 @@ export function getLinesWithOffsets(value) {
|
|
|
33
33
|
return trimmedLines;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function matchFromLines({ trimmedLines, plugin }) {
|
|
37
|
-
for (const { line, start } of trimmedLines) {
|
|
38
|
-
const match = line.match(plugin.pattern);
|
|
39
|
-
if (match) {
|
|
40
|
-
match.index += start;
|
|
41
|
-
return match;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
36
|
function createShortcodeTokenizer({ plugins }) {
|
|
47
37
|
return function tokenizeShortcode(eat, value, silent) {
|
|
48
|
-
// Plugin patterns may rely on `^` and `$` tokens, even if they don't
|
|
49
|
-
// use the multiline flag. To support this, we fall back to searching
|
|
50
|
-
// through each line individually, trimming trailing whitespace and
|
|
51
|
-
// newlines, if we don't initially match on a pattern. We keep track of
|
|
52
|
-
// the starting position of each line so that we can sort correctly
|
|
53
|
-
// across the full multiline matches.
|
|
54
|
-
const trimmedLines = getLinesWithOffsets(value);
|
|
55
|
-
|
|
56
38
|
// Attempt to find a regex match for each plugin's pattern, and then
|
|
57
39
|
// select the first by its occurrence in `value`. This ensures we won't
|
|
58
40
|
// skip a plugin that occurs later in the plugin registry, but earlier
|
|
59
41
|
// in the `value`.
|
|
60
42
|
const [{ plugin, match } = {}] = plugins
|
|
61
43
|
.toArray()
|
|
62
|
-
.map(plugin =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
44
|
+
.map(plugin => {
|
|
45
|
+
let { pattern } = plugin;
|
|
46
|
+
// Plugin patterns must start with a caret (^) to match the beginning of the line.
|
|
47
|
+
// If the pattern does not start with a caret, we add it
|
|
48
|
+
// to ensure that remark consumes only the shortcode, without any leading text.
|
|
49
|
+
if (!pattern.source.startsWith('^')) {
|
|
50
|
+
pattern = new RegExp(`^${pattern.source}`, pattern.flags);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
match: value.match(pattern),
|
|
55
|
+
plugin,
|
|
56
|
+
};
|
|
57
|
+
})
|
|
66
58
|
.filter(({ match }) => !!match)
|
|
67
59
|
.sort((a, b) => a.match.index - b.match.index);
|
|
68
60
|
|