testomatio-editor-blocks 0.4.52 → 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.d.ts +12 -0
- package/package/editor/blocks/stepField.js +37 -13
- package/package/editor/blocks/useAutoResize.d.ts +3 -4
- package/package/editor/blocks/useAutoResize.js +70 -26
- package/package/editor/customMarkdownConverter.js +88 -28
- package/package/styles.css +6 -0
- package/package.json +1 -1
- package/src/editor/blocks/markdown.ts +110 -40
- package/src/editor/blocks/stepField.tsx +48 -15
- package/src/editor/blocks/stepFieldFormatting.test.ts +62 -1
- package/src/editor/blocks/useAutoResize.ts +73 -30
- package/src/editor/customMarkdownConverter.test.ts +32 -11
- package/src/editor/customMarkdownConverter.ts +102 -29
- package/src/editor/markdownToBlocks.test.ts +39 -0
- package/src/editor/styles.css +6 -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) {
|
|
@@ -40,6 +40,18 @@ export type FormattingMeta = {
|
|
|
40
40
|
end: number;
|
|
41
41
|
type: "bold" | "italic" | "code";
|
|
42
42
|
};
|
|
43
|
+
type FormatType = "bold" | "italic" | "code";
|
|
44
|
+
/**
|
|
45
|
+
* Remove formatting and link entries that conflict with applying `fmtType`
|
|
46
|
+
* over the half-open range [start, end). Exclusion rules:
|
|
47
|
+
* - code: exclusive with everything — drops any overlapping formatting and links
|
|
48
|
+
* - bold/italic: coexist with each other, but exclusive with code and links
|
|
49
|
+
* — drops overlapping same-type, overlapping code, and overlapping links
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyInlineExclusion(formatting: FormattingMeta[], links: LinkMeta[], start: number, end: number, fmtType: FormatType): {
|
|
52
|
+
formatting: FormattingMeta[];
|
|
53
|
+
links: LinkMeta[];
|
|
54
|
+
};
|
|
43
55
|
export declare function buildFullMarkdown(plainText: string, links: LinkMeta[], formatting: FormattingMeta[]): string;
|
|
44
56
|
export declare function StepField({ label, showLabel, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
45
57
|
export {};
|
|
@@ -18,6 +18,27 @@ const markdownParser = OverType.MarkdownParser;
|
|
|
18
18
|
function ImageUploadIcon() {
|
|
19
19
|
return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M12.667 2C13.0335 2.00008 13.3474 2.13057 13.6084 2.3916C13.8694 2.65264 13.9999 2.96648 14 3.33301V12.667C13.9999 13.0335 13.8694 13.3474 13.6084 13.6084C13.3474 13.8694 13.0335 13.9999 12.667 14H3.33301C2.96648 13.9999 2.65264 13.8694 2.3916 13.6084C2.13057 13.3474 2.00008 13.0335 2 12.667V3.33301C2.00008 2.96648 2.13057 2.65264 2.3916 2.3916C2.65264 2.13057 2.96648 2.00008 3.33301 2H12.667ZM3.33301 12.667H12.667V3.33301H3.33301V12.667ZM12 11.333H4L6 8.66699L7.5 10.667L9.5 8L12 11.333ZM5.66699 4.66699C5.94455 4.66707 6.18066 4.76375 6.375 4.95801C6.56944 5.15245 6.66699 5.38921 6.66699 5.66699C6.66692 5.94463 6.56937 6.18063 6.375 6.375C6.18063 6.56937 5.94463 6.66692 5.66699 6.66699C5.38921 6.66699 5.15245 6.56944 4.95801 6.375C4.76375 6.18066 4.66707 5.94455 4.66699 5.66699C4.66699 5.38921 4.76356 5.15245 4.95801 4.95801C5.15245 4.76356 5.38921 4.66699 5.66699 4.66699Z", fill: "currentColor" }) }));
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Remove formatting and link entries that conflict with applying `fmtType`
|
|
23
|
+
* over the half-open range [start, end). Exclusion rules:
|
|
24
|
+
* - code: exclusive with everything — drops any overlapping formatting and links
|
|
25
|
+
* - bold/italic: coexist with each other, but exclusive with code and links
|
|
26
|
+
* — drops overlapping same-type, overlapping code, and overlapping links
|
|
27
|
+
*/
|
|
28
|
+
export function applyInlineExclusion(formatting, links, start, end, fmtType) {
|
|
29
|
+
const overlaps = (a) => !(a.start >= end || a.end <= start);
|
|
30
|
+
const nextFormatting = formatting.filter((f) => {
|
|
31
|
+
if (!overlaps(f))
|
|
32
|
+
return true;
|
|
33
|
+
if (fmtType === "code")
|
|
34
|
+
return false;
|
|
35
|
+
if (f.type === "code")
|
|
36
|
+
return false;
|
|
37
|
+
return f.type !== fmtType;
|
|
38
|
+
});
|
|
39
|
+
const nextLinks = links.filter((l) => !overlaps(l));
|
|
40
|
+
return { formatting: nextFormatting, links: nextLinks };
|
|
41
|
+
}
|
|
21
42
|
const UNDO_STACK_LIMIT = 100;
|
|
22
43
|
function getActiveFormats(formatting, selStart, selEnd) {
|
|
23
44
|
const active = new Set();
|
|
@@ -732,6 +753,15 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
732
753
|
}
|
|
733
754
|
textareaNode.focus();
|
|
734
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
|
+
});
|
|
735
765
|
useEffect(() => {
|
|
736
766
|
var _a;
|
|
737
767
|
const instance = editorInstanceRef.current;
|
|
@@ -793,12 +823,6 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
793
823
|
}
|
|
794
824
|
textareaNode.readOnly = readOnly;
|
|
795
825
|
}, [readOnly, textareaNode]);
|
|
796
|
-
useAutoResize({
|
|
797
|
-
textarea: textareaNode,
|
|
798
|
-
multiline,
|
|
799
|
-
minRows: 3,
|
|
800
|
-
maxRows: 16,
|
|
801
|
-
});
|
|
802
826
|
useEffect(() => {
|
|
803
827
|
if (!textareaNode) {
|
|
804
828
|
return;
|
|
@@ -965,12 +989,9 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
965
989
|
formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
|
|
966
990
|
}
|
|
967
991
|
else if (start !== end) {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
formattingRef.current = formattingRef.current.filter((f) => f.start >= end || f.end <= start || (fmtType !== "code" && f.type !== fmtType));
|
|
972
|
-
// Add formatting for selection
|
|
973
|
-
formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
|
|
992
|
+
const cleaned = applyInlineExclusion(formattingRef.current, linksRef.current, start, end, fmtType);
|
|
993
|
+
formattingRef.current = [...cleaned.formatting, { start, end, type: fmtType }];
|
|
994
|
+
linksRef.current = cleaned.links;
|
|
974
995
|
}
|
|
975
996
|
else {
|
|
976
997
|
// No selection — nothing to format
|
|
@@ -1071,7 +1092,10 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1071
1092
|
const adjustedLinks = adjustLinksForEdit(linksRef.current.filter((l) => !(l.start < sel.end && l.end > sel.start)), sel.start, delta);
|
|
1072
1093
|
const newLink = { start: sel.start, end: sel.start + linkText.length, url };
|
|
1073
1094
|
linksRef.current = [...adjustedLinks, newLink];
|
|
1074
|
-
|
|
1095
|
+
// Links are exclusive with bold/italic/code: strip any formatting that
|
|
1096
|
+
// overlaps the original selection before shifting positions.
|
|
1097
|
+
const keptFormatting = formattingRef.current.filter((f) => f.start >= sel.end || f.end <= sel.start);
|
|
1098
|
+
formattingRef.current = adjustFormattingForEdit(keptFormatting, sel.start, delta);
|
|
1075
1099
|
prevTextRef.current = nextValue;
|
|
1076
1100
|
isSyncingRef.current = true;
|
|
1077
1101
|
instance.setValue(nextValue);
|
|
@@ -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,33 +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
|
-
if (!textarea || !
|
|
32
|
+
if (!textarea || !enabled)
|
|
6
33
|
return;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
var _a;
|
|
21
|
-
cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
|
|
22
|
-
frameRef.current = requestAnimationFrame(resize);
|
|
34
|
+
let cancelled = false;
|
|
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;
|
|
42
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
43
|
+
frameRef.current = 0;
|
|
44
|
+
if (!cancelled)
|
|
45
|
+
onResizeRef.current();
|
|
46
|
+
});
|
|
23
47
|
};
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
53
|
+
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (entry.isIntersecting) {
|
|
56
|
+
schedule();
|
|
57
|
+
intersectionObserver.disconnect();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
intersectionObserver.observe(textarea);
|
|
63
|
+
const unregisterFonts = registerFontsReady(schedule);
|
|
26
64
|
return () => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
65
|
+
cancelled = true;
|
|
66
|
+
intersectionObserver.disconnect();
|
|
67
|
+
unregisterFonts();
|
|
68
|
+
if (frameRef.current) {
|
|
69
|
+
cancelAnimationFrame(frameRef.current);
|
|
70
|
+
frameRef.current = 0;
|
|
71
|
+
}
|
|
31
72
|
};
|
|
32
|
-
}, [textarea,
|
|
73
|
+
}, [textarea, enabled]);
|
|
74
|
+
return useCallback(() => {
|
|
75
|
+
onResizeRef.current();
|
|
76
|
+
}, []);
|
|
33
77
|
}
|
|
@@ -365,7 +365,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
365
365
|
const normalizedExpected = stripExpectedPrefix(expectedResult).trim();
|
|
366
366
|
if (normalizedExpected.length > 0) {
|
|
367
367
|
const expectedLines = normalizedExpected.split(/\r?\n/);
|
|
368
|
-
const label = "*Expected*";
|
|
368
|
+
const label = "*Expected result*";
|
|
369
369
|
expectedLines.forEach((expectedLine, index) => {
|
|
370
370
|
const trimmedLine = expectedLine.trim();
|
|
371
371
|
if (trimmedLine.length === 0) {
|
|
@@ -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
|
}
|
|
@@ -656,7 +696,27 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
656
696
|
const rawLine = lines[index];
|
|
657
697
|
const trimmed = rawLine.trim();
|
|
658
698
|
if (!trimmed) {
|
|
659
|
-
|
|
699
|
+
// Peek at the next non-blank line. If it's another item of this list
|
|
700
|
+
// (same indent level and list type), treat the blank lines as loose-list
|
|
701
|
+
// separators and consume them. Otherwise leave the blank line for the
|
|
702
|
+
// outer loop so it can become an empty paragraph block.
|
|
703
|
+
let lookahead = index + 1;
|
|
704
|
+
while (lookahead < lines.length && !lines[lookahead].trim()) {
|
|
705
|
+
lookahead += 1;
|
|
706
|
+
}
|
|
707
|
+
if (lookahead >= lines.length) {
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
const nextLine = lines[lookahead];
|
|
711
|
+
const nextIndent = countIndent(nextLine);
|
|
712
|
+
if (nextIndent < indentLevel * 2) {
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
const nextType = detectListType(nextLine.trim());
|
|
716
|
+
if (nextType !== listType) {
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
index = lookahead;
|
|
660
720
|
continue;
|
|
661
721
|
}
|
|
662
722
|
let indent = countIndent(rawLine);
|
package/package/styles.css
CHANGED
|
@@ -1100,6 +1100,12 @@ html.dark .bn-step-image-preview__content {
|
|
|
1100
1100
|
color: rgb(146, 64, 14) !important;
|
|
1101
1101
|
}
|
|
1102
1102
|
|
|
1103
|
+
.bn-step-editor .overtype-wrapper .overtype-preview li.bullet-list .syntax-marker,
|
|
1104
|
+
.bn-step-editor .overtype-wrapper .overtype-preview li.ordered-list .syntax-marker {
|
|
1105
|
+
color: inherit !important;
|
|
1106
|
+
opacity: 1 !important;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1103
1109
|
.bn-step-custom-caret {
|
|
1104
1110
|
display: none;
|
|
1105
1111
|
position: absolute;
|
package/package.json
CHANGED