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.
@@ -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, "&amp;")
@@ -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
- const rawSegments = normalized.split(INLINE_SEGMENT_REGEX).filter(Boolean);
21
- return rawSegments.map((segment) => {
22
- const baseStyles = { bold: false, italic: false, underline: false };
23
- if (/^\*\*\*(.+)\*\*\*$/.test(segment) || /^___(.+)___$/.test(segment)) {
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
- // Remove overlapping formatting:
969
- // - Code: remove ALL overlapping formatting (code replaces bold/italic)
970
- // - Bold/Italic: remove only overlapping formatting of the SAME type
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
- formattingRef.current = adjustFormattingForEdit(formattingRef.current, sel.start, delta);
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
- multiline?: boolean;
4
- minRows?: number;
5
- maxRows?: number;
3
+ enabled?: boolean;
4
+ onResize: () => void;
6
5
  };
7
- export declare function useAutoResize({ textarea, multiline, minRows, maxRows }: Options): void;
6
+ export declare function useAutoResize({ textarea, enabled, onResize }: Options): () => void;
8
7
  export {};
@@ -1,33 +1,77 @@
1
- import { useEffect, useRef } from "react";
2
- export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRows = 12 }) {
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 || !multiline) {
32
+ if (!textarea || !enabled)
6
33
  return;
7
- }
8
- const resize = () => {
9
- textarea.style.height = "auto";
10
- const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight || "16");
11
- const minHeight = lineHeight * minRows;
12
- const maxHeight = lineHeight * maxRows;
13
- const clampedHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
14
- textarea.style.height = `${clampedHeight}px`;
15
- textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden";
16
- };
17
- const observer = new MutationObserver(resize);
18
- observer.observe(textarea, { childList: true, characterData: true, subtree: true });
19
- const handleInput = () => {
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
- textarea.addEventListener("input", handleInput);
25
- resize();
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
- var _a;
28
- observer.disconnect();
29
- textarea.removeEventListener("input", handleInput);
30
- cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
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, multiline, minRows, maxRows]);
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
- const cleaned = stripHtmlWrappers(text);
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({ type: "text", text: unescapeMarkdown(buffer), styles: {} });
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
- pushPlain();
540
- const inner = cleaned.slice(i + 2, end);
541
- result.push({
542
- type: "text",
543
- text: unescapeMarkdown(inner),
544
- styles: { bold: true },
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
- pushPlain();
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 = parseInlineMarkdown(label);
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.indexOf(marker, i + 1);
646
+ const end = findItalicClose(cleaned, i + 1, marker);
601
647
  if (end !== -1) {
602
- pushPlain();
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
- index += 1;
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);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.52",
3
+ "version": "0.4.54",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",