testomatio-editor-blocks 0.3.0 → 0.4.1

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.
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ type StepHorizontalViewProps = {
3
+ blockId: string;
4
+ stepNumber: number;
5
+ stepValue: string;
6
+ expectedResult: string;
7
+ onStepChange: (next: string) => void;
8
+ onExpectedChange: (next: string) => void;
9
+ onInsertNextStep: () => void;
10
+ onFieldFocus: () => void;
11
+ viewToggle?: ReactNode;
12
+ };
13
+ export declare function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }: StepHorizontalViewProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StepField } from "./stepField";
3
+ const STEP_PLACEHOLDER = "Enter step name";
4
+ const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result";
5
+ export function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }) {
6
+ return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsxs("div", { className: "bn-teststep__horizontal-col", children: [_jsx("div", { className: "bn-teststep__header", children: _jsx("span", { className: "bn-teststep__title", children: "Step" }) }), _jsx(StepField, { label: "Step", showLabel: false, value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true })] }), _jsxs("div", { className: "bn-teststep__horizontal-col", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Expected result" }), viewToggle] }), _jsx(StepField, { label: "Expected result", showLabel: false, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus })] })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
7
+ }
@@ -0,0 +1,8 @@
1
+ type Options = {
2
+ textarea: HTMLTextAreaElement | null;
3
+ multiline?: boolean;
4
+ minRows?: number;
5
+ maxRows?: number;
6
+ };
7
+ export declare function useAutoResize({ textarea, multiline, minRows, maxRows }: Options): void;
8
+ export {};
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from "react";
2
+ export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRows = 12 }) {
3
+ const frameRef = useRef(0);
4
+ useEffect(() => {
5
+ if (!textarea || !multiline) {
6
+ 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
+ textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight)}px`;
14
+ };
15
+ const observer = new MutationObserver(resize);
16
+ observer.observe(textarea, { childList: true, characterData: true, subtree: true });
17
+ const handleInput = () => {
18
+ var _a;
19
+ cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
20
+ frameRef.current = requestAnimationFrame(resize);
21
+ };
22
+ textarea.addEventListener("input", handleInput);
23
+ resize();
24
+ return () => {
25
+ var _a;
26
+ observer.disconnect();
27
+ textarea.removeEventListener("input", handleInput);
28
+ cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
29
+ };
30
+ }, [textarea, multiline, minRows, maxRows]);
31
+ }
@@ -4,5 +4,6 @@ type Schema = typeof customSchema;
4
4
  export type CustomEditorBlock = Block<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>;
5
5
  export type CustomPartialBlock = PartialBlock<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>;
6
6
  export declare function blocksToMarkdown(blocks: CustomEditorBlock[]): string;
7
+ export declare function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPartialBlock[];
7
8
  export declare function markdownToBlocks(markdown: string): CustomPartialBlock[];
8
9
  export {};
@@ -23,7 +23,7 @@ const headingPrefixes = {
23
23
  const SPECIAL_CHAR_REGEX = /([*_`~\[\]()<>\\])/g;
24
24
  const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
25
25
  const HTML_UNDERLINE_REGEX = /<\/?u>/g;
26
- const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?(?:\s*[:\-–—]?\s*)/i;
26
+ const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?\s*[:\-–—]\s*/i;
27
27
  // Matches any non-empty line that falls between the step title and the expected result line.
28
28
  const STEP_DATA_LINE_REGEX = /^(?!\s*(?:[*_`]*\s*)?(?:expected(?:\s+result)?)\b).+/i;
29
29
  const NUMBERED_STEP_REGEX = /^\d+[.)]\s+/;
@@ -124,18 +124,53 @@ function inlineToMarkdown(content) {
124
124
  if (!content || !Array.isArray(content)) {
125
125
  return "";
126
126
  }
127
- return content
128
- .map((item) => {
127
+ const result = [];
128
+ let i = 0;
129
+ while (i < content.length) {
130
+ const item = content[i];
129
131
  if (isStyledTextInlineContent(item)) {
130
- return applyTextStyles(escapeMarkdown(item.text), item.styles);
132
+ // Check if this is a "!" followed by a link (image syntax)
133
+ if (item.text === "!" && i + 1 < content.length && isLinkInlineContent(content[i + 1])) {
134
+ const link = content[i + 1];
135
+ const inner = inlineToMarkdown(link.content);
136
+ const safeHref = escapeMarkdown(link.href);
137
+ result.push(`![${inner}](${safeHref})`);
138
+ i += 2;
139
+ continue;
140
+ }
141
+ result.push(applyTextStyles(escapeMarkdown(item.text), item.styles));
142
+ i += 1;
143
+ continue;
131
144
  }
132
145
  if (isLinkInlineContent(item)) {
133
146
  const inner = inlineToMarkdown(item.content);
134
147
  const safeHref = escapeMarkdown(item.href);
135
- return `[${inner}](${safeHref})`;
148
+ result.push(`[${inner}](${safeHref})`);
149
+ i += 1;
150
+ continue;
136
151
  }
137
152
  if (Array.isArray(item.content)) {
138
- return inlineToMarkdown(item.content);
153
+ result.push(inlineToMarkdown(item.content));
154
+ i += 1;
155
+ continue;
156
+ }
157
+ i += 1;
158
+ }
159
+ return result.join("");
160
+ }
161
+ function inlineContentToPlainText(content) {
162
+ if (!Array.isArray(content)) {
163
+ return "";
164
+ }
165
+ return content
166
+ .map((node) => {
167
+ if (node && typeof node === "object") {
168
+ if (typeof node.text === "string") {
169
+ return node.text;
170
+ }
171
+ if (Array.isArray(node.content)) {
172
+ return inlineContentToPlainText(node.content);
173
+ }
139
174
  }
140
175
  return "";
141
176
  })
@@ -479,10 +514,13 @@ function parseInlineMarkdown(text) {
479
514
  pushPlain();
480
515
  const label = cleaned.slice(i + 1, endLabel);
481
516
  const href = cleaned.slice(startLink + 1, endLink);
517
+ const parsedLabel = parseInlineMarkdown(label);
518
+ // Ensure link content is never undefined - if empty, add empty text
519
+ const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
482
520
  result.push({
483
521
  type: "link",
484
522
  href: unescapeMarkdown(href),
485
- content: parseInlineMarkdown(label),
523
+ content: linkContent,
486
524
  });
487
525
  i = endLink + 1;
488
526
  continue;
@@ -534,7 +572,7 @@ function countIndent(line) {
534
572
  const match = line.match(/^ */);
535
573
  return match ? match[0].length : 0;
536
574
  }
537
- function parseList(lines, startIndex, listType, indentLevel) {
575
+ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = false) {
538
576
  var _a, _b, _c, _d;
539
577
  const items = [];
540
578
  let index = startIndex;
@@ -546,14 +584,13 @@ function parseList(lines, startIndex, listType, indentLevel) {
546
584
  continue;
547
585
  }
548
586
  let indent = countIndent(rawLine);
549
- const baseIndent = indentLevel * 2;
550
- if (indent > baseIndent && indent <= baseIndent + 1) {
551
- indent = baseIndent;
552
- }
553
587
  if (indent < indentLevel * 2) {
554
588
  break;
555
589
  }
556
- if (indent > indentLevel * 2) {
590
+ // Check if this line should be parsed as nested content
591
+ // Only go deeper if indent is at least 2 more than the next level's expected indent
592
+ const nextLevelExpectedIndent = (indentLevel + 1) * 2;
593
+ if (indent >= nextLevelExpectedIndent) {
557
594
  const lastItem = items.at(-1);
558
595
  if (!lastItem) {
559
596
  break;
@@ -562,7 +599,12 @@ function parseList(lines, startIndex, listType, indentLevel) {
562
599
  if (!nestedType) {
563
600
  break;
564
601
  }
565
- const nested = parseList(lines, index, nestedType, indentLevel + 1);
602
+ const nested = parseList(lines, index, nestedType, indentLevel + 1, allowEmptySteps);
603
+ // If nested parsing made no progress, skip this line to avoid infinite loop
604
+ if (nested.nextIndex === index) {
605
+ index += 1;
606
+ continue;
607
+ }
566
608
  lastItem.children = [...((_a = lastItem.children) !== null && _a !== void 0 ? _a : []), ...nested.items];
567
609
  index = nested.nextIndex;
568
610
  continue;
@@ -571,6 +613,23 @@ function parseList(lines, startIndex, listType, indentLevel) {
571
613
  if (detectedType !== listType) {
572
614
  break;
573
615
  }
616
+ // Only try to parse as testStep for top-level items (indentLevel === 0)
617
+ // when we're under a Steps heading AND the list type is bullet
618
+ // Numbered lists under Steps heading are only parsed as test steps if they look like test steps
619
+ if (indentLevel === 0 && allowEmptySteps) {
620
+ // For bullet lists, always try to parse as test steps
621
+ // For numbered lists, only try if they have step-like characteristics
622
+ const looksLikeTestStep = listType === "bullet" ||
623
+ (listType === "numbered" && (isLikelyStep(lines, index)));
624
+ if (looksLikeTestStep) {
625
+ const nextStep = parseTestStep(lines, index, allowEmptySteps);
626
+ if (nextStep) {
627
+ items.push(nextStep.block);
628
+ index = nextStep.nextIndex;
629
+ continue;
630
+ }
631
+ }
632
+ }
574
633
  if (listType === "check") {
575
634
  const checkMatch = trimmed.match(/^[-*+]\s+\[([xX\s])\]\s+(.*)$/);
576
635
  const checked = ((_b = checkMatch === null || checkMatch === void 0 ? void 0 : checkMatch[1]) !== null && _b !== void 0 ? _b : "").toLowerCase() === "x";
@@ -607,13 +666,46 @@ function parseList(lines, startIndex, listType, indentLevel) {
607
666
  }
608
667
  return { items, nextIndex: index };
609
668
  }
610
- function parseTestStep(lines, index, snippetId) {
669
+ function isLikelyStep(lines, index) {
670
+ // Look ahead to see if there's indented content or expected result
671
+ if (index + 1 >= lines.length)
672
+ return false;
673
+ const nextLine = lines[index + 1];
674
+ const hasIndent = /^\s{2,}/.test(nextLine);
675
+ // Check if the next line contains expected result markers
676
+ const nextTrimmed = nextLine.trim();
677
+ const hasExpectedResult = EXPECTED_LABEL_REGEX.test(nextTrimmed);
678
+ // Only consider it a test step if:
679
+ // 1. It has an expected result, OR
680
+ // 2. The next line is indented but doesn't start with a numbered or bullet list
681
+ if (hasExpectedResult)
682
+ return true;
683
+ if (hasIndent && !/^\d+[.)]/.test(nextTrimmed) && !/^[-*+]/.test(nextTrimmed))
684
+ return true;
685
+ return false;
686
+ }
687
+ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
611
688
  const current = lines[index];
612
689
  const trimmed = current.trim();
613
- if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
690
+ const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
691
+ const isNumbered = /^\d+[.)]\s+/.test(trimmed);
692
+ if (!isBullet && !isNumbered) {
693
+ return null;
694
+ }
695
+ // For numbered lists, only parse as test steps if called from parseList with allowEmpty=true
696
+ // The first call to parseTestStep from markdownToBlocks uses allowEmpty=stepsHeadingLevel !== null
697
+ // which should be false unless we're under a Steps heading
698
+ if (isNumbered && !allowEmpty) {
614
699
  return null;
615
700
  }
616
- let rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
701
+ let rawTitle;
702
+ if (isBullet) {
703
+ rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
704
+ }
705
+ else {
706
+ // For numbered lists, remove the number and delimiter
707
+ rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
708
+ }
617
709
  let blockType = "testStep";
618
710
  const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
619
711
  if (snippetMatch) {
@@ -640,8 +732,13 @@ function parseTestStep(lines, index, snippetId) {
640
732
  const hasIndent = /^\s{2,}/.test(line);
641
733
  const rawTrimmed = line.trim();
642
734
  if (!rawTrimmed) {
643
- if (stepDataLines.length > 0) {
644
- stepDataLines.push("");
735
+ if (stepDataLines.length > 0 || inExpectedResult) {
736
+ if (inExpectedResult) {
737
+ expectedResult += "\n";
738
+ }
739
+ else {
740
+ stepDataLines.push("");
741
+ }
645
742
  }
646
743
  next += 1;
647
744
  continue;
@@ -658,20 +755,66 @@ function parseTestStep(lines, index, snippetId) {
658
755
  rawTrimmed.startsWith("|")) {
659
756
  break;
660
757
  }
661
- if (rawTrimmed.match(EXPECTED_LABEL_REGEX)) {
758
+ // Check for expected result labels with different formatting
759
+ const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
760
+ const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
761
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
762
+ if (expectedMatch || expectedStarMatch) {
763
+ inExpectedResult = true;
764
+ const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
765
+ let content = rawTrimmed.slice(label.length).trim();
766
+ // Add the content (if any) from this line
767
+ if (content) {
768
+ const expectedContent = unescapeMarkdown(content);
769
+ if (expectedResult.length > 0) {
770
+ expectedResult += "\n" + expectedContent;
771
+ }
772
+ else {
773
+ expectedResult = expectedContent;
774
+ }
775
+ }
776
+ next += 1;
777
+ continue;
778
+ }
779
+ // Check for lines that start with * and contain Expected (but don't match the above patterns)
780
+ if (rawTrimmed.match(/^\*[^*]*expected/i)) {
662
781
  inExpectedResult = true;
663
- const withoutLabel = stripExpectedPrefix(rawTrimmed);
664
- expectedResult = unescapeMarkdown(withoutLabel);
782
+ // Remove the leading * and trim
783
+ let content = rawTrimmed.slice(1).trim();
784
+ // Remove any "Expected:" prefix
785
+ content = content.replace(/^expected\s*:?\s*/i, '').trim();
786
+ const expectedContent = unescapeMarkdown(content);
787
+ if (expectedResult.length > 0) {
788
+ expectedResult += "\n" + expectedContent;
789
+ }
790
+ else {
791
+ expectedResult = expectedContent;
792
+ }
665
793
  next += 1;
666
794
  continue;
667
795
  }
668
796
  if (rawTrimmed.startsWith("```")) {
669
- stepDataLines.push(unescapeMarkdown(rawTrimmed));
797
+ if (inExpectedResult) {
798
+ if (expectedResult.length > 0) {
799
+ expectedResult += "\n" + unescapeMarkdown(rawTrimmed);
800
+ }
801
+ else {
802
+ expectedResult = unescapeMarkdown(rawTrimmed);
803
+ }
804
+ }
805
+ else {
806
+ stepDataLines.push(unescapeMarkdown(rawTrimmed));
807
+ }
670
808
  next += 1;
671
809
  while (next < lines.length) {
672
810
  const fenceLine = lines[next];
673
811
  const fenceTrimmed = fenceLine.trim();
674
- stepDataLines.push(unescapeMarkdown(fenceTrimmed));
812
+ if (inExpectedResult) {
813
+ expectedResult += "\n" + unescapeMarkdown(fenceTrimmed);
814
+ }
815
+ else {
816
+ stepDataLines.push(unescapeMarkdown(fenceTrimmed));
817
+ }
675
818
  next += 1;
676
819
  if (fenceTrimmed.startsWith("```")) {
677
820
  break;
@@ -680,8 +823,16 @@ function parseTestStep(lines, index, snippetId) {
680
823
  continue;
681
824
  }
682
825
  if (inExpectedResult) {
683
- const withoutLabel = stripExpectedPrefix(rawTrimmed);
684
- expectedResult += "\n" + unescapeMarkdown(withoutLabel);
826
+ // After finding the first expected result, indented lines are part of it
827
+ if (hasIndent) {
828
+ const expectedContent = unescapeMarkdown(rawTrimmed);
829
+ if (expectedResult.length > 0) {
830
+ expectedResult += "\n" + expectedContent;
831
+ }
832
+ else {
833
+ expectedResult = expectedContent;
834
+ }
835
+ }
685
836
  next += 1;
686
837
  continue;
687
838
  }
@@ -691,13 +842,23 @@ function parseTestStep(lines, index, snippetId) {
691
842
  next += 1;
692
843
  continue;
693
844
  }
845
+ // If we have indent and the line doesn't match other patterns, treat it as step data
846
+ if (hasIndent) {
847
+ const content = unescapeMarkdown(rawTrimmed);
848
+ stepDataLines.push(content);
849
+ next += 1;
850
+ continue;
851
+ }
694
852
  break;
695
853
  }
696
854
  const stepData = stepDataLines
697
855
  .map((line) => line.trimEnd())
698
856
  .join("\n")
699
857
  .trim();
700
- if (!isLikelyStep && !expectedResult && stepDataLines.length === 0) {
858
+ if (!isLikelyStep &&
859
+ !expectedResult &&
860
+ stepDataLines.length === 0 &&
861
+ !(allowEmpty && titleWithPlaceholders.length > 0)) {
701
862
  return null;
702
863
  }
703
864
  const stepDataWithImages = [stepData, titleImages.join("\n")]
@@ -899,11 +1060,67 @@ function parseSnippetWrapper(lines, index) {
899
1060
  nextIndex: next,
900
1061
  };
901
1062
  }
1063
+ // Post-process blocks to fix malformed image blocks
1064
+ export function fixMalformedImageBlocks(blocks) {
1065
+ var _a, _b, _c, _d;
1066
+ const result = [];
1067
+ let i = 0;
1068
+ while (i < blocks.length) {
1069
+ const current = blocks[i];
1070
+ const next = blocks[i + 1];
1071
+ // Skip empty paragraphs
1072
+ if (current.type === "paragraph" &&
1073
+ (!current.content || !Array.isArray(current.content) || current.content.length === 0)) {
1074
+ i += 1;
1075
+ continue;
1076
+ }
1077
+ // Check if current is a paragraph with just "!" - this is definitely a malformed image
1078
+ if (current.type === "paragraph" &&
1079
+ current.content &&
1080
+ Array.isArray(current.content) &&
1081
+ current.content.length === 1 &&
1082
+ ((_a = current.content[0]) === null || _a === void 0 ? void 0 : _a.type) === "text" &&
1083
+ ((_b = current.content[0]) === null || _b === void 0 ? void 0 : _b.text) === "!") {
1084
+ // This is a malformed image block, skip it entirely
1085
+ // The full image was likely parsed as ![](...) but got corrupted
1086
+ i += 1;
1087
+ continue;
1088
+ }
1089
+ // Check if current is a paragraph with just "!" and next is an empty paragraph
1090
+ if (current.type === "paragraph" &&
1091
+ (next === null || next === void 0 ? void 0 : next.type) === "paragraph" &&
1092
+ current.content &&
1093
+ Array.isArray(current.content) &&
1094
+ current.content.length === 1 &&
1095
+ ((_c = current.content[0]) === null || _c === void 0 ? void 0 : _c.type) === "text" &&
1096
+ ((_d = current.content[0]) === null || _d === void 0 ? void 0 : _d.text) === "!" &&
1097
+ (!next.content || !Array.isArray(next.content) || next.content.length === 0)) {
1098
+ // This looks like a malformed image, skip both blocks
1099
+ i += 2;
1100
+ continue;
1101
+ }
1102
+ // Check if current has "!" but no link
1103
+ if (current.type === "paragraph" &&
1104
+ current.content &&
1105
+ Array.isArray(current.content) &&
1106
+ current.content.some((item) => item.type === "text" && item.text === "!") &&
1107
+ !current.content.some((item) => item.type === "link")) {
1108
+ // Skip malformed image block
1109
+ i += 1;
1110
+ continue;
1111
+ }
1112
+ result.push(current);
1113
+ i += 1;
1114
+ }
1115
+ return result;
1116
+ }
902
1117
  export function markdownToBlocks(markdown) {
1118
+ var _a, _b;
903
1119
  const normalized = markdown.replace(/\r\n/g, "\n");
904
1120
  const lines = normalized.split("\n");
905
1121
  const blocks = [];
906
1122
  let index = 0;
1123
+ let stepsHeadingLevel = null;
907
1124
  while (index < lines.length) {
908
1125
  const line = lines[index];
909
1126
  if (!line.trim()) {
@@ -916,7 +1133,7 @@ export function markdownToBlocks(markdown) {
916
1133
  index = snippetWrapper.nextIndex;
917
1134
  continue;
918
1135
  }
919
- const stepLikeBlock = parseTestStep(lines, index);
1136
+ const stepLikeBlock = parseTestStep(lines, index, stepsHeadingLevel !== null);
920
1137
  if (stepLikeBlock) {
921
1138
  blocks.push(stepLikeBlock.block);
922
1139
  index = stepLikeBlock.nextIndex;
@@ -930,7 +1147,19 @@ export function markdownToBlocks(markdown) {
930
1147
  }
931
1148
  const heading = parseHeading(lines, index);
932
1149
  if (heading) {
933
- blocks.push(heading.block);
1150
+ const headingBlock = heading.block;
1151
+ const headingLevel = (_b = (_a = headingBlock.props) === null || _a === void 0 ? void 0 : _a.level) !== null && _b !== void 0 ? _b : 3;
1152
+ const headingText = inlineContentToPlainText(headingBlock.content);
1153
+ const normalizedHeading = headingText.trim().toLowerCase();
1154
+ if (normalizedHeading.replace(/[:\-–—]$/, "") === "steps") {
1155
+ stepsHeadingLevel = headingLevel;
1156
+ }
1157
+ else if (stepsHeadingLevel !== null &&
1158
+ headingLevel <= stepsHeadingLevel &&
1159
+ normalizedHeading.length > 0) {
1160
+ stepsHeadingLevel = null;
1161
+ }
1162
+ blocks.push(headingBlock);
934
1163
  index = heading.nextIndex;
935
1164
  continue;
936
1165
  }
@@ -948,7 +1177,7 @@ export function markdownToBlocks(markdown) {
948
1177
  }
949
1178
  const listType = detectListType(line.trim());
950
1179
  if (listType) {
951
- const { items, nextIndex } = parseList(lines, index, listType, 0);
1180
+ const { items, nextIndex } = parseList(lines, index, listType, 0, stepsHeadingLevel !== null);
952
1181
  blocks.push(...items);
953
1182
  index = nextIndex;
954
1183
  continue;
@@ -957,7 +1186,7 @@ export function markdownToBlocks(markdown) {
957
1186
  blocks.push(paragraph.block);
958
1187
  index = paragraph.nextIndex;
959
1188
  }
960
- return blocks;
1189
+ return fixMalformedImageBlocks(blocks);
961
1190
  }
962
1191
  function splitTableRow(line) {
963
1192
  let value = line.trim();