testomatio-editor-blocks 0.3.0 → 0.4.0
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 +127 -2
- package/package/editor/blocks/step.js +82 -15
- package/package/editor/blocks/stepField.d.ts +6 -1
- package/package/editor/blocks/stepField.js +20 -2
- package/package/editor/blocks/useAutoResize.d.ts +8 -0
- package/package/editor/blocks/useAutoResize.js +31 -0
- package/package/editor/customMarkdownConverter.js +150 -21
- package/package/styles.css +204 -71
- package/package.json +5 -2
- package/src/App.tsx +1 -1
- package/src/editor/blocks/markdown.ts +27 -7
- package/src/editor/blocks/snippet.tsx +202 -26
- package/src/editor/blocks/step.tsx +132 -36
- package/src/editor/blocks/stepField.tsx +552 -267
- package/src/editor/blocks/useAutoResize.ts +44 -0
- package/src/editor/customMarkdownConverter.test.ts +99 -2
- package/src/editor/customMarkdownConverter.ts +166 -19
- package/src/editor/customSchema.test.ts +35 -0
- package/src/editor/markdownToBlocks.test.ts +119 -0
- package/src/editor/styles.css +342 -71
|
@@ -141,6 +141,24 @@ function inlineToMarkdown(content) {
|
|
|
141
141
|
})
|
|
142
142
|
.join("");
|
|
143
143
|
}
|
|
144
|
+
function inlineContentToPlainText(content) {
|
|
145
|
+
if (!Array.isArray(content)) {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
return content
|
|
149
|
+
.map((node) => {
|
|
150
|
+
if (node && typeof node === "object") {
|
|
151
|
+
if (typeof node.text === "string") {
|
|
152
|
+
return node.text;
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(node.content)) {
|
|
155
|
+
return inlineContentToPlainText(node.content);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return "";
|
|
159
|
+
})
|
|
160
|
+
.join("");
|
|
161
|
+
}
|
|
144
162
|
function serializeChildren(block, ctx) {
|
|
145
163
|
var _a;
|
|
146
164
|
if (!((_a = block.children) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
@@ -534,7 +552,7 @@ function countIndent(line) {
|
|
|
534
552
|
const match = line.match(/^ */);
|
|
535
553
|
return match ? match[0].length : 0;
|
|
536
554
|
}
|
|
537
|
-
function parseList(lines, startIndex, listType, indentLevel) {
|
|
555
|
+
function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = false) {
|
|
538
556
|
var _a, _b, _c, _d;
|
|
539
557
|
const items = [];
|
|
540
558
|
let index = startIndex;
|
|
@@ -546,14 +564,13 @@ function parseList(lines, startIndex, listType, indentLevel) {
|
|
|
546
564
|
continue;
|
|
547
565
|
}
|
|
548
566
|
let indent = countIndent(rawLine);
|
|
549
|
-
const baseIndent = indentLevel * 2;
|
|
550
|
-
if (indent > baseIndent && indent <= baseIndent + 1) {
|
|
551
|
-
indent = baseIndent;
|
|
552
|
-
}
|
|
553
567
|
if (indent < indentLevel * 2) {
|
|
554
568
|
break;
|
|
555
569
|
}
|
|
556
|
-
if
|
|
570
|
+
// Check if this line should be parsed as nested content
|
|
571
|
+
// Only go deeper if indent is at least 2 more than the next level's expected indent
|
|
572
|
+
const nextLevelExpectedIndent = (indentLevel + 1) * 2;
|
|
573
|
+
if (indent >= nextLevelExpectedIndent) {
|
|
557
574
|
const lastItem = items.at(-1);
|
|
558
575
|
if (!lastItem) {
|
|
559
576
|
break;
|
|
@@ -562,7 +579,12 @@ function parseList(lines, startIndex, listType, indentLevel) {
|
|
|
562
579
|
if (!nestedType) {
|
|
563
580
|
break;
|
|
564
581
|
}
|
|
565
|
-
const nested = parseList(lines, index, nestedType, indentLevel + 1);
|
|
582
|
+
const nested = parseList(lines, index, nestedType, indentLevel + 1, allowEmptySteps);
|
|
583
|
+
// If nested parsing made no progress, skip this line to avoid infinite loop
|
|
584
|
+
if (nested.nextIndex === index) {
|
|
585
|
+
index += 1;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
566
588
|
lastItem.children = [...((_a = lastItem.children) !== null && _a !== void 0 ? _a : []), ...nested.items];
|
|
567
589
|
index = nested.nextIndex;
|
|
568
590
|
continue;
|
|
@@ -571,6 +593,16 @@ function parseList(lines, startIndex, listType, indentLevel) {
|
|
|
571
593
|
if (detectedType !== listType) {
|
|
572
594
|
break;
|
|
573
595
|
}
|
|
596
|
+
// Only try to parse as testStep for top-level bullet items (indentLevel === 0)
|
|
597
|
+
// Nested bullets within numbered lists should remain as regular bulletListItem
|
|
598
|
+
if (listType === "bullet" && indentLevel === 0) {
|
|
599
|
+
const nextStep = parseTestStep(lines, index, allowEmptySteps);
|
|
600
|
+
if (nextStep) {
|
|
601
|
+
items.push(nextStep.block);
|
|
602
|
+
index = nextStep.nextIndex;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
574
606
|
if (listType === "check") {
|
|
575
607
|
const checkMatch = trimmed.match(/^[-*+]\s+\[([xX\s])\]\s+(.*)$/);
|
|
576
608
|
const checked = ((_b = checkMatch === null || checkMatch === void 0 ? void 0 : checkMatch[1]) !== null && _b !== void 0 ? _b : "").toLowerCase() === "x";
|
|
@@ -607,7 +639,7 @@ function parseList(lines, startIndex, listType, indentLevel) {
|
|
|
607
639
|
}
|
|
608
640
|
return { items, nextIndex: index };
|
|
609
641
|
}
|
|
610
|
-
function parseTestStep(lines, index, snippetId) {
|
|
642
|
+
function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
611
643
|
const current = lines[index];
|
|
612
644
|
const trimmed = current.trim();
|
|
613
645
|
if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
|
|
@@ -635,13 +667,19 @@ function parseTestStep(lines, index, snippetId) {
|
|
|
635
667
|
let expectedResult = "";
|
|
636
668
|
let next = index + 1;
|
|
637
669
|
let inExpectedResult = false;
|
|
670
|
+
let foundFirstExpected = false;
|
|
638
671
|
while (next < lines.length) {
|
|
639
672
|
const line = lines[next];
|
|
640
673
|
const hasIndent = /^\s{2,}/.test(line);
|
|
641
674
|
const rawTrimmed = line.trim();
|
|
642
675
|
if (!rawTrimmed) {
|
|
643
|
-
if (stepDataLines.length > 0) {
|
|
644
|
-
|
|
676
|
+
if (stepDataLines.length > 0 || inExpectedResult) {
|
|
677
|
+
if (inExpectedResult) {
|
|
678
|
+
expectedResult += "\n";
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
stepDataLines.push("");
|
|
682
|
+
}
|
|
645
683
|
}
|
|
646
684
|
next += 1;
|
|
647
685
|
continue;
|
|
@@ -658,20 +696,68 @@ function parseTestStep(lines, index, snippetId) {
|
|
|
658
696
|
rawTrimmed.startsWith("|")) {
|
|
659
697
|
break;
|
|
660
698
|
}
|
|
661
|
-
|
|
699
|
+
// Check for expected result labels with different formatting
|
|
700
|
+
const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
|
|
701
|
+
const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
|
|
702
|
+
rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
|
|
703
|
+
if (expectedMatch || expectedStarMatch) {
|
|
704
|
+
foundFirstExpected = true;
|
|
705
|
+
inExpectedResult = true;
|
|
706
|
+
const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
|
|
707
|
+
let content = rawTrimmed.slice(label.length).trim();
|
|
708
|
+
// Add the content (if any) from this line
|
|
709
|
+
if (content) {
|
|
710
|
+
const expectedContent = unescapeMarkdown(content);
|
|
711
|
+
if (expectedResult.length > 0) {
|
|
712
|
+
expectedResult += "\n" + expectedContent;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
expectedResult = expectedContent;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
next += 1;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
// Check for lines that start with * and contain Expected (but don't match the above patterns)
|
|
722
|
+
if (rawTrimmed.match(/^\*[^*]*expected/i)) {
|
|
723
|
+
foundFirstExpected = true;
|
|
662
724
|
inExpectedResult = true;
|
|
663
|
-
|
|
664
|
-
|
|
725
|
+
// Remove the leading * and trim
|
|
726
|
+
let content = rawTrimmed.slice(1).trim();
|
|
727
|
+
// Remove any "Expected:" prefix
|
|
728
|
+
content = content.replace(/^expected\s*:?\s*/i, '').trim();
|
|
729
|
+
const expectedContent = unescapeMarkdown(content);
|
|
730
|
+
if (expectedResult.length > 0) {
|
|
731
|
+
expectedResult += "\n" + expectedContent;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
expectedResult = expectedContent;
|
|
735
|
+
}
|
|
665
736
|
next += 1;
|
|
666
737
|
continue;
|
|
667
738
|
}
|
|
668
739
|
if (rawTrimmed.startsWith("```")) {
|
|
669
|
-
|
|
740
|
+
if (inExpectedResult) {
|
|
741
|
+
if (expectedResult.length > 0) {
|
|
742
|
+
expectedResult += "\n" + unescapeMarkdown(rawTrimmed);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
expectedResult = unescapeMarkdown(rawTrimmed);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
stepDataLines.push(unescapeMarkdown(rawTrimmed));
|
|
750
|
+
}
|
|
670
751
|
next += 1;
|
|
671
752
|
while (next < lines.length) {
|
|
672
753
|
const fenceLine = lines[next];
|
|
673
754
|
const fenceTrimmed = fenceLine.trim();
|
|
674
|
-
|
|
755
|
+
if (inExpectedResult) {
|
|
756
|
+
expectedResult += "\n" + unescapeMarkdown(fenceTrimmed);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
stepDataLines.push(unescapeMarkdown(fenceTrimmed));
|
|
760
|
+
}
|
|
675
761
|
next += 1;
|
|
676
762
|
if (fenceTrimmed.startsWith("```")) {
|
|
677
763
|
break;
|
|
@@ -680,8 +766,27 @@ function parseTestStep(lines, index, snippetId) {
|
|
|
680
766
|
continue;
|
|
681
767
|
}
|
|
682
768
|
if (inExpectedResult) {
|
|
683
|
-
|
|
684
|
-
|
|
769
|
+
// After finding the first expected result, indented lines are part of it
|
|
770
|
+
if (hasIndent) {
|
|
771
|
+
const expectedContent = unescapeMarkdown(rawTrimmed);
|
|
772
|
+
if (expectedResult.length > 0) {
|
|
773
|
+
expectedResult += "\n" + expectedContent;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
expectedResult = expectedContent;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
else if (foundFirstExpected && rawTrimmed.startsWith("*") && !rawTrimmed.startsWith("* ")) {
|
|
780
|
+
// Non-indented lines starting with single * (not list item) are likely more expected results
|
|
781
|
+
// Remove the leading * and treat the rest as content
|
|
782
|
+
const expectedContent = unescapeMarkdown(rawTrimmed.slice(1).trim());
|
|
783
|
+
if (expectedResult.length > 0) {
|
|
784
|
+
expectedResult += "\n" + expectedContent;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
expectedResult = expectedContent;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
685
790
|
next += 1;
|
|
686
791
|
continue;
|
|
687
792
|
}
|
|
@@ -691,13 +796,23 @@ function parseTestStep(lines, index, snippetId) {
|
|
|
691
796
|
next += 1;
|
|
692
797
|
continue;
|
|
693
798
|
}
|
|
799
|
+
// If we have indent and the line doesn't match other patterns, treat it as step data
|
|
800
|
+
if (hasIndent) {
|
|
801
|
+
const content = unescapeMarkdown(rawTrimmed);
|
|
802
|
+
stepDataLines.push(content);
|
|
803
|
+
next += 1;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
694
806
|
break;
|
|
695
807
|
}
|
|
696
808
|
const stepData = stepDataLines
|
|
697
809
|
.map((line) => line.trimEnd())
|
|
698
810
|
.join("\n")
|
|
699
811
|
.trim();
|
|
700
|
-
if (!isLikelyStep &&
|
|
812
|
+
if (!isLikelyStep &&
|
|
813
|
+
!expectedResult &&
|
|
814
|
+
stepDataLines.length === 0 &&
|
|
815
|
+
!(allowEmpty && titleWithPlaceholders.length > 0)) {
|
|
701
816
|
return null;
|
|
702
817
|
}
|
|
703
818
|
const stepDataWithImages = [stepData, titleImages.join("\n")]
|
|
@@ -900,10 +1015,12 @@ function parseSnippetWrapper(lines, index) {
|
|
|
900
1015
|
};
|
|
901
1016
|
}
|
|
902
1017
|
export function markdownToBlocks(markdown) {
|
|
1018
|
+
var _a, _b;
|
|
903
1019
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
904
1020
|
const lines = normalized.split("\n");
|
|
905
1021
|
const blocks = [];
|
|
906
1022
|
let index = 0;
|
|
1023
|
+
let stepsHeadingLevel = null;
|
|
907
1024
|
while (index < lines.length) {
|
|
908
1025
|
const line = lines[index];
|
|
909
1026
|
if (!line.trim()) {
|
|
@@ -916,7 +1033,7 @@ export function markdownToBlocks(markdown) {
|
|
|
916
1033
|
index = snippetWrapper.nextIndex;
|
|
917
1034
|
continue;
|
|
918
1035
|
}
|
|
919
|
-
const stepLikeBlock = parseTestStep(lines, index);
|
|
1036
|
+
const stepLikeBlock = parseTestStep(lines, index, stepsHeadingLevel !== null);
|
|
920
1037
|
if (stepLikeBlock) {
|
|
921
1038
|
blocks.push(stepLikeBlock.block);
|
|
922
1039
|
index = stepLikeBlock.nextIndex;
|
|
@@ -930,7 +1047,19 @@ export function markdownToBlocks(markdown) {
|
|
|
930
1047
|
}
|
|
931
1048
|
const heading = parseHeading(lines, index);
|
|
932
1049
|
if (heading) {
|
|
933
|
-
|
|
1050
|
+
const headingBlock = heading.block;
|
|
1051
|
+
const headingLevel = (_b = (_a = headingBlock.props) === null || _a === void 0 ? void 0 : _a.level) !== null && _b !== void 0 ? _b : 3;
|
|
1052
|
+
const headingText = inlineContentToPlainText(headingBlock.content);
|
|
1053
|
+
const normalizedHeading = headingText.trim().toLowerCase();
|
|
1054
|
+
if (normalizedHeading === "steps") {
|
|
1055
|
+
stepsHeadingLevel = headingLevel;
|
|
1056
|
+
}
|
|
1057
|
+
else if (stepsHeadingLevel !== null &&
|
|
1058
|
+
headingLevel <= stepsHeadingLevel &&
|
|
1059
|
+
normalizedHeading.length > 0) {
|
|
1060
|
+
stepsHeadingLevel = null;
|
|
1061
|
+
}
|
|
1062
|
+
blocks.push(headingBlock);
|
|
934
1063
|
index = heading.nextIndex;
|
|
935
1064
|
continue;
|
|
936
1065
|
}
|
|
@@ -948,7 +1077,7 @@ export function markdownToBlocks(markdown) {
|
|
|
948
1077
|
}
|
|
949
1078
|
const listType = detectListType(line.trim());
|
|
950
1079
|
if (listType) {
|
|
951
|
-
const { items, nextIndex } = parseList(lines, index, listType, 0);
|
|
1080
|
+
const { items, nextIndex } = parseList(lines, index, listType, 0, stepsHeadingLevel !== null);
|
|
952
1081
|
blocks.push(...items);
|
|
953
1082
|
index = nextIndex;
|
|
954
1083
|
continue;
|