@uiw/react-md-editor 4.1.6 → 4.2.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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/mdeditor.css +5 -0
  3. package/dist/mdeditor.js +2378 -1029
  4. package/dist/mdeditor.min.css +1 -1
  5. package/dist/mdeditor.min.js +1 -1
  6. package/esm/Editor.factory.js +70 -17
  7. package/esm/components/TextArea/Markdown.common.d.ts +3 -5
  8. package/esm/components/TextArea/Markdown.common.js +2 -56
  9. package/esm/components/TextArea/Markdown.d.ts +3 -5
  10. package/esm/components/TextArea/Markdown.factory.d.ts +6 -0
  11. package/esm/components/TextArea/Markdown.factory.js +138 -0
  12. package/esm/components/TextArea/Markdown.js +2 -63
  13. package/esm/components/TextArea/index.css +5 -0
  14. package/esm/components/TextArea/index.less +5 -0
  15. package/esm/components/TextArea/splitBlocks.d.ts +13 -0
  16. package/esm/components/TextArea/splitBlocks.js +111 -0
  17. package/esm/package.json +1 -0
  18. package/lib/Editor.factory.js +78 -18
  19. package/lib/components/TextArea/Markdown.common.d.ts +3 -5
  20. package/lib/components/TextArea/Markdown.common.js +3 -60
  21. package/lib/components/TextArea/Markdown.d.ts +3 -5
  22. package/lib/components/TextArea/Markdown.factory.d.ts +6 -0
  23. package/lib/components/TextArea/Markdown.factory.js +148 -0
  24. package/lib/components/TextArea/Markdown.js +3 -67
  25. package/lib/components/TextArea/index.less +5 -0
  26. package/lib/components/TextArea/splitBlocks.d.ts +13 -0
  27. package/lib/components/TextArea/splitBlocks.js +117 -0
  28. package/markdown-editor.css +5 -0
  29. package/package.json +18 -12
  30. package/src/Editor.factory.tsx +64 -12
  31. package/src/components/TextArea/Markdown.common.tsx +3 -46
  32. package/src/components/TextArea/Markdown.factory.tsx +132 -0
  33. package/src/components/TextArea/Markdown.tsx +3 -55
  34. package/src/components/TextArea/index.less +5 -0
  35. 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.6",
3
+ "version": "4.2.0",
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
- "require": "./lib/index.js"
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
- "require": "./lib/index.common.js"
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
- "require": "./lib/index.nohighlight.js"
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
- "require": "./lib/commands/index.js"
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
- "require": "./lib/commands/index.cn.js"
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
  }
@@ -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,
@@ -184,13 +232,15 @@ export function createMDEditor<
184
232
  initScroll.current = true;
185
233
  }
186
234
  if (textareaDom && previewDom) {
187
- const scale =
188
- (textareaDom.scrollHeight - textareaDom.offsetHeight) / (previewDom.scrollHeight - previewDom.offsetHeight);
189
- if (e.target === textareaDom && active.current === 'text') {
190
- previewDom.scrollTop = textareaDom.scrollTop / scale;
191
- }
192
- if (e.target === previewDom && active.current === 'preview') {
193
- textareaDom.scrollTop = previewDom.scrollTop * scale;
235
+ const inputRange = Math.max(0, textareaDom.scrollHeight - textareaDom.offsetHeight);
236
+ const previewRange = Math.max(0, previewDom.scrollHeight - previewDom.offsetHeight);
237
+ if (inputRange > 0 && previewRange > 0) {
238
+ if (e.target === textareaDom && active.current === 'text') {
239
+ previewDom.scrollTop = (textareaDom.scrollTop / inputRange) * previewRange;
240
+ }
241
+ if (e.target === previewDom && active.current === 'preview') {
242
+ textareaDom.scrollTop = (previewDom.scrollTop / previewRange) * inputRange;
243
+ }
194
244
  }
195
245
  let scrollTop = 0;
196
246
  if (active.current === 'text') {
@@ -204,13 +254,15 @@ export function createMDEditor<
204
254
 
205
255
  const previewClassName = `${prefixCls}-preview ${previewOptions.className || ''}`;
206
256
  const handlePreviewScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => handleScroll(e, 'preview');
257
+ const previewRenderMs = useRef(0);
258
+ const previewMarkdown = useDeferredValue(usePreviewSource(state.markdown || '', previewRenderMs));
207
259
  let mdPreview = useMemo(
208
260
  () => (
209
- <div ref={previewRef} className={previewClassName}>
210
- <PreviewComponent {...previewOptions} onScroll={handlePreviewScroll} source={state.markdown || ''} />
261
+ <div ref={previewRef} className={previewClassName} onScroll={handlePreviewScroll}>
262
+ <TimedPreview {...previewOptions} renderMsRef={previewRenderMs} source={previewMarkdown} />
211
263
  </div>
212
264
  ),
213
- [previewClassName, previewOptions, state.markdown],
265
+ [previewClassName, previewOptions, previewMarkdown],
214
266
  );
215
267
  const preview = components?.preview && components?.preview(state.markdown || '', state, dispatch);
216
268
  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 { type IProps } from '../../Types';
5
- import { EditorContext } from '../../Context';
2
+ import { createMarkdown } from './Markdown.factory';
6
3
 
7
- function html2Escape(sHtml: string) {
8
- return sHtml.replace(
9
- /[<&"]/g,
10
- (c: string) => (({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }) as Record<string, string>)[c],
11
- );
12
- }
4
+ export type { MarkdownProps } from './Markdown.factory';
13
5
 
14
- export interface MarkdownProps extends IProps, React.HTMLAttributes<HTMLPreElement> {}
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) => (({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }) 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 { type IProps } from '../../Types';
5
- import { EditorContext } from '../../Context';
2
+ import { createMarkdown } from './Markdown.factory';
6
3
 
7
- function html2Escape(sHtml: string) {
8
- return (
9
- sHtml
10
- // .replace(/```(\w+)?([\s\S]*?)(\s.+)?```/g, (str: string) => {
11
- // return str.replace(
12
- // /[<&"]/g,
13
- // (c: string) => (({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' } as Record<string, string>)[c]),
14
- // );
15
- // })
16
- .replace(
17
- /[<&"]/g,
18
- (c: string) => (({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }) as Record<string, string>)[c],
19
- )
20
- );
21
- }
4
+ export type { MarkdownProps } from './Markdown.factory';
22
5
 
23
- export interface MarkdownProps extends IProps, React.HTMLAttributes<HTMLPreElement> {}
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);
@@ -119,6 +119,11 @@
119
119
  .token.bold {
120
120
  font-weight: unset !important;
121
121
  }
122
+ .token.list,
123
+ .token.table {
124
+ display: inline;
125
+ font-size: inherit;
126
+ }
122
127
  .token.title {
123
128
  line-height: unset !important;
124
129
  font-size: unset !important;
@@ -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
+ }