testomatio-editor-blocks 0.4.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 +32 -138
- package/package/editor/blocks/step.js +116 -52
- package/package/editor/blocks/stepField.d.ts +4 -1
- package/package/editor/blocks/stepField.js +651 -39
- package/package/editor/blocks/stepHorizontalView.d.ts +14 -0
- package/package/editor/blocks/stepHorizontalView.js +7 -0
- package/package/editor/customMarkdownConverter.d.ts +1 -0
- package/package/editor/customMarkdownConverter.js +136 -36
- package/package/styles.css +565 -122
- package/package.json +5 -1
- package/src/editor/blocks/snippet.tsx +92 -212
- package/src/editor/blocks/step.tsx +265 -123
- package/src/editor/blocks/stepField.tsx +907 -95
- package/src/editor/blocks/stepHorizontalView.tsx +90 -0
- package/src/editor/customMarkdownConverter.test.ts +443 -1
- package/src/editor/customMarkdownConverter.ts +168 -41
- package/src/editor/styles.css +561 -133
|
@@ -61,7 +61,7 @@ const headingPrefixes: Record<number, string> = {
|
|
|
61
61
|
const SPECIAL_CHAR_REGEX = /([*_`~\[\]()<>\\])/g;
|
|
62
62
|
const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
|
|
63
63
|
const HTML_UNDERLINE_REGEX = /<\/?u>/g;
|
|
64
|
-
const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)
|
|
64
|
+
const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?\s*[:\-–—]\s*/i;
|
|
65
65
|
// Matches any non-empty line that falls between the step title and the expected result line.
|
|
66
66
|
const STEP_DATA_LINE_REGEX =
|
|
67
67
|
/^(?!\s*(?:[*_`]*\s*)?(?:expected(?:\s+result)?)\b).+/i;
|
|
@@ -185,25 +185,45 @@ function inlineToMarkdown(content: CustomEditorBlock["content"]): string {
|
|
|
185
185
|
return "";
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (isStyledTextInlineContent(item)) {
|
|
191
|
-
return applyTextStyles(escapeMarkdown(item.text), item.styles);
|
|
192
|
-
}
|
|
188
|
+
const result: string[] = [];
|
|
189
|
+
let i = 0;
|
|
193
190
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
while (i < content.length) {
|
|
192
|
+
const item = content[i];
|
|
193
|
+
|
|
194
|
+
if (isStyledTextInlineContent(item)) {
|
|
195
|
+
// Check if this is a "!" followed by a link (image syntax)
|
|
196
|
+
if (item.text === "!" && i + 1 < content.length && isLinkInlineContent(content[i + 1])) {
|
|
197
|
+
const link = content[i + 1] as any;
|
|
198
|
+
const inner = inlineToMarkdown(link.content);
|
|
199
|
+
const safeHref = escapeMarkdown(link.href);
|
|
200
|
+
result.push(``);
|
|
201
|
+
i += 2;
|
|
202
|
+
continue;
|
|
198
203
|
}
|
|
204
|
+
result.push(applyTextStyles(escapeMarkdown(item.text), item.styles));
|
|
205
|
+
i += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
199
208
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
209
|
+
if (isLinkInlineContent(item)) {
|
|
210
|
+
const inner = inlineToMarkdown(item.content);
|
|
211
|
+
const safeHref = escapeMarkdown(item.href);
|
|
212
|
+
result.push(`[${inner}](${safeHref})`);
|
|
213
|
+
i += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
203
216
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
if (Array.isArray((item as any).content)) {
|
|
218
|
+
result.push(inlineToMarkdown((item as any).content));
|
|
219
|
+
i += 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
i += 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result.join("");
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
function inlineContentToPlainText(content: CustomEditorBlock["content"]): string {
|
|
@@ -613,10 +633,13 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
613
633
|
pushPlain();
|
|
614
634
|
const label = cleaned.slice(i + 1, endLabel);
|
|
615
635
|
const href = cleaned.slice(startLink + 1, endLink);
|
|
636
|
+
const parsedLabel = parseInlineMarkdown(label);
|
|
637
|
+
// Ensure link content is never undefined - if empty, add empty text
|
|
638
|
+
const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
|
|
616
639
|
result.push({
|
|
617
640
|
type: "link",
|
|
618
641
|
href: unescapeMarkdown(href),
|
|
619
|
-
content:
|
|
642
|
+
content: linkContent,
|
|
620
643
|
} as any);
|
|
621
644
|
i = endLink + 1;
|
|
622
645
|
continue;
|
|
@@ -741,14 +764,24 @@ function parseList(
|
|
|
741
764
|
break;
|
|
742
765
|
}
|
|
743
766
|
|
|
744
|
-
// Only try to parse as testStep for top-level
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
767
|
+
// Only try to parse as testStep for top-level items (indentLevel === 0)
|
|
768
|
+
// when we're under a Steps heading AND the list type is bullet
|
|
769
|
+
// Numbered lists under Steps heading are only parsed as test steps if they look like test steps
|
|
770
|
+
if (indentLevel === 0 && allowEmptySteps) {
|
|
771
|
+
// For bullet lists, always try to parse as test steps
|
|
772
|
+
// For numbered lists, only try if they have step-like characteristics
|
|
773
|
+
const looksLikeTestStep = listType === "bullet" ||
|
|
774
|
+
(listType === "numbered" && (
|
|
775
|
+
isLikelyStep(lines, index)
|
|
776
|
+
));
|
|
777
|
+
|
|
778
|
+
if (looksLikeTestStep) {
|
|
779
|
+
const nextStep = parseTestStep(lines, index, allowEmptySteps);
|
|
780
|
+
if (nextStep) {
|
|
781
|
+
items.push(nextStep.block);
|
|
782
|
+
index = nextStep.nextIndex;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
752
785
|
}
|
|
753
786
|
}
|
|
754
787
|
|
|
@@ -789,6 +822,26 @@ function parseList(
|
|
|
789
822
|
return { items, nextIndex: index };
|
|
790
823
|
}
|
|
791
824
|
|
|
825
|
+
function isLikelyStep(lines: string[], index: number): boolean {
|
|
826
|
+
// Look ahead to see if there's indented content or expected result
|
|
827
|
+
if (index + 1 >= lines.length) return false;
|
|
828
|
+
|
|
829
|
+
const nextLine = lines[index + 1];
|
|
830
|
+
const hasIndent = /^\s{2,}/.test(nextLine);
|
|
831
|
+
|
|
832
|
+
// Check if the next line contains expected result markers
|
|
833
|
+
const nextTrimmed = nextLine.trim();
|
|
834
|
+
const hasExpectedResult = EXPECTED_LABEL_REGEX.test(nextTrimmed);
|
|
835
|
+
|
|
836
|
+
// Only consider it a test step if:
|
|
837
|
+
// 1. It has an expected result, OR
|
|
838
|
+
// 2. The next line is indented but doesn't start with a numbered or bullet list
|
|
839
|
+
if (hasExpectedResult) return true;
|
|
840
|
+
if (hasIndent && !/^\d+[.)]/.test(nextTrimmed) && !/^[-*+]/.test(nextTrimmed)) return true;
|
|
841
|
+
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
|
|
792
845
|
function parseTestStep(
|
|
793
846
|
lines: string[],
|
|
794
847
|
index: number,
|
|
@@ -797,11 +850,28 @@ function parseTestStep(
|
|
|
797
850
|
): { block: CustomPartialBlock; nextIndex: number } | null {
|
|
798
851
|
const current = lines[index];
|
|
799
852
|
const trimmed = current.trim();
|
|
800
|
-
|
|
853
|
+
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
|
|
854
|
+
const isNumbered = /^\d+[.)]\s+/.test(trimmed);
|
|
855
|
+
|
|
856
|
+
if (!isBullet && !isNumbered) {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// For numbered lists, only parse as test steps if called from parseList with allowEmpty=true
|
|
861
|
+
// The first call to parseTestStep from markdownToBlocks uses allowEmpty=stepsHeadingLevel !== null
|
|
862
|
+
// which should be false unless we're under a Steps heading
|
|
863
|
+
if (isNumbered && !allowEmpty) {
|
|
801
864
|
return null;
|
|
802
865
|
}
|
|
803
866
|
|
|
804
|
-
let rawTitle
|
|
867
|
+
let rawTitle: string;
|
|
868
|
+
if (isBullet) {
|
|
869
|
+
rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
|
|
870
|
+
} else {
|
|
871
|
+
// For numbered lists, remove the number and delimiter
|
|
872
|
+
rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
|
|
873
|
+
}
|
|
874
|
+
|
|
805
875
|
let blockType: "testStep" | "snippet" = "testStep";
|
|
806
876
|
const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
|
|
807
877
|
if (snippetMatch) {
|
|
@@ -825,7 +895,6 @@ function parseTestStep(
|
|
|
825
895
|
let expectedResult = "";
|
|
826
896
|
let next = index + 1;
|
|
827
897
|
let inExpectedResult = false;
|
|
828
|
-
let foundFirstExpected = false;
|
|
829
898
|
|
|
830
899
|
while (next < lines.length) {
|
|
831
900
|
const line = lines[next];
|
|
@@ -867,7 +936,6 @@ function parseTestStep(
|
|
|
867
936
|
rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
|
|
868
937
|
|
|
869
938
|
if (expectedMatch || expectedStarMatch) {
|
|
870
|
-
foundFirstExpected = true;
|
|
871
939
|
inExpectedResult = true;
|
|
872
940
|
const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
|
|
873
941
|
let content = rawTrimmed.slice(label.length).trim();
|
|
@@ -887,7 +955,6 @@ function parseTestStep(
|
|
|
887
955
|
|
|
888
956
|
// Check for lines that start with * and contain Expected (but don't match the above patterns)
|
|
889
957
|
if (rawTrimmed.match(/^\*[^*]*expected/i)) {
|
|
890
|
-
foundFirstExpected = true;
|
|
891
958
|
inExpectedResult = true;
|
|
892
959
|
// Remove the leading * and trim
|
|
893
960
|
let content = rawTrimmed.slice(1).trim();
|
|
@@ -940,15 +1007,6 @@ function parseTestStep(
|
|
|
940
1007
|
} else {
|
|
941
1008
|
expectedResult = expectedContent;
|
|
942
1009
|
}
|
|
943
|
-
} else if (foundFirstExpected && rawTrimmed.startsWith("*") && !rawTrimmed.startsWith("* ")) {
|
|
944
|
-
// Non-indented lines starting with single * (not list item) are likely more expected results
|
|
945
|
-
// Remove the leading * and treat the rest as content
|
|
946
|
-
const expectedContent = unescapeMarkdown(rawTrimmed.slice(1).trim());
|
|
947
|
-
if (expectedResult.length > 0) {
|
|
948
|
-
expectedResult += "\n" + expectedContent;
|
|
949
|
-
} else {
|
|
950
|
-
expectedResult = expectedContent;
|
|
951
|
-
}
|
|
952
1010
|
}
|
|
953
1011
|
next += 1;
|
|
954
1012
|
continue;
|
|
@@ -1215,6 +1273,75 @@ function parseSnippetWrapper(
|
|
|
1215
1273
|
};
|
|
1216
1274
|
}
|
|
1217
1275
|
|
|
1276
|
+
// Post-process blocks to fix malformed image blocks
|
|
1277
|
+
export function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPartialBlock[] {
|
|
1278
|
+
const result: CustomPartialBlock[] = [];
|
|
1279
|
+
let i = 0;
|
|
1280
|
+
|
|
1281
|
+
while (i < blocks.length) {
|
|
1282
|
+
const current = blocks[i];
|
|
1283
|
+
const next = blocks[i + 1];
|
|
1284
|
+
|
|
1285
|
+
// Skip empty paragraphs
|
|
1286
|
+
if (
|
|
1287
|
+
current.type === "paragraph" &&
|
|
1288
|
+
(!current.content || !Array.isArray(current.content) || current.content.length === 0)
|
|
1289
|
+
) {
|
|
1290
|
+
i += 1;
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Check if current is a paragraph with just "!" - this is definitely a malformed image
|
|
1295
|
+
if (
|
|
1296
|
+
current.type === "paragraph" &&
|
|
1297
|
+
current.content &&
|
|
1298
|
+
Array.isArray(current.content) &&
|
|
1299
|
+
current.content.length === 1 &&
|
|
1300
|
+
(current.content[0] as any)?.type === "text" &&
|
|
1301
|
+
(current.content[0] as any)?.text === "!"
|
|
1302
|
+
) {
|
|
1303
|
+
// This is a malformed image block, skip it entirely
|
|
1304
|
+
// The full image was likely parsed as  but got corrupted
|
|
1305
|
+
i += 1;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Check if current is a paragraph with just "!" and next is an empty paragraph
|
|
1310
|
+
if (
|
|
1311
|
+
current.type === "paragraph" &&
|
|
1312
|
+
next?.type === "paragraph" &&
|
|
1313
|
+
current.content &&
|
|
1314
|
+
Array.isArray(current.content) &&
|
|
1315
|
+
current.content.length === 1 &&
|
|
1316
|
+
(current.content[0] as any)?.type === "text" &&
|
|
1317
|
+
(current.content[0] as any)?.text === "!" &&
|
|
1318
|
+
(!next.content || !Array.isArray(next.content) || next.content.length === 0)
|
|
1319
|
+
) {
|
|
1320
|
+
// This looks like a malformed image, skip both blocks
|
|
1321
|
+
i += 2;
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Check if current has "!" but no link
|
|
1326
|
+
if (
|
|
1327
|
+
current.type === "paragraph" &&
|
|
1328
|
+
current.content &&
|
|
1329
|
+
Array.isArray(current.content) &&
|
|
1330
|
+
current.content.some((item: any) => item.type === "text" && item.text === "!") &&
|
|
1331
|
+
!current.content.some((item: any) => item.type === "link")
|
|
1332
|
+
) {
|
|
1333
|
+
// Skip malformed image block
|
|
1334
|
+
i += 1;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
result.push(current);
|
|
1339
|
+
i += 1;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
return result;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1218
1345
|
export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
1219
1346
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1220
1347
|
const lines = normalized.split("\n");
|
|
@@ -1257,7 +1384,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1257
1384
|
const headingText = inlineContentToPlainText(headingBlock.content as any);
|
|
1258
1385
|
const normalizedHeading = headingText.trim().toLowerCase();
|
|
1259
1386
|
|
|
1260
|
-
if (normalizedHeading === "steps") {
|
|
1387
|
+
if (normalizedHeading.replace(/[:\-–—]$/, "") === "steps") {
|
|
1261
1388
|
stepsHeadingLevel = headingLevel;
|
|
1262
1389
|
} else if (
|
|
1263
1390
|
stepsHeadingLevel !== null &&
|
|
@@ -1305,7 +1432,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1305
1432
|
index = paragraph.nextIndex;
|
|
1306
1433
|
}
|
|
1307
1434
|
|
|
1308
|
-
return blocks;
|
|
1435
|
+
return fixMalformedImageBlocks(blocks);
|
|
1309
1436
|
}
|
|
1310
1437
|
|
|
1311
1438
|
function splitTableRow(line: string): string[] {
|