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.
- package/package/editor/blocks/snippet.js +42 -23
- package/package/editor/blocks/step.js +166 -35
- package/package/editor/blocks/stepField.d.ts +9 -1
- package/package/editor/blocks/stepField.js +664 -34
- package/package/editor/blocks/stepHorizontalView.d.ts +14 -0
- package/package/editor/blocks/stepHorizontalView.js +7 -0
- package/package/editor/blocks/useAutoResize.d.ts +8 -0
- package/package/editor/blocks/useAutoResize.js +31 -0
- package/package/editor/customMarkdownConverter.d.ts +1 -0
- package/package/editor/customMarkdownConverter.js +260 -31
- package/package/styles.css +706 -130
- package/package.json +9 -2
- package/src/App.tsx +1 -1
- package/src/editor/blocks/markdown.ts +27 -7
- package/src/editor/blocks/snippet.tsx +117 -61
- package/src/editor/blocks/step.tsx +325 -87
- package/src/editor/blocks/stepField.tsx +1396 -299
- package/src/editor/blocks/stepHorizontalView.tsx +90 -0
- package/src/editor/blocks/useAutoResize.ts +44 -0
- package/src/editor/customMarkdownConverter.test.ts +542 -3
- package/src/editor/customMarkdownConverter.ts +310 -36
- package/src/editor/customSchema.test.ts +35 -0
- package/src/editor/markdownToBlocks.test.ts +119 -0
- package/src/editor/styles.css +827 -128
|
@@ -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,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*)
|
|
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
|
-
|
|
128
|
-
|
|
127
|
+
const result = [];
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < content.length) {
|
|
130
|
+
const item = content[i];
|
|
129
131
|
if (isStyledTextInlineContent(item)) {
|
|
130
|
-
|
|
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(``);
|
|
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
|
-
|
|
148
|
+
result.push(`[${inner}](${safeHref})`);
|
|
149
|
+
i += 1;
|
|
150
|
+
continue;
|
|
136
151
|
}
|
|
137
152
|
if (Array.isArray(item.content)) {
|
|
138
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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 &&
|
|
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
|
-
|
|
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();
|