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.
- package/CHANGELOG.md +12 -0
- package/dist/decap-cms-widget-markdown.js +6 -6
- package/dist/decap-cms-widget-markdown.js.map +1 -1
- package/dist/esm/MarkdownControl/Toolbar.js +6 -6
- package/dist/esm/MarkdownControl/VisualEditor.js +7 -3
- package/dist/esm/MarkdownControl/components/InlineShortcode.js +67 -0
- package/dist/esm/MarkdownControl/index.js +2 -2
- 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/MarkdownPreview.js +37 -3
- package/dist/esm/serializers/remarkRehypeShortcodes.js +18 -15
- 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 +23 -13
- package/src/MarkdownControl/Toolbar.js +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/index.js +2 -2
- 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/MarkdownPreview.js +38 -3
- package/src/__tests__/renderer.spec.js +68 -0
- package/src/serializers/__tests__/remarkShortcodes.spec.js +210 -1
- package/src/serializers/__tests__/slate.spec.js +80 -0
- package/src/serializers/remarkRehypeShortcodes.js +19 -9
- package/src/serializers/remarkShortcodes.js +140 -12
- package/src/serializers/remarkSlate.js +7 -0
- package/src/serializers/slateRemark.js +13 -2
- package/LICENSE +0 -22
- package/dist/esm/MarkdownControl/plugins/BreakToDefaultBlock.js +0 -26
- package/dist/esm/MarkdownControl/plugins/CloseBlock.js +0 -28
- package/dist/esm/MarkdownControl/plugins/CommandsAndQueries.js +0 -180
- package/dist/esm/MarkdownControl/plugins/ForceInsert.js +0 -42
- package/dist/esm/MarkdownControl/plugins/Hotkey.js +0 -26
- package/dist/esm/MarkdownControl/plugins/LineBreak.js +0 -13
- package/dist/esm/MarkdownControl/plugins/Link.js +0 -49
- package/dist/esm/MarkdownControl/plugins/lists/locations/isCursorInItemContainingNestedList.js +0 -6
- package/dist/esm/MarkdownControl/plugins/util.js +0 -11
- package/dist/esm/serializers/remarkImagesToText.js +0 -31
- package/dist/esm/types.js +0 -2
|
@@ -1,10 +1,44 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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__*/
|
|
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
|
|
6
|
-
const
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
match =
|
|
50
|
+
pattern = createPattern(pattern, {
|
|
51
|
+
anchored: true
|
|
52
|
+
});
|
|
53
|
+
match = pattern.exec(value);
|
|
34
54
|
if (!match) {
|
|
35
|
-
match =
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "3.13.0",
|
|
5
5
|
"homepage": "https://www.decapcms.org/docs/widgets/#markdown",
|
|
6
|
-
"repository":
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
25
|
+
export default class MarkdownControl extends Component {
|
|
26
26
|
static propTypes = {
|
|
27
27
|
onChange: PropTypes.func.isRequired,
|
|
28
28
|
onAddAsset: PropTypes.func.isRequired,
|