testomatio-editor-blocks 0.4.53 → 0.4.54
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/package/editor/blocks/markdown.js +97 -36
- package/package/editor/blocks/stepField.js +9 -6
- package/package/editor/blocks/useAutoResize.d.ts +3 -4
- package/package/editor/blocks/useAutoResize.js +57 -45
- package/package/editor/customMarkdownConverter.js +66 -26
- package/package.json +1 -1
- package/src/editor/blocks/markdown.ts +110 -40
- package/src/editor/blocks/stepField.tsx +9 -7
- package/src/editor/blocks/useAutoResize.ts +60 -50
- package/src/editor/customMarkdownConverter.ts +80 -27
- package/src/editor/markdownToBlocks.test.ts +39 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+?)(?:\s+=\d+x(?:\d+|\*))?\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
-
const INLINE_SEGMENT_REGEX = /(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
4
3
|
export function escapeHtml(text) {
|
|
5
4
|
return text
|
|
6
5
|
.replace(/&/g, "&")
|
|
@@ -12,46 +11,108 @@ export function escapeHtml(text) {
|
|
|
12
11
|
function restoreEscapes(text) {
|
|
13
12
|
return text.replace(/\uE000/g, "\\");
|
|
14
13
|
}
|
|
14
|
+
function findItalicClose(text, start, marker) {
|
|
15
|
+
let j = start;
|
|
16
|
+
while (j < text.length) {
|
|
17
|
+
const ch = text[j];
|
|
18
|
+
if (ch === "\uE000") {
|
|
19
|
+
j += 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
23
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
24
|
+
if (close === -1) {
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
27
|
+
j = close + 2;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === marker) {
|
|
31
|
+
return j;
|
|
32
|
+
}
|
|
33
|
+
j += 1;
|
|
34
|
+
}
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
function parseInlineSegments(normalized, outer) {
|
|
38
|
+
const result = [];
|
|
39
|
+
let buffer = "";
|
|
40
|
+
const pushPlain = () => {
|
|
41
|
+
if (!buffer)
|
|
42
|
+
return;
|
|
43
|
+
result.push({ text: restoreEscapes(buffer), styles: { ...outer } });
|
|
44
|
+
buffer = "";
|
|
45
|
+
};
|
|
46
|
+
const wrap = (inner, add) => {
|
|
47
|
+
pushPlain();
|
|
48
|
+
result.push(...parseInlineSegments(inner, { ...outer, ...add }));
|
|
49
|
+
};
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < normalized.length) {
|
|
52
|
+
if (normalized.startsWith("***", i)) {
|
|
53
|
+
const end = normalized.indexOf("***", i + 3);
|
|
54
|
+
if (end !== -1) {
|
|
55
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
56
|
+
i = end + 3;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (normalized.startsWith("___", i)) {
|
|
61
|
+
const end = normalized.indexOf("___", i + 3);
|
|
62
|
+
if (end !== -1) {
|
|
63
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
64
|
+
i = end + 3;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (normalized.startsWith("**", i)) {
|
|
69
|
+
const end = normalized.indexOf("**", i + 2);
|
|
70
|
+
if (end !== -1) {
|
|
71
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
72
|
+
i = end + 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (normalized.startsWith("__", i)) {
|
|
77
|
+
const end = normalized.indexOf("__", i + 2);
|
|
78
|
+
if (end !== -1) {
|
|
79
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
80
|
+
i = end + 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (normalized.startsWith("<u>", i)) {
|
|
85
|
+
const end = normalized.indexOf("</u>", i + 3);
|
|
86
|
+
if (end !== -1) {
|
|
87
|
+
wrap(normalized.slice(i + 3, end), { underline: true });
|
|
88
|
+
i = end + 4;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (normalized[i] === "*" || normalized[i] === "_") {
|
|
93
|
+
const marker = normalized[i];
|
|
94
|
+
const end = findItalicClose(normalized, i + 1, marker);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
wrap(normalized.slice(i + 1, end), { italic: true });
|
|
97
|
+
i = end + 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
buffer += normalized[i];
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
pushPlain();
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
15
107
|
function parseInlineMarkdown(text) {
|
|
16
108
|
if (!text) {
|
|
17
109
|
return [];
|
|
18
110
|
}
|
|
19
111
|
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const content = segment.slice(3, -3);
|
|
25
|
-
return {
|
|
26
|
-
text: restoreEscapes(content),
|
|
27
|
-
styles: { bold: true, italic: true, underline: false },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
|
|
31
|
-
const content = segment.slice(2, -2);
|
|
32
|
-
return {
|
|
33
|
-
text: restoreEscapes(content),
|
|
34
|
-
styles: { ...baseStyles, bold: true },
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
if (/^\*(.+)\*$/.test(segment) || /^_(.+)_$/.test(segment)) {
|
|
38
|
-
const content = segment.slice(1, -1);
|
|
39
|
-
return {
|
|
40
|
-
text: restoreEscapes(content),
|
|
41
|
-
styles: { ...baseStyles, italic: true },
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (/^<u>(.+)<\/u>$/.test(segment)) {
|
|
45
|
-
const content = segment.slice(3, -4);
|
|
46
|
-
return {
|
|
47
|
-
text: restoreEscapes(content),
|
|
48
|
-
styles: { ...baseStyles, underline: true },
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
text: restoreEscapes(segment),
|
|
53
|
-
styles: { ...baseStyles },
|
|
54
|
-
};
|
|
112
|
+
return parseInlineSegments(normalized, {
|
|
113
|
+
bold: false,
|
|
114
|
+
italic: false,
|
|
115
|
+
underline: false,
|
|
55
116
|
});
|
|
56
117
|
}
|
|
57
118
|
function inlineToHtml(inline) {
|
|
@@ -753,6 +753,15 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
753
753
|
}
|
|
754
754
|
textareaNode.focus();
|
|
755
755
|
}, [focusSignal, textareaNode]);
|
|
756
|
+
useAutoResize({
|
|
757
|
+
textarea: textareaNode,
|
|
758
|
+
enabled: multiline,
|
|
759
|
+
onResize: useCallback(() => {
|
|
760
|
+
var _a;
|
|
761
|
+
const instance = editorInstanceRef.current;
|
|
762
|
+
(_a = instance === null || instance === void 0 ? void 0 : instance._updateAutoHeight) === null || _a === void 0 ? void 0 : _a.call(instance);
|
|
763
|
+
}, []),
|
|
764
|
+
});
|
|
756
765
|
useEffect(() => {
|
|
757
766
|
var _a;
|
|
758
767
|
const instance = editorInstanceRef.current;
|
|
@@ -814,12 +823,6 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
814
823
|
}
|
|
815
824
|
textareaNode.readOnly = readOnly;
|
|
816
825
|
}, [readOnly, textareaNode]);
|
|
817
|
-
useAutoResize({
|
|
818
|
-
textarea: textareaNode,
|
|
819
|
-
multiline,
|
|
820
|
-
minRows: 3,
|
|
821
|
-
maxRows: 16,
|
|
822
|
-
});
|
|
823
826
|
useEffect(() => {
|
|
824
827
|
if (!textareaNode) {
|
|
825
828
|
return;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
type Options = {
|
|
2
2
|
textarea: HTMLTextAreaElement | null;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
maxRows?: number;
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
onResize: () => void;
|
|
6
5
|
};
|
|
7
|
-
export declare function useAutoResize({ textarea,
|
|
6
|
+
export declare function useAutoResize({ textarea, enabled, onResize }: Options): () => void;
|
|
8
7
|
export {};
|
|
@@ -1,65 +1,77 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
// Shared across all hook instances: one fonts.ready promise per document,
|
|
3
|
+
// fanned out to every registered callback so N fields cost 1 .then().
|
|
4
|
+
const fontsReadyCallbacks = new Set();
|
|
5
|
+
let fontsReadyAttached = false;
|
|
6
|
+
function registerFontsReady(cb) {
|
|
7
|
+
var _a;
|
|
8
|
+
if (typeof document === "undefined" || !((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)) {
|
|
9
|
+
return () => { };
|
|
10
|
+
}
|
|
11
|
+
fontsReadyCallbacks.add(cb);
|
|
12
|
+
if (!fontsReadyAttached) {
|
|
13
|
+
fontsReadyAttached = true;
|
|
14
|
+
document.fonts.ready
|
|
15
|
+
.then(() => {
|
|
16
|
+
for (const fn of fontsReadyCallbacks)
|
|
17
|
+
fn();
|
|
18
|
+
})
|
|
19
|
+
.catch(() => { });
|
|
20
|
+
}
|
|
21
|
+
return () => {
|
|
22
|
+
fontsReadyCallbacks.delete(cb);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function useAutoResize({ textarea, enabled = true, onResize }) {
|
|
3
26
|
const frameRef = useRef(0);
|
|
27
|
+
const onResizeRef = useRef(onResize);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
onResizeRef.current = onResize;
|
|
30
|
+
}, [onResize]);
|
|
4
31
|
useEffect(() => {
|
|
5
|
-
|
|
6
|
-
if (!textarea || !multiline) {
|
|
32
|
+
if (!textarea || !enabled)
|
|
7
33
|
return;
|
|
8
|
-
}
|
|
9
|
-
const resize = () => {
|
|
10
|
-
textarea.style.height = "auto";
|
|
11
|
-
const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight || "16");
|
|
12
|
-
const minHeight = lineHeight * minRows;
|
|
13
|
-
const maxHeight = lineHeight * maxRows;
|
|
14
|
-
const clampedHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
|
|
15
|
-
textarea.style.height = `${clampedHeight}px`;
|
|
16
|
-
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
17
|
-
};
|
|
18
|
-
const mutationObserver = new MutationObserver(resize);
|
|
19
|
-
mutationObserver.observe(textarea, { childList: true, characterData: true, subtree: true });
|
|
20
|
-
const resizeObserver = new ResizeObserver(resize);
|
|
21
|
-
resizeObserver.observe(textarea);
|
|
22
|
-
const handleInput = () => {
|
|
23
|
-
var _a;
|
|
24
|
-
cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
|
|
25
|
-
frameRef.current = requestAnimationFrame(resize);
|
|
26
|
-
};
|
|
27
|
-
textarea.addEventListener("input", handleInput);
|
|
28
34
|
let cancelled = false;
|
|
29
|
-
|
|
35
|
+
// All callers go through this coalescing scheduler: repeated pings in a
|
|
36
|
+
// single frame collapse to one reflow.
|
|
37
|
+
const schedule = () => {
|
|
38
|
+
if (cancelled)
|
|
39
|
+
return;
|
|
40
|
+
if (frameRef.current)
|
|
41
|
+
return;
|
|
30
42
|
frameRef.current = requestAnimationFrame(() => {
|
|
43
|
+
frameRef.current = 0;
|
|
31
44
|
if (!cancelled)
|
|
32
|
-
|
|
45
|
+
onResizeRef.current();
|
|
33
46
|
});
|
|
34
|
-
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
47
|
+
};
|
|
48
|
+
// Initial pass after layout.
|
|
49
|
+
schedule();
|
|
50
|
+
// One-shot: re-run once the textarea actually enters the layout tree.
|
|
51
|
+
// This is the piece that fixes the drag-drop remount and
|
|
52
|
+
// snippet-insert-while-hidden cases OverType itself cannot recover from.
|
|
38
53
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
39
54
|
for (const entry of entries) {
|
|
40
|
-
if (entry.isIntersecting
|
|
41
|
-
|
|
55
|
+
if (entry.isIntersecting) {
|
|
56
|
+
schedule();
|
|
42
57
|
intersectionObserver.disconnect();
|
|
43
58
|
break;
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
61
|
});
|
|
47
62
|
intersectionObserver.observe(textarea);
|
|
48
|
-
|
|
49
|
-
document.fonts.ready.then(() => {
|
|
50
|
-
if (!cancelled)
|
|
51
|
-
resize();
|
|
52
|
-
}).catch(() => { });
|
|
53
|
-
}
|
|
63
|
+
const unregisterFonts = registerFontsReady(schedule);
|
|
54
64
|
return () => {
|
|
55
|
-
var _a;
|
|
56
65
|
cancelled = true;
|
|
57
|
-
mutationObserver.disconnect();
|
|
58
|
-
resizeObserver.disconnect();
|
|
59
66
|
intersectionObserver.disconnect();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
unregisterFonts();
|
|
68
|
+
if (frameRef.current) {
|
|
69
|
+
cancelAnimationFrame(frameRef.current);
|
|
70
|
+
frameRef.current = 0;
|
|
71
|
+
}
|
|
63
72
|
};
|
|
64
|
-
}, [textarea,
|
|
73
|
+
}, [textarea, enabled]);
|
|
74
|
+
return useCallback(() => {
|
|
75
|
+
onResizeRef.current();
|
|
76
|
+
}, []);
|
|
65
77
|
}
|
|
@@ -521,28 +521,80 @@ export function blocksToMarkdown(blocks) {
|
|
|
521
521
|
return cleaned;
|
|
522
522
|
}
|
|
523
523
|
function parseInlineMarkdown(text) {
|
|
524
|
-
|
|
524
|
+
return parseInlineSegments(stripHtmlWrappers(text), {});
|
|
525
|
+
}
|
|
526
|
+
function findItalicClose(text, start, marker) {
|
|
527
|
+
let j = start;
|
|
528
|
+
while (j < text.length) {
|
|
529
|
+
const ch = text[j];
|
|
530
|
+
if (ch === "\\") {
|
|
531
|
+
j += 2;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
535
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
536
|
+
if (close === -1) {
|
|
537
|
+
return -1;
|
|
538
|
+
}
|
|
539
|
+
j = close + 2;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (ch === marker) {
|
|
543
|
+
return j;
|
|
544
|
+
}
|
|
545
|
+
j += 1;
|
|
546
|
+
}
|
|
547
|
+
return -1;
|
|
548
|
+
}
|
|
549
|
+
function parseInlineSegments(cleaned, outerStyles) {
|
|
525
550
|
const result = [];
|
|
526
551
|
let buffer = "";
|
|
527
552
|
const pushPlain = () => {
|
|
528
553
|
if (buffer.length === 0) {
|
|
529
554
|
return;
|
|
530
555
|
}
|
|
531
|
-
result.push({
|
|
556
|
+
result.push({
|
|
557
|
+
type: "text",
|
|
558
|
+
text: unescapeMarkdown(buffer),
|
|
559
|
+
styles: { ...outerStyles },
|
|
560
|
+
});
|
|
532
561
|
buffer = "";
|
|
533
562
|
};
|
|
563
|
+
const wrap = (inner, add) => {
|
|
564
|
+
pushPlain();
|
|
565
|
+
const nested = parseInlineSegments(inner, { ...outerStyles, ...add });
|
|
566
|
+
result.push(...nested);
|
|
567
|
+
};
|
|
534
568
|
let i = 0;
|
|
535
569
|
while (i < cleaned.length) {
|
|
570
|
+
if (cleaned.startsWith("***", i)) {
|
|
571
|
+
const end = cleaned.indexOf("***", i + 3);
|
|
572
|
+
if (end !== -1) {
|
|
573
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
574
|
+
i = end + 3;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (cleaned.startsWith("___", i)) {
|
|
579
|
+
const end = cleaned.indexOf("___", i + 3);
|
|
580
|
+
if (end !== -1) {
|
|
581
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
582
|
+
i = end + 3;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
536
586
|
if (cleaned.startsWith("**", i)) {
|
|
537
587
|
const end = cleaned.indexOf("**", i + 2);
|
|
538
588
|
if (end !== -1) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
589
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
590
|
+
i = end + 2;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (cleaned.startsWith("__", i)) {
|
|
595
|
+
const end = cleaned.indexOf("__", i + 2);
|
|
596
|
+
if (end !== -1) {
|
|
597
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
546
598
|
i = end + 2;
|
|
547
599
|
continue;
|
|
548
600
|
}
|
|
@@ -550,13 +602,7 @@ function parseInlineMarkdown(text) {
|
|
|
550
602
|
if (cleaned.startsWith("~~", i)) {
|
|
551
603
|
const end = cleaned.indexOf("~~", i + 2);
|
|
552
604
|
if (end !== -1) {
|
|
553
|
-
|
|
554
|
-
const inner = cleaned.slice(i + 2, end);
|
|
555
|
-
result.push({
|
|
556
|
-
type: "text",
|
|
557
|
-
text: unescapeMarkdown(inner),
|
|
558
|
-
styles: { strike: true },
|
|
559
|
-
});
|
|
605
|
+
wrap(cleaned.slice(i + 2, end), { strike: true });
|
|
560
606
|
i = end + 2;
|
|
561
607
|
continue;
|
|
562
608
|
}
|
|
@@ -569,7 +615,7 @@ function parseInlineMarkdown(text) {
|
|
|
569
615
|
result.push({
|
|
570
616
|
type: "text",
|
|
571
617
|
text: unescapeMarkdown(inner),
|
|
572
|
-
styles: { code: true },
|
|
618
|
+
styles: { ...outerStyles, code: true },
|
|
573
619
|
});
|
|
574
620
|
i = end + 1;
|
|
575
621
|
continue;
|
|
@@ -583,7 +629,7 @@ function parseInlineMarkdown(text) {
|
|
|
583
629
|
pushPlain();
|
|
584
630
|
const label = cleaned.slice(i + 1, endLabel);
|
|
585
631
|
const href = cleaned.slice(startLink + 1, endLink);
|
|
586
|
-
const parsedLabel =
|
|
632
|
+
const parsedLabel = parseInlineSegments(label, {});
|
|
587
633
|
// Ensure link content is never undefined - if empty, add empty text
|
|
588
634
|
const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
|
|
589
635
|
result.push({
|
|
@@ -597,15 +643,9 @@ function parseInlineMarkdown(text) {
|
|
|
597
643
|
}
|
|
598
644
|
if (cleaned[i] === "*" || cleaned[i] === "_") {
|
|
599
645
|
const marker = cleaned[i];
|
|
600
|
-
const end = cleaned
|
|
646
|
+
const end = findItalicClose(cleaned, i + 1, marker);
|
|
601
647
|
if (end !== -1) {
|
|
602
|
-
|
|
603
|
-
const inner = cleaned.slice(i + 1, end);
|
|
604
|
-
result.push({
|
|
605
|
-
type: "text",
|
|
606
|
-
text: unescapeMarkdown(inner),
|
|
607
|
-
styles: { italic: true },
|
|
608
|
-
});
|
|
648
|
+
wrap(cleaned.slice(i + 1, end), { italic: true });
|
|
609
649
|
i = end + 1;
|
|
610
650
|
continue;
|
|
611
651
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+?)(?:\s+=\d+x(?:\d+|\*))?\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
-
const INLINE_SEGMENT_REGEX =
|
|
4
|
-
/(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
5
3
|
|
|
6
4
|
export function escapeHtml(text: string): string {
|
|
7
5
|
return text
|
|
@@ -25,53 +23,125 @@ function restoreEscapes(text: string): string {
|
|
|
25
23
|
return text.replace(/\uE000/g, "\\");
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
function findItalicClose(
|
|
27
|
+
text: string,
|
|
28
|
+
start: number,
|
|
29
|
+
marker: "*" | "_",
|
|
30
|
+
): number {
|
|
31
|
+
let j = start;
|
|
32
|
+
while (j < text.length) {
|
|
33
|
+
const ch = text[j];
|
|
34
|
+
if (ch === "\uE000") {
|
|
35
|
+
j += 2;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
39
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
40
|
+
if (close === -1) {
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
j = close + 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === marker) {
|
|
47
|
+
return j;
|
|
48
|
+
}
|
|
49
|
+
j += 1;
|
|
31
50
|
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
32
53
|
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
function parseInlineSegments(
|
|
55
|
+
normalized: string,
|
|
56
|
+
outer: { bold: boolean; italic: boolean; underline: boolean },
|
|
57
|
+
): InlineSegment[] {
|
|
58
|
+
const result: InlineSegment[] = [];
|
|
59
|
+
let buffer = "";
|
|
60
|
+
|
|
61
|
+
const pushPlain = () => {
|
|
62
|
+
if (!buffer) return;
|
|
63
|
+
result.push({ text: restoreEscapes(buffer), styles: { ...outer } });
|
|
64
|
+
buffer = "";
|
|
65
|
+
};
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
const wrap = (
|
|
68
|
+
inner: string,
|
|
69
|
+
add: Partial<{ bold: boolean; italic: boolean; underline: boolean }>,
|
|
70
|
+
) => {
|
|
71
|
+
pushPlain();
|
|
72
|
+
result.push(...parseInlineSegments(inner, { ...outer, ...add }));
|
|
73
|
+
};
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
let i = 0;
|
|
76
|
+
while (i < normalized.length) {
|
|
77
|
+
if (normalized.startsWith("***", i)) {
|
|
78
|
+
const end = normalized.indexOf("***", i + 3);
|
|
79
|
+
if (end !== -1) {
|
|
80
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
81
|
+
i = end + 3;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
45
84
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
85
|
+
if (normalized.startsWith("___", i)) {
|
|
86
|
+
const end = normalized.indexOf("___", i + 3);
|
|
87
|
+
if (end !== -1) {
|
|
88
|
+
wrap(normalized.slice(i + 3, end), { bold: true, italic: true });
|
|
89
|
+
i = end + 3;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
53
92
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
93
|
+
if (normalized.startsWith("**", i)) {
|
|
94
|
+
const end = normalized.indexOf("**", i + 2);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
97
|
+
i = end + 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
61
100
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
101
|
+
if (normalized.startsWith("__", i)) {
|
|
102
|
+
const end = normalized.indexOf("__", i + 2);
|
|
103
|
+
if (end !== -1) {
|
|
104
|
+
wrap(normalized.slice(i + 2, end), { bold: true });
|
|
105
|
+
i = end + 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (normalized.startsWith("<u>", i)) {
|
|
110
|
+
const end = normalized.indexOf("</u>", i + 3);
|
|
111
|
+
if (end !== -1) {
|
|
112
|
+
wrap(normalized.slice(i + 3, end), { underline: true });
|
|
113
|
+
i = end + 4;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
69
116
|
}
|
|
117
|
+
if (normalized[i] === "*" || normalized[i] === "_") {
|
|
118
|
+
const marker = normalized[i] as "*" | "_";
|
|
119
|
+
const end = findItalicClose(normalized, i + 1, marker);
|
|
120
|
+
if (end !== -1) {
|
|
121
|
+
wrap(normalized.slice(i + 1, end), { italic: true });
|
|
122
|
+
i = end + 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
buffer += normalized[i];
|
|
128
|
+
i += 1;
|
|
129
|
+
}
|
|
70
130
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
131
|
+
pushPlain();
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseInlineMarkdown(text: string): InlineSegment[] {
|
|
136
|
+
if (!text) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
141
|
+
return parseInlineSegments(normalized, {
|
|
142
|
+
bold: false,
|
|
143
|
+
italic: false,
|
|
144
|
+
underline: false,
|
|
75
145
|
});
|
|
76
146
|
}
|
|
77
147
|
|
|
@@ -945,6 +945,15 @@ export function StepField({
|
|
|
945
945
|
textareaNode.focus();
|
|
946
946
|
}, [focusSignal, textareaNode]);
|
|
947
947
|
|
|
948
|
+
useAutoResize({
|
|
949
|
+
textarea: textareaNode,
|
|
950
|
+
enabled: multiline,
|
|
951
|
+
onResize: useCallback(() => {
|
|
952
|
+
const instance = editorInstanceRef.current as (OverTypeInstance & { _updateAutoHeight?: () => void }) | null;
|
|
953
|
+
instance?._updateAutoHeight?.();
|
|
954
|
+
}, []),
|
|
955
|
+
});
|
|
956
|
+
|
|
948
957
|
useEffect(() => {
|
|
949
958
|
const instance = editorInstanceRef.current;
|
|
950
959
|
if (!instance) {
|
|
@@ -1012,13 +1021,6 @@ export function StepField({
|
|
|
1012
1021
|
textareaNode.readOnly = readOnly;
|
|
1013
1022
|
}, [readOnly, textareaNode]);
|
|
1014
1023
|
|
|
1015
|
-
useAutoResize({
|
|
1016
|
-
textarea: textareaNode,
|
|
1017
|
-
multiline,
|
|
1018
|
-
minRows: 3,
|
|
1019
|
-
maxRows: 16,
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
1024
|
useEffect(() => {
|
|
1023
1025
|
if (!textareaNode) {
|
|
1024
1026
|
return;
|
|
@@ -1,58 +1,68 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
type Options = {
|
|
4
4
|
textarea: HTMLTextAreaElement | null;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
maxRows?: number;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
onResize: () => void;
|
|
8
7
|
};
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!textarea || !multiline) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
9
|
+
// Shared across all hook instances: one fonts.ready promise per document,
|
|
10
|
+
// fanned out to every registered callback so N fields cost 1 .then().
|
|
11
|
+
const fontsReadyCallbacks = new Set<() => void>();
|
|
12
|
+
let fontsReadyAttached = false;
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
function registerFontsReady(cb: () => void): () => void {
|
|
15
|
+
if (typeof document === "undefined" || !document.fonts?.ready) {
|
|
16
|
+
return () => {};
|
|
17
|
+
}
|
|
18
|
+
fontsReadyCallbacks.add(cb);
|
|
19
|
+
if (!fontsReadyAttached) {
|
|
20
|
+
fontsReadyAttached = true;
|
|
21
|
+
document.fonts.ready
|
|
22
|
+
.then(() => {
|
|
23
|
+
for (const fn of fontsReadyCallbacks) fn();
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {});
|
|
26
|
+
}
|
|
27
|
+
return () => {
|
|
28
|
+
fontsReadyCallbacks.delete(cb);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
export function useAutoResize({ textarea, enabled = true, onResize }: Options) {
|
|
33
|
+
const frameRef = useRef<number>(0);
|
|
34
|
+
const onResizeRef = useRef(onResize);
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
onResizeRef.current = onResize;
|
|
38
|
+
}, [onResize]);
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!textarea || !enabled) return;
|
|
41
42
|
|
|
42
43
|
let cancelled = false;
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
// All callers go through this coalescing scheduler: repeated pings in a
|
|
46
|
+
// single frame collapse to one reflow.
|
|
47
|
+
const schedule = () => {
|
|
48
|
+
if (cancelled) return;
|
|
49
|
+
if (frameRef.current) return;
|
|
44
50
|
frameRef.current = requestAnimationFrame(() => {
|
|
45
|
-
|
|
51
|
+
frameRef.current = 0;
|
|
52
|
+
if (!cancelled) onResizeRef.current();
|
|
46
53
|
});
|
|
47
|
-
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Initial pass after layout.
|
|
57
|
+
schedule();
|
|
48
58
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
59
|
+
// One-shot: re-run once the textarea actually enters the layout tree.
|
|
60
|
+
// This is the piece that fixes the drag-drop remount and
|
|
61
|
+
// snippet-insert-while-hidden cases OverType itself cannot recover from.
|
|
52
62
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
53
63
|
for (const entry of entries) {
|
|
54
|
-
if (entry.isIntersecting
|
|
55
|
-
|
|
64
|
+
if (entry.isIntersecting) {
|
|
65
|
+
schedule();
|
|
56
66
|
intersectionObserver.disconnect();
|
|
57
67
|
break;
|
|
58
68
|
}
|
|
@@ -60,20 +70,20 @@ export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRow
|
|
|
60
70
|
});
|
|
61
71
|
intersectionObserver.observe(textarea);
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
document.fonts.ready.then(() => {
|
|
65
|
-
if (!cancelled) resize();
|
|
66
|
-
}).catch(() => {});
|
|
67
|
-
}
|
|
73
|
+
const unregisterFonts = registerFontsReady(schedule);
|
|
68
74
|
|
|
69
75
|
return () => {
|
|
70
76
|
cancelled = true;
|
|
71
|
-
mutationObserver.disconnect();
|
|
72
|
-
resizeObserver.disconnect();
|
|
73
77
|
intersectionObserver.disconnect();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
unregisterFonts();
|
|
79
|
+
if (frameRef.current) {
|
|
80
|
+
cancelAnimationFrame(frameRef.current);
|
|
81
|
+
frameRef.current = 0;
|
|
82
|
+
}
|
|
77
83
|
};
|
|
78
|
-
}, [textarea,
|
|
84
|
+
}, [textarea, enabled]);
|
|
85
|
+
|
|
86
|
+
return useCallback(() => {
|
|
87
|
+
onResizeRef.current();
|
|
88
|
+
}, []);
|
|
79
89
|
}
|
|
@@ -630,7 +630,41 @@ export function blocksToMarkdown(blocks: CustomEditorBlock[]): string {
|
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
function parseInlineMarkdown(text: string): EditorInline[] {
|
|
633
|
-
|
|
633
|
+
return parseInlineSegments(stripHtmlWrappers(text), {});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function findItalicClose(
|
|
637
|
+
text: string,
|
|
638
|
+
start: number,
|
|
639
|
+
marker: "*" | "_",
|
|
640
|
+
): number {
|
|
641
|
+
let j = start;
|
|
642
|
+
while (j < text.length) {
|
|
643
|
+
const ch = text[j];
|
|
644
|
+
if (ch === "\\") {
|
|
645
|
+
j += 2;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if ((ch === "*" || ch === "_") && text[j + 1] === ch) {
|
|
649
|
+
const close = text.indexOf(ch + ch, j + 2);
|
|
650
|
+
if (close === -1) {
|
|
651
|
+
return -1;
|
|
652
|
+
}
|
|
653
|
+
j = close + 2;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (ch === marker) {
|
|
657
|
+
return j;
|
|
658
|
+
}
|
|
659
|
+
j += 1;
|
|
660
|
+
}
|
|
661
|
+
return -1;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function parseInlineSegments(
|
|
665
|
+
cleaned: string,
|
|
666
|
+
outerStyles: Record<string, boolean>,
|
|
667
|
+
): EditorInline[] {
|
|
634
668
|
const result: EditorInline[] = [];
|
|
635
669
|
let buffer = "";
|
|
636
670
|
|
|
@@ -638,22 +672,53 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
638
672
|
if (buffer.length === 0) {
|
|
639
673
|
return;
|
|
640
674
|
}
|
|
641
|
-
result.push({
|
|
675
|
+
result.push({
|
|
676
|
+
type: "text",
|
|
677
|
+
text: unescapeMarkdown(buffer),
|
|
678
|
+
styles: { ...outerStyles } as EditorStyles,
|
|
679
|
+
});
|
|
642
680
|
buffer = "";
|
|
643
681
|
};
|
|
644
682
|
|
|
683
|
+
const wrap = (inner: string, add: Record<string, boolean>) => {
|
|
684
|
+
pushPlain();
|
|
685
|
+
const nested = parseInlineSegments(inner, { ...outerStyles, ...add });
|
|
686
|
+
result.push(...nested);
|
|
687
|
+
};
|
|
688
|
+
|
|
645
689
|
let i = 0;
|
|
646
690
|
while (i < cleaned.length) {
|
|
691
|
+
if (cleaned.startsWith("***", i)) {
|
|
692
|
+
const end = cleaned.indexOf("***", i + 3);
|
|
693
|
+
if (end !== -1) {
|
|
694
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
695
|
+
i = end + 3;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (cleaned.startsWith("___", i)) {
|
|
701
|
+
const end = cleaned.indexOf("___", i + 3);
|
|
702
|
+
if (end !== -1) {
|
|
703
|
+
wrap(cleaned.slice(i + 3, end), { bold: true, italic: true });
|
|
704
|
+
i = end + 3;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
647
709
|
if (cleaned.startsWith("**", i)) {
|
|
648
710
|
const end = cleaned.indexOf("**", i + 2);
|
|
649
711
|
if (end !== -1) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
712
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
713
|
+
i = end + 2;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (cleaned.startsWith("__", i)) {
|
|
719
|
+
const end = cleaned.indexOf("__", i + 2);
|
|
720
|
+
if (end !== -1) {
|
|
721
|
+
wrap(cleaned.slice(i + 2, end), { bold: true });
|
|
657
722
|
i = end + 2;
|
|
658
723
|
continue;
|
|
659
724
|
}
|
|
@@ -662,13 +727,7 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
662
727
|
if (cleaned.startsWith("~~", i)) {
|
|
663
728
|
const end = cleaned.indexOf("~~", i + 2);
|
|
664
729
|
if (end !== -1) {
|
|
665
|
-
|
|
666
|
-
const inner = cleaned.slice(i + 2, end);
|
|
667
|
-
result.push({
|
|
668
|
-
type: "text",
|
|
669
|
-
text: unescapeMarkdown(inner),
|
|
670
|
-
styles: { strike: true },
|
|
671
|
-
});
|
|
730
|
+
wrap(cleaned.slice(i + 2, end), { strike: true });
|
|
672
731
|
i = end + 2;
|
|
673
732
|
continue;
|
|
674
733
|
}
|
|
@@ -682,7 +741,7 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
682
741
|
result.push({
|
|
683
742
|
type: "text",
|
|
684
743
|
text: unescapeMarkdown(inner),
|
|
685
|
-
styles: { code: true },
|
|
744
|
+
styles: { ...outerStyles, code: true } as EditorStyles,
|
|
686
745
|
});
|
|
687
746
|
i = end + 1;
|
|
688
747
|
continue;
|
|
@@ -697,7 +756,7 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
697
756
|
pushPlain();
|
|
698
757
|
const label = cleaned.slice(i + 1, endLabel);
|
|
699
758
|
const href = cleaned.slice(startLink + 1, endLink);
|
|
700
|
-
const parsedLabel =
|
|
759
|
+
const parsedLabel = parseInlineSegments(label, {});
|
|
701
760
|
// Ensure link content is never undefined - if empty, add empty text
|
|
702
761
|
const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
|
|
703
762
|
result.push({
|
|
@@ -711,16 +770,10 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
711
770
|
}
|
|
712
771
|
|
|
713
772
|
if (cleaned[i] === "*" || cleaned[i] === "_") {
|
|
714
|
-
const marker = cleaned[i];
|
|
715
|
-
const end = cleaned
|
|
773
|
+
const marker = cleaned[i] as "*" | "_";
|
|
774
|
+
const end = findItalicClose(cleaned, i + 1, marker);
|
|
716
775
|
if (end !== -1) {
|
|
717
|
-
|
|
718
|
-
const inner = cleaned.slice(i + 1, end);
|
|
719
|
-
result.push({
|
|
720
|
-
type: "text",
|
|
721
|
-
text: unescapeMarkdown(inner),
|
|
722
|
-
styles: { italic: true },
|
|
723
|
-
});
|
|
776
|
+
wrap(cleaned.slice(i + 1, end), { italic: true });
|
|
724
777
|
i = end + 1;
|
|
725
778
|
continue;
|
|
726
779
|
}
|
|
@@ -130,4 +130,43 @@ describe("markdownToBlocks", () => {
|
|
|
130
130
|
children: [],
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it("parses combined bold+italic using nested delimiters", () => {
|
|
135
|
+
const blocks = markdownToBlocks(
|
|
136
|
+
"The _**Username**_ and **_Password_** fields and ***both*** and ___both___.",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(blocks).toHaveLength(1);
|
|
140
|
+
expect(blocks[0]).toEqual({
|
|
141
|
+
type: "paragraph",
|
|
142
|
+
props: baseProps,
|
|
143
|
+
content: [
|
|
144
|
+
{ type: "text", text: "The ", styles: {} },
|
|
145
|
+
{ type: "text", text: "Username", styles: { italic: true, bold: true } },
|
|
146
|
+
{ type: "text", text: " and ", styles: {} },
|
|
147
|
+
{ type: "text", text: "Password", styles: { bold: true, italic: true } },
|
|
148
|
+
{ type: "text", text: " fields and ", styles: {} },
|
|
149
|
+
{ type: "text", text: "both", styles: { bold: true, italic: true } },
|
|
150
|
+
{ type: "text", text: " and ", styles: {} },
|
|
151
|
+
{ type: "text", text: "both", styles: { bold: true, italic: true } },
|
|
152
|
+
{ type: "text", text: ".", styles: {} },
|
|
153
|
+
],
|
|
154
|
+
children: [],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("parses bold with nested italic keeping both styles", () => {
|
|
159
|
+
const blocks = markdownToBlocks("**foo _bar_ baz**");
|
|
160
|
+
|
|
161
|
+
expect(blocks[0]).toEqual({
|
|
162
|
+
type: "paragraph",
|
|
163
|
+
props: baseProps,
|
|
164
|
+
content: [
|
|
165
|
+
{ type: "text", text: "foo ", styles: { bold: true } },
|
|
166
|
+
{ type: "text", text: "bar", styles: { bold: true, italic: true } },
|
|
167
|
+
{ type: "text", text: " baz", styles: { bold: true } },
|
|
168
|
+
],
|
|
169
|
+
children: [],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
133
172
|
});
|