@uiw/react-md-editor 4.1.6 → 4.1.7
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/README.md +1 -1
- package/dist/mdeditor.css +5 -0
- package/dist/mdeditor.js +2378 -1029
- package/dist/mdeditor.min.css +1 -1
- package/dist/mdeditor.min.js +1 -1
- package/esm/Editor.factory.js +60 -10
- package/esm/components/TextArea/Markdown.common.d.ts +3 -5
- package/esm/components/TextArea/Markdown.common.js +2 -56
- package/esm/components/TextArea/Markdown.d.ts +3 -5
- package/esm/components/TextArea/Markdown.factory.d.ts +6 -0
- package/esm/components/TextArea/Markdown.factory.js +138 -0
- package/esm/components/TextArea/Markdown.js +2 -63
- package/esm/components/TextArea/index.css +5 -0
- package/esm/components/TextArea/index.less +5 -0
- package/esm/components/TextArea/splitBlocks.d.ts +13 -0
- package/esm/components/TextArea/splitBlocks.js +111 -0
- package/esm/package.json +1 -0
- package/lib/Editor.factory.js +68 -11
- package/lib/components/TextArea/Markdown.common.d.ts +3 -5
- package/lib/components/TextArea/Markdown.common.js +3 -60
- package/lib/components/TextArea/Markdown.d.ts +3 -5
- package/lib/components/TextArea/Markdown.factory.d.ts +6 -0
- package/lib/components/TextArea/Markdown.factory.js +148 -0
- package/lib/components/TextArea/Markdown.js +3 -67
- package/lib/components/TextArea/index.less +5 -0
- package/lib/components/TextArea/splitBlocks.d.ts +13 -0
- package/lib/components/TextArea/splitBlocks.js +117 -0
- package/markdown-editor.css +5 -0
- package/package.json +18 -12
- package/src/Editor.factory.tsx +59 -4
- package/src/components/TextArea/Markdown.common.tsx +3 -46
- package/src/components/TextArea/Markdown.factory.tsx +132 -0
- package/src/components/TextArea/Markdown.tsx +3 -55
- package/src/components/TextArea/index.less +5 -0
- package/src/components/TextArea/splitBlocks.ts +114 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uiw/react-md-editor",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.7",
|
|
4
4
|
"description": "A markdown editor with preview, implemented with React.js and TypeScript.",
|
|
5
5
|
"homepage": "https://uiwjs.github.io/react-md-editor/",
|
|
6
6
|
"funding": "https://jaywcjlove.github.io/#/sponsor",
|
|
@@ -12,29 +12,34 @@
|
|
|
12
12
|
"./package.json": "./package.json",
|
|
13
13
|
"./markdown-editor.css": "./markdown-editor.css",
|
|
14
14
|
".": {
|
|
15
|
-
"import": "./esm/index.js",
|
|
16
15
|
"types": "./lib/index.d.ts",
|
|
17
|
-
"
|
|
16
|
+
"import": "./esm/index.js",
|
|
17
|
+
"require": "./lib/index.js",
|
|
18
|
+
"default": "./esm/index.js"
|
|
18
19
|
},
|
|
19
20
|
"./common": {
|
|
20
|
-
"import": "./esm/index.common.js",
|
|
21
21
|
"types": "./lib/index.common.d.ts",
|
|
22
|
-
"
|
|
22
|
+
"import": "./esm/index.common.js",
|
|
23
|
+
"require": "./lib/index.common.js",
|
|
24
|
+
"default": "./esm/index.common.js"
|
|
23
25
|
},
|
|
24
26
|
"./nohighlight": {
|
|
25
|
-
"import": "./esm/index.nohighlight.js",
|
|
26
27
|
"types": "./lib/index.nohighlight.d.ts",
|
|
27
|
-
"
|
|
28
|
+
"import": "./esm/index.nohighlight.js",
|
|
29
|
+
"require": "./lib/index.nohighlight.js",
|
|
30
|
+
"default": "./esm/index.nohighlight.js"
|
|
28
31
|
},
|
|
29
32
|
"./commands": {
|
|
30
|
-
"import": "./esm/commands/index.js",
|
|
31
33
|
"types": "./lib/commands/index.d.ts",
|
|
32
|
-
"
|
|
34
|
+
"import": "./esm/commands/index.js",
|
|
35
|
+
"require": "./lib/commands/index.js",
|
|
36
|
+
"default": "./esm/commands/index.js"
|
|
33
37
|
},
|
|
34
38
|
"./commands-cn": {
|
|
35
|
-
"import": "./esm/commands/index.cn.js",
|
|
36
39
|
"types": "./lib/commands/index.cn.d.ts",
|
|
37
|
-
"
|
|
40
|
+
"import": "./esm/commands/index.cn.js",
|
|
41
|
+
"require": "./lib/commands/index.cn.js",
|
|
42
|
+
"default": "./esm/commands/index.cn.js"
|
|
38
43
|
}
|
|
39
44
|
},
|
|
40
45
|
"repository": {
|
|
@@ -79,5 +84,6 @@
|
|
|
79
84
|
"uiw",
|
|
80
85
|
"uiwjs",
|
|
81
86
|
"code"
|
|
82
|
-
]
|
|
87
|
+
],
|
|
88
|
+
"types": "lib/index.d.ts"
|
|
83
89
|
}
|
package/src/Editor.factory.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useReducer, useMemo, useRef, useImperativeHandle } from 'react';
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useReducer, useMemo, useRef, useImperativeHandle } from 'react';
|
|
2
2
|
import { ToolbarVisibility } from './components/Toolbar/';
|
|
3
3
|
import DragBar from './components/DragBar/';
|
|
4
4
|
import { getCommands, getExtraCommands, type ICommand, type TextState, TextAreaCommandOrchestrator } from './commands/';
|
|
@@ -12,6 +12,45 @@ function setGroupPopFalse(data: Record<string, boolean> = {}) {
|
|
|
12
12
|
return data;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const DEFAULT_PREVIEW_OPTIONS: NonNullable<MDEditorProps['previewOptions']> = {};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lets the preview render after the textarea on React 18+, so typing in a large
|
|
19
|
+
* document stays responsive. Older React versions render it right away, as before.
|
|
20
|
+
*/
|
|
21
|
+
const useDeferredValue: <T>(value: T) => T = (React as any).useDeferredValue || ((value) => value);
|
|
22
|
+
|
|
23
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
24
|
+
|
|
25
|
+
const SLOW_PREVIEW_MS = 50;
|
|
26
|
+
const MAX_PREVIEW_DELAY_MS = 500;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keeps the preview in step with every keystroke while it renders fast. Once a preview
|
|
30
|
+
* render takes longer than SLOW_PREVIEW_MS, it waits for a pause in typing as long as
|
|
31
|
+
* that render (at most MAX_PREVIEW_DELAY_MS) before updating.
|
|
32
|
+
* https://github.com/uiwjs/react-md-editor/issues/620
|
|
33
|
+
*/
|
|
34
|
+
function usePreviewSource(markdown: string, previewRenderMs: React.MutableRefObject<number>) {
|
|
35
|
+
const [, rerender] = useReducer((count: number) => count + 1, 0);
|
|
36
|
+
const shownRef = useRef(markdown);
|
|
37
|
+
if (previewRenderMs.current < SLOW_PREVIEW_MS) {
|
|
38
|
+
shownRef.current = markdown;
|
|
39
|
+
}
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (shownRef.current === markdown) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const delay = Math.min(previewRenderMs.current, MAX_PREVIEW_DELAY_MS);
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
shownRef.current = markdown;
|
|
47
|
+
rerender();
|
|
48
|
+
}, delay);
|
|
49
|
+
return () => clearTimeout(timer);
|
|
50
|
+
}, [markdown, previewRenderMs]);
|
|
51
|
+
return shownRef.current;
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
export interface RefMDEditor extends ContextStore {}
|
|
16
55
|
|
|
17
56
|
type PreviewComponent = React.ComponentType<any>;
|
|
@@ -25,6 +64,15 @@ export function createMDEditor<
|
|
|
25
64
|
const PreviewComponent = MarkdownPreview as React.ComponentType<any>;
|
|
26
65
|
const TextAreaComponent = TextArea as React.ComponentType<any>;
|
|
27
66
|
|
|
67
|
+
function TimedPreview(props: { renderMsRef: React.MutableRefObject<number>; [key: string]: any }) {
|
|
68
|
+
const { renderMsRef, ...previewProps } = props;
|
|
69
|
+
const renderStart = performance.now();
|
|
70
|
+
useIsomorphicLayoutEffect(() => {
|
|
71
|
+
renderMsRef.current = performance.now() - renderStart;
|
|
72
|
+
});
|
|
73
|
+
return <PreviewComponent {...previewProps} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
28
76
|
const InternalMDEditor = React.forwardRef<RefMDEditor, MDEditorProps>(
|
|
29
77
|
(props: MDEditorProps, ref: React.ForwardedRef<RefMDEditor>) => {
|
|
30
78
|
const {
|
|
@@ -42,7 +90,7 @@ export function createMDEditor<
|
|
|
42
90
|
preview: previewType = 'live',
|
|
43
91
|
fullscreen = false,
|
|
44
92
|
overflow = true,
|
|
45
|
-
previewOptions =
|
|
93
|
+
previewOptions = DEFAULT_PREVIEW_OPTIONS,
|
|
46
94
|
textareaProps,
|
|
47
95
|
id,
|
|
48
96
|
name,
|
|
@@ -204,13 +252,20 @@ export function createMDEditor<
|
|
|
204
252
|
|
|
205
253
|
const previewClassName = `${prefixCls}-preview ${previewOptions.className || ''}`;
|
|
206
254
|
const handlePreviewScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => handleScroll(e, 'preview');
|
|
255
|
+
const previewRenderMs = useRef(0);
|
|
256
|
+
const previewMarkdown = useDeferredValue(usePreviewSource(state.markdown || '', previewRenderMs));
|
|
207
257
|
let mdPreview = useMemo(
|
|
208
258
|
() => (
|
|
209
259
|
<div ref={previewRef} className={previewClassName}>
|
|
210
|
-
<
|
|
260
|
+
<TimedPreview
|
|
261
|
+
{...previewOptions}
|
|
262
|
+
renderMsRef={previewRenderMs}
|
|
263
|
+
onScroll={handlePreviewScroll}
|
|
264
|
+
source={previewMarkdown}
|
|
265
|
+
/>
|
|
211
266
|
</div>
|
|
212
267
|
),
|
|
213
|
-
[previewClassName, previewOptions,
|
|
268
|
+
[previewClassName, previewOptions, previewMarkdown],
|
|
214
269
|
);
|
|
215
270
|
const preview = components?.preview && components?.preview(state.markdown || '', state, dispatch);
|
|
216
271
|
if (preview && React.isValidElement(preview)) {
|
|
@@ -1,49 +1,6 @@
|
|
|
1
|
-
import React, { useContext, useEffect } from 'react';
|
|
2
|
-
import { rehype } from 'rehype';
|
|
3
1
|
import rehypePrism from 'rehype-prism-plus/common';
|
|
4
|
-
import {
|
|
5
|
-
import { EditorContext } from '../../Context';
|
|
2
|
+
import { createMarkdown } from './Markdown.factory';
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
return sHtml.replace(
|
|
9
|
-
/[<&"]/g,
|
|
10
|
-
(c: string) => (({ '<': '<', '>': '>', '&': '&', '"': '"' }) as Record<string, string>)[c],
|
|
11
|
-
);
|
|
12
|
-
}
|
|
4
|
+
export type { MarkdownProps } from './Markdown.factory';
|
|
13
5
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
export default function Markdown(props: MarkdownProps) {
|
|
17
|
-
const { prefixCls } = props;
|
|
18
|
-
const { markdown = '', highlightEnable, dispatch } = useContext(EditorContext);
|
|
19
|
-
const preRef = React.createRef<HTMLPreElement>();
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (preRef.current && dispatch) {
|
|
22
|
-
dispatch({ textareaPre: preRef.current });
|
|
23
|
-
}
|
|
24
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
-
}, []);
|
|
26
|
-
if (!markdown) {
|
|
27
|
-
return <pre ref={preRef} className={`${prefixCls}-text-pre wmde-markdown-color`} />;
|
|
28
|
-
}
|
|
29
|
-
let mdStr = `<pre class="language-markdown ${prefixCls}-text-pre wmde-markdown-color"><code class="language-markdown">${html2Escape(
|
|
30
|
-
String.raw`${markdown}`,
|
|
31
|
-
)}\n</code></pre>`;
|
|
32
|
-
|
|
33
|
-
if (highlightEnable) {
|
|
34
|
-
try {
|
|
35
|
-
mdStr = rehype()
|
|
36
|
-
.data('settings', { fragment: true })
|
|
37
|
-
// https://github.com/uiwjs/react-md-editor/issues/593
|
|
38
|
-
// @ts-ignore
|
|
39
|
-
.use(rehypePrism, { ignoreMissing: true })
|
|
40
|
-
.processSync(mdStr)
|
|
41
|
-
.toString();
|
|
42
|
-
} catch (error) {}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return React.createElement('div', {
|
|
46
|
-
className: 'wmde-markdown-color',
|
|
47
|
-
dangerouslySetInnerHTML: { __html: mdStr || '' },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
6
|
+
export default createMarkdown(rehypePrism);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { rehype } from 'rehype';
|
|
3
|
+
import type rehypePrismPlus from 'rehype-prism-plus';
|
|
4
|
+
import { type IProps } from '../../Types';
|
|
5
|
+
import { EditorContext } from '../../Context';
|
|
6
|
+
import { splitBlocks } from './splitBlocks';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prism's markdown grammar takes quadratic time on long lines, so a single base64 image
|
|
10
|
+
* line can block the editor for minutes. Blocks with such a line are shown uncoloured.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_HIGHLIGHT_LINE_LENGTH = 1000;
|
|
13
|
+
|
|
14
|
+
export interface MarkdownProps extends IProps, React.HTMLAttributes<HTMLPreElement> {}
|
|
15
|
+
|
|
16
|
+
type HighlightedBlock = {
|
|
17
|
+
id: number;
|
|
18
|
+
html: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function html2Escape(sHtml: string) {
|
|
22
|
+
return sHtml.replace(
|
|
23
|
+
/[<&"]/g,
|
|
24
|
+
(c: string) => (({ '<': '<', '>': '>', '&': '&', '"': '"' }) as Record<string, string>)[c],
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasLongLine(text: string) {
|
|
29
|
+
let lineStart = 0;
|
|
30
|
+
while (lineStart <= text.length) {
|
|
31
|
+
const lineEnd = text.indexOf('\n', lineStart);
|
|
32
|
+
const end = lineEnd === -1 ? text.length : lineEnd;
|
|
33
|
+
if (end - lineStart > MAX_HIGHLIGHT_LINE_LENGTH) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
lineStart = end + 1;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getCodeInnerHtml(html: string) {
|
|
42
|
+
const codeStart = html.indexOf('<code');
|
|
43
|
+
const contentStart = html.indexOf('>', codeStart) + 1;
|
|
44
|
+
const contentEnd = html.lastIndexOf('</code>');
|
|
45
|
+
return html.slice(contentStart, contentEnd);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MarkdownBlock = React.memo(function MarkdownBlock(props: { html: string }) {
|
|
49
|
+
return React.createElement('span', { dangerouslySetInnerHTML: { __html: props.html } });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export function createMarkdown(rehypePrism: typeof rehypePrismPlus) {
|
|
53
|
+
const processor = rehype()
|
|
54
|
+
.data('settings', { fragment: true })
|
|
55
|
+
// https://github.com/uiwjs/react-md-editor/issues/593
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
.use(rehypePrism, { ignoreMissing: true });
|
|
58
|
+
|
|
59
|
+
function highlightBlock(block: string) {
|
|
60
|
+
const escaped = html2Escape(block);
|
|
61
|
+
if (hasLongLine(block)) {
|
|
62
|
+
return escaped;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const html = processor.processSync(`<pre><code class="language-markdown">${escaped}</code></pre>`).toString();
|
|
66
|
+
return getCodeInnerHtml(html);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return escaped;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Highlighting runs per block, and blocks whose text did not change reuse their cached
|
|
74
|
+
* HTML and DOM, so typing in a large document only re-highlights the edited block.
|
|
75
|
+
* https://github.com/uiwjs/react-md-editor/issues/620
|
|
76
|
+
*/
|
|
77
|
+
return function Markdown(props: MarkdownProps) {
|
|
78
|
+
const { prefixCls } = props;
|
|
79
|
+
const { markdown = '', highlightEnable, dispatch } = useContext(EditorContext);
|
|
80
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
81
|
+
const cacheRef = useRef(new Map<string, HighlightedBlock>());
|
|
82
|
+
const nextIdRef = useRef(0);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (preRef.current && dispatch) {
|
|
85
|
+
dispatch({ textareaPre: preRef.current });
|
|
86
|
+
}
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const blocks = useMemo(() => {
|
|
91
|
+
if (!highlightEnable) {
|
|
92
|
+
cacheRef.current = new Map();
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const cache = new Map<string, HighlightedBlock>();
|
|
96
|
+
const occurrences = new Map<string, number>();
|
|
97
|
+
const result = splitBlocks(markdown).map((block) => {
|
|
98
|
+
let highlighted = cacheRef.current.get(block) || cache.get(block);
|
|
99
|
+
if (!highlighted) {
|
|
100
|
+
highlighted = { id: nextIdRef.current++, html: highlightBlock(block) };
|
|
101
|
+
}
|
|
102
|
+
cache.set(block, highlighted);
|
|
103
|
+
const occurrence = occurrences.get(block) || 0;
|
|
104
|
+
occurrences.set(block, occurrence + 1);
|
|
105
|
+
return { key: `${highlighted.id}-${occurrence}`, html: highlighted.html };
|
|
106
|
+
});
|
|
107
|
+
cacheRef.current = cache;
|
|
108
|
+
return result;
|
|
109
|
+
}, [markdown, highlightEnable]);
|
|
110
|
+
|
|
111
|
+
if (!markdown) {
|
|
112
|
+
return <pre ref={preRef} className={`${prefixCls}-text-pre wmde-markdown-color`} />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="wmde-markdown-color">
|
|
117
|
+
<pre ref={preRef} className={`language-markdown ${prefixCls}-text-pre wmde-markdown-color`}>
|
|
118
|
+
{highlightEnable ? (
|
|
119
|
+
<code className="language-markdown code-highlight">
|
|
120
|
+
{blocks.map((block) => (
|
|
121
|
+
<MarkdownBlock key={block.key} html={block.html} />
|
|
122
|
+
))}
|
|
123
|
+
{'\n'}
|
|
124
|
+
</code>
|
|
125
|
+
) : (
|
|
126
|
+
<code className="language-markdown">{`${markdown}\n`}</code>
|
|
127
|
+
)}
|
|
128
|
+
</pre>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -1,58 +1,6 @@
|
|
|
1
|
-
import React, { useContext, useEffect } from 'react';
|
|
2
|
-
import { rehype } from 'rehype';
|
|
3
1
|
import rehypePrism from 'rehype-prism-plus';
|
|
4
|
-
import {
|
|
5
|
-
import { EditorContext } from '../../Context';
|
|
2
|
+
import { createMarkdown } from './Markdown.factory';
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
return (
|
|
9
|
-
sHtml
|
|
10
|
-
// .replace(/```(\w+)?([\s\S]*?)(\s.+)?```/g, (str: string) => {
|
|
11
|
-
// return str.replace(
|
|
12
|
-
// /[<&"]/g,
|
|
13
|
-
// (c: string) => (({ '<': '<', '>': '>', '&': '&', '"': '"' } as Record<string, string>)[c]),
|
|
14
|
-
// );
|
|
15
|
-
// })
|
|
16
|
-
.replace(
|
|
17
|
-
/[<&"]/g,
|
|
18
|
-
(c: string) => (({ '<': '<', '>': '>', '&': '&', '"': '"' }) as Record<string, string>)[c],
|
|
19
|
-
)
|
|
20
|
-
);
|
|
21
|
-
}
|
|
4
|
+
export type { MarkdownProps } from './Markdown.factory';
|
|
22
5
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
export default function Markdown(props: MarkdownProps) {
|
|
26
|
-
const { prefixCls } = props;
|
|
27
|
-
const { markdown = '', highlightEnable, dispatch } = useContext(EditorContext);
|
|
28
|
-
const preRef = React.createRef<HTMLPreElement>();
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (preRef.current && dispatch) {
|
|
31
|
-
dispatch({ textareaPre: preRef.current });
|
|
32
|
-
}
|
|
33
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
-
}, []);
|
|
35
|
-
if (!markdown) {
|
|
36
|
-
return <pre ref={preRef} className={`${prefixCls}-text-pre wmde-markdown-color`} />;
|
|
37
|
-
}
|
|
38
|
-
let mdStr = `<pre class="language-markdown ${prefixCls}-text-pre wmde-markdown-color"><code class="language-markdown">${html2Escape(
|
|
39
|
-
String.raw`${markdown}`,
|
|
40
|
-
)}\n</code></pre>`;
|
|
41
|
-
|
|
42
|
-
if (highlightEnable) {
|
|
43
|
-
try {
|
|
44
|
-
mdStr = rehype()
|
|
45
|
-
.data('settings', { fragment: true })
|
|
46
|
-
// https://github.com/uiwjs/react-md-editor/issues/593
|
|
47
|
-
// @ts-ignore
|
|
48
|
-
.use(rehypePrism, { ignoreMissing: true })
|
|
49
|
-
.processSync(mdStr)
|
|
50
|
-
.toString();
|
|
51
|
-
} catch (error) {}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return React.createElement('div', {
|
|
55
|
-
className: 'wmde-markdown-color',
|
|
56
|
-
dangerouslySetInnerHTML: { __html: mdStr || '' },
|
|
57
|
-
});
|
|
58
|
-
}
|
|
6
|
+
export default createMarkdown(rehypePrism);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const FENCE_OPEN = /^```/;
|
|
2
|
+
const FENCE_CLOSE = /^```\r?\n?$/;
|
|
3
|
+
const BLANK_LINE = /^[ \t]*\r?\n?$/;
|
|
4
|
+
const FRONT_MATTER_FENCE = /^---\r?\n?$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Splits markdown into blocks that Prism's markdown grammar highlights the same way
|
|
8
|
+
* on their own as it does in the full document, so unchanged blocks can be cached.
|
|
9
|
+
* Joining the returned blocks always yields the original string.
|
|
10
|
+
*
|
|
11
|
+
* A block ends where a run of blank lines starts, so each block keeps its leading blank
|
|
12
|
+
* lines (indented code needs them). No split happens inside a ``` fence, front matter or
|
|
13
|
+
* multiline markup or link references, or right before a `---` line, which Prism
|
|
14
|
+
* would read as front matter. Whitespace-only lines stay in their block because
|
|
15
|
+
* Prism's inline emphasis can span them.
|
|
16
|
+
* Fences follow Prism's rule, not CommonMark's, so the split matches what Prism sees.
|
|
17
|
+
*/
|
|
18
|
+
export function splitBlocks(markdown: string): string[] {
|
|
19
|
+
const lines: string[] = markdown.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
20
|
+
const blocks: string[] = [];
|
|
21
|
+
let current = '';
|
|
22
|
+
let pendingSplit = -1;
|
|
23
|
+
let inFence = false;
|
|
24
|
+
let inFrontMatter = /^(?:\s*[\r\n])?---(?:\r?\n|$)/.test(markdown);
|
|
25
|
+
let frontMatterStarted = false;
|
|
26
|
+
const syntax = { end: '', quote: '', brackets: 0, subset: 0 };
|
|
27
|
+
let previousIsBlank = true;
|
|
28
|
+
|
|
29
|
+
lines.forEach((line) => {
|
|
30
|
+
const isBlank = BLANK_LINE.test(line);
|
|
31
|
+
const canSplit = !inFence && !inFrontMatter && !syntax.end && !syntax.brackets;
|
|
32
|
+
if (/^\r?\n$/.test(line) && !previousIsBlank && canSplit) {
|
|
33
|
+
pendingSplit = current.length;
|
|
34
|
+
}
|
|
35
|
+
if (!isBlank && pendingSplit > 0) {
|
|
36
|
+
if (!FRONT_MATTER_FENCE.test(line)) {
|
|
37
|
+
blocks.push(current.slice(0, pendingSplit));
|
|
38
|
+
current = current.slice(pendingSplit);
|
|
39
|
+
}
|
|
40
|
+
pendingSplit = -1;
|
|
41
|
+
}
|
|
42
|
+
current += line;
|
|
43
|
+
previousIsBlank = isBlank;
|
|
44
|
+
|
|
45
|
+
if (inFrontMatter) {
|
|
46
|
+
if (FRONT_MATTER_FENCE.test(line)) {
|
|
47
|
+
inFrontMatter = !frontMatterStarted;
|
|
48
|
+
frontMatterStarted = true;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (inFence) {
|
|
53
|
+
inFence = !FENCE_CLOSE.test(line);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!syntax.end && FENCE_OPEN.test(line)) {
|
|
57
|
+
inFence = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
updateSyntax(line, syntax);
|
|
61
|
+
});
|
|
62
|
+
if (current) {
|
|
63
|
+
blocks.push(current);
|
|
64
|
+
}
|
|
65
|
+
return blocks;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Keep potential multiline Prism tokens together. Unclosed syntax conservatively
|
|
69
|
+
// keeps the rest of the document in one block until the user closes it.
|
|
70
|
+
function updateSyntax(line: string, state: { end: string; quote: string; brackets: number; subset: number }) {
|
|
71
|
+
let reference = false;
|
|
72
|
+
for (let i = 0; i < line.length; i++) {
|
|
73
|
+
const char = line[i];
|
|
74
|
+
if (state.end && state.end !== '>') {
|
|
75
|
+
const end = line.indexOf(state.end, i);
|
|
76
|
+
if (end === -1) return;
|
|
77
|
+
i = end + state.end.length - 1;
|
|
78
|
+
state.end = '';
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (state.end === '>') {
|
|
82
|
+
if (state.quote) {
|
|
83
|
+
if (char === state.quote) state.quote = '';
|
|
84
|
+
} else if (char === '"' || char === "'") {
|
|
85
|
+
state.quote = char;
|
|
86
|
+
} else if (char === '[') {
|
|
87
|
+
state.subset++;
|
|
88
|
+
} else if (char === ']' && state.subset) {
|
|
89
|
+
state.subset--;
|
|
90
|
+
} else if (char === '>' && !state.subset) {
|
|
91
|
+
state.end = '';
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === '\\') {
|
|
96
|
+
i++;
|
|
97
|
+
} else if (char === '<' && /[a-z!?/]/i.test(line[i + 1] || '')) {
|
|
98
|
+
state.end = line.startsWith('<!--', i)
|
|
99
|
+
? '-->'
|
|
100
|
+
: line.startsWith('<![CDATA[', i)
|
|
101
|
+
? ']]>'
|
|
102
|
+
: line.startsWith('<?', i)
|
|
103
|
+
? '?>'
|
|
104
|
+
: '>';
|
|
105
|
+
} else if (char === '[') {
|
|
106
|
+
state.brackets++;
|
|
107
|
+
} else if (char === ']') {
|
|
108
|
+
state.brackets = Math.max(0, state.brackets - 1);
|
|
109
|
+
reference = line[i + 1] === ':';
|
|
110
|
+
} else if (reference && (char === '"' || char === "'" || char === '(')) {
|
|
111
|
+
state.end = char === '(' ? ')' : char;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|