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.
@@ -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
- match: value.match(plugin.pattern) || matchFromLines({
73
- trimmedLines,
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
- plugin
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.1",
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": "^2.2.6",
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": "bfe122aee875ceaaf9f132110db7f8797eedfc5b"
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
- // Stub of Remark Parser
6
- function process(value, plugins, processEat = () => {}) {
7
- function eat() {
8
- return processEat;
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('output', () => {
76
- it('should be a remark shortcode node', () => {
77
- const processEat = jest.fn();
78
- const shortcodeData = { bar: 'baz' };
79
- const expectedNode = { type: 'shortcode', data: { shortcode: 'foo', shortcodeData } };
80
- const editorComponent = EditorComponent({ pattern: /bar/, fromBlock: () => shortcodeData });
81
- process('foo bar', Map({ [editorComponent.id]: editorComponent }), processEat);
82
- expect(processEat).toHaveBeenCalledWith(expectedNode);
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
- match: value.match(plugin.pattern) || matchFromLines({ trimmedLines, plugin }),
64
- plugin,
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