testomatio-editor-blocks 0.1.1 → 0.2.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/README.md +13 -11
- package/package/editor/blocks/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +160 -0
- package/package/editor/blocks/snippet.d.ts +38 -0
- package/package/editor/blocks/snippet.js +65 -0
- package/package/editor/blocks/step.d.ts +32 -0
- package/package/editor/blocks/step.js +97 -0
- package/package/editor/blocks/stepField.d.ts +26 -0
- package/package/editor/blocks/stepField.js +316 -0
- package/package/editor/customMarkdownConverter.js +111 -80
- package/package/editor/customSchema.d.ts +31 -45
- package/package/editor/customSchema.js +6 -616
- package/package/editor/snippetAutocomplete.d.ts +28 -0
- package/package/editor/snippetAutocomplete.js +94 -0
- package/package/editor/stepAutocomplete.d.ts +1 -1
- package/package/editor/stepAutocomplete.js +15 -2
- package/package/editor/stepImageUpload.d.ts +1 -1
- package/package/editor/stepImageUpload.js +4 -9
- package/package/index.d.ts +2 -2
- package/package/index.js +2 -2
- package/package/styles.css +57 -0
- package/package.json +1 -1
- package/src/App.tsx +161 -45
- package/src/editor/blocks/blocks.test.ts +22 -0
- package/src/editor/blocks/markdown.ts +199 -0
- package/src/editor/blocks/snippet.tsx +109 -0
- package/src/editor/blocks/step.tsx +175 -0
- package/src/editor/blocks/stepField.tsx +487 -0
- package/src/editor/customMarkdownConverter.test.ts +121 -36
- package/src/editor/customMarkdownConverter.ts +128 -85
- package/src/editor/customSchema.tsx +6 -935
- package/src/editor/snippetAutocomplete.test.ts +54 -0
- package/src/editor/snippetAutocomplete.ts +133 -0
- package/src/editor/stepAutocomplete.test.ts +20 -0
- package/src/editor/stepAutocomplete.tsx +15 -2
- package/src/editor/stepImageUpload.test.ts +25 -0
- package/src/editor/stepImageUpload.ts +11 -0
- package/src/editor/styles.css +57 -0
- package/src/index.ts +2 -2
- package/src/editor/customSchema.test.ts +0 -47
- package/src/editor/stepImageUpload.tsx +0 -19
|
@@ -58,16 +58,6 @@ const headingPrefixes: Record<number, string> = {
|
|
|
58
58
|
6: "######",
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const STEP_STATUSES = new Set(["draft", "ready", "blocked"] as const);
|
|
62
|
-
type StepStatus = "draft" | "ready" | "blocked";
|
|
63
|
-
|
|
64
|
-
function normalizeStatus(value: string | undefined): StepStatus {
|
|
65
|
-
if (value && STEP_STATUSES.has(value as StepStatus)) {
|
|
66
|
-
return value as StepStatus;
|
|
67
|
-
}
|
|
68
|
-
return "draft";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
61
|
const SPECIAL_CHAR_REGEX = /([*_`~\[\]()<>\\])/g;
|
|
72
62
|
const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
|
|
73
63
|
const HTML_UNDERLINE_REGEX = /<\/?u>/g;
|
|
@@ -304,25 +294,38 @@ function serializeBlock(
|
|
|
304
294
|
lines.push(...serializeChildren(block, ctx));
|
|
305
295
|
return lines;
|
|
306
296
|
}
|
|
307
|
-
case "
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
297
|
+
case "testStep":
|
|
298
|
+
case "snippet": {
|
|
299
|
+
const isSnippet = block.type === "snippet";
|
|
300
|
+
const snippetId = isSnippet ? (((block.props as any).snippetId ?? "") as string).trim() : "";
|
|
301
|
+
const stepTitle = isSnippet
|
|
302
|
+
? ((block.props as any).snippetTitle ?? "").trim()
|
|
303
|
+
: ((block.props as any).stepTitle ?? "").trim();
|
|
304
|
+
const stepData = isSnippet
|
|
305
|
+
? ((block.props as any).snippetData ?? "").trim()
|
|
306
|
+
: ((block.props as any).stepData ?? "").trim();
|
|
307
|
+
const expectedResult = isSnippet
|
|
308
|
+
? ((block.props as any).snippetExpectedResult ?? "").trim()
|
|
309
|
+
: ((block.props as any).expectedResult ?? "").trim();
|
|
310
|
+
|
|
311
|
+
if (isSnippet) {
|
|
312
|
+
if (snippetId) {
|
|
313
|
+
lines.push(`<!-- begin snippet #${snippetId} -->`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const dataLines = stepData
|
|
317
|
+
.split(/\r?\n/)
|
|
318
|
+
.filter((line: string) => !/^<!--\s*(begin|end)\s+snippet/i.test(line.trim()));
|
|
319
|
+
if (dataLines.length > 0) {
|
|
320
|
+
lines.push(...dataLines);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (snippetId) {
|
|
324
|
+
lines.push(`<!-- end snippet #${snippetId} -->`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return flattenWithBlankLine(lines, true);
|
|
318
328
|
}
|
|
319
|
-
lines.push(":::");
|
|
320
|
-
return flattenWithBlankLine(lines, true);
|
|
321
|
-
}
|
|
322
|
-
case "testStep": {
|
|
323
|
-
const stepTitle = ((block.props as any).stepTitle ?? "").trim();
|
|
324
|
-
const stepData = ((block.props as any).stepData ?? "").trim();
|
|
325
|
-
const expectedResult = ((block.props as any).expectedResult ?? "").trim();
|
|
326
329
|
|
|
327
330
|
if (stepTitle.length > 0) {
|
|
328
331
|
const normalizedTitle = stepTitle
|
|
@@ -743,14 +746,24 @@ function parseList(
|
|
|
743
746
|
return { items, nextIndex: index };
|
|
744
747
|
}
|
|
745
748
|
|
|
746
|
-
function parseTestStep(
|
|
749
|
+
function parseTestStep(
|
|
750
|
+
lines: string[],
|
|
751
|
+
index: number,
|
|
752
|
+
snippetId?: string,
|
|
753
|
+
): { block: CustomPartialBlock; nextIndex: number } | null {
|
|
747
754
|
const current = lines[index];
|
|
748
755
|
const trimmed = current.trim();
|
|
749
756
|
if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
|
|
750
757
|
return null;
|
|
751
758
|
}
|
|
752
759
|
|
|
753
|
-
|
|
760
|
+
let rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
|
|
761
|
+
let blockType: "testStep" | "snippet" = "testStep";
|
|
762
|
+
const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
|
|
763
|
+
if (snippetMatch) {
|
|
764
|
+
blockType = "snippet";
|
|
765
|
+
rawTitle = snippetMatch[1].trim();
|
|
766
|
+
}
|
|
754
767
|
const titleImages: string[] = [];
|
|
755
768
|
const titleWithPlaceholders = rawTitle
|
|
756
769
|
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, (match) => {
|
|
@@ -760,7 +773,10 @@ function parseTestStep(lines: string[], index: number): { block: CustomPartialBl
|
|
|
760
773
|
.replace(/\s{2,}/g, " ")
|
|
761
774
|
.trim();
|
|
762
775
|
|
|
763
|
-
const isLikelyStep =
|
|
776
|
+
const isLikelyStep =
|
|
777
|
+
blockType === "snippet" ||
|
|
778
|
+
/^step\b/i.test(titleWithPlaceholders) ||
|
|
779
|
+
titleImages.length > 0;
|
|
764
780
|
const stepDataLines: string[] = [];
|
|
765
781
|
let expectedResult = "";
|
|
766
782
|
let next = index + 1;
|
|
@@ -849,55 +865,28 @@ function parseTestStep(lines: string[], index: number): { block: CustomPartialBl
|
|
|
849
865
|
.filter(Boolean)
|
|
850
866
|
.join(stepData ? "\n" : "");
|
|
851
867
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
868
|
+
const blockProps =
|
|
869
|
+
blockType === "snippet"
|
|
870
|
+
? {
|
|
871
|
+
snippetId: snippetId ?? "",
|
|
872
|
+
snippetTitle: titleWithPlaceholders,
|
|
873
|
+
snippetData: stepDataWithImages,
|
|
874
|
+
snippetExpectedResult: expectedResult,
|
|
875
|
+
}
|
|
876
|
+
: {
|
|
877
|
+
stepTitle: titleWithPlaceholders,
|
|
878
|
+
stepData: stepDataWithImages,
|
|
879
|
+
expectedResult,
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const parsedBlock: CustomPartialBlock = {
|
|
883
|
+
type: blockType as any,
|
|
884
|
+
props: blockProps as any,
|
|
885
|
+
children: [],
|
|
863
886
|
};
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function parseTestCase(lines: string[], index: number): { block: CustomPartialBlock; nextIndex: number } | null {
|
|
867
|
-
const trimmed = lines[index].trim();
|
|
868
|
-
if (!trimmed.startsWith(":::test-case")) {
|
|
869
|
-
return null;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const statusMatch = trimmed.match(/status="([^"]*)"/);
|
|
873
|
-
const referenceMatch = trimmed.match(/reference="([^"]*)"/);
|
|
874
|
-
|
|
875
|
-
let bodyLines: string[] = [];
|
|
876
|
-
let next = index + 1;
|
|
877
|
-
while (next < lines.length && lines[next].trim() !== ":::") {
|
|
878
|
-
bodyLines.push(lines[next]);
|
|
879
|
-
next += 1;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
if (next < lines.length && lines[next].trim() === ":::") {
|
|
883
|
-
next += 1;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const contentText = bodyLines.join("\n").trim();
|
|
887
887
|
|
|
888
888
|
return {
|
|
889
|
-
block:
|
|
890
|
-
type: "testCase",
|
|
891
|
-
props: {
|
|
892
|
-
...cloneBaseProps(),
|
|
893
|
-
status: normalizeStatus(statusMatch?.[1]),
|
|
894
|
-
reference: referenceMatch?.[1] ?? "",
|
|
895
|
-
},
|
|
896
|
-
content: contentText
|
|
897
|
-
? [{ type: "text", text: unescapeMarkdown(contentText), styles: {} }]
|
|
898
|
-
: undefined,
|
|
899
|
-
children: [],
|
|
900
|
-
},
|
|
889
|
+
block: parsedBlock,
|
|
901
890
|
nextIndex: next,
|
|
902
891
|
};
|
|
903
892
|
}
|
|
@@ -1047,6 +1036,60 @@ function parseParagraph(lines: string[], index: number): { block: CustomPartialB
|
|
|
1047
1036
|
};
|
|
1048
1037
|
}
|
|
1049
1038
|
|
|
1039
|
+
function parseSnippetWrapper(
|
|
1040
|
+
lines: string[],
|
|
1041
|
+
index: number,
|
|
1042
|
+
): { block: CustomPartialBlock; nextIndex: number } | null {
|
|
1043
|
+
const trimmed = lines[index].trim();
|
|
1044
|
+
const startMatch = trimmed.match(/^<!--\s*begin snippet\s*#?([^\s>]+)\s*-->/i);
|
|
1045
|
+
if (!startMatch) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const snippetId = startMatch[1];
|
|
1050
|
+
const innerLines: string[] = [];
|
|
1051
|
+
let next = index + 1;
|
|
1052
|
+
|
|
1053
|
+
while (next < lines.length) {
|
|
1054
|
+
const maybeEnd = lines[next].trim();
|
|
1055
|
+
const endMatch = maybeEnd.match(/^<!--\s*end snippet\s*#?([^\s>]+)?\s*-->/i);
|
|
1056
|
+
if (endMatch) {
|
|
1057
|
+
const endId = endMatch[1];
|
|
1058
|
+
if (!endId || endId === snippetId) {
|
|
1059
|
+
next += 1;
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
// Ignore unrelated snippet end markers but keep scanning.
|
|
1063
|
+
next += 1;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const otherStart = maybeEnd.match(/^<!--\s*begin snippet\s*#?([^\s>]+)\s*-->/i);
|
|
1067
|
+
if (otherStart) {
|
|
1068
|
+
// Skip nested snippet wrappers from the body entirely.
|
|
1069
|
+
next += 1;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
innerLines.push(lines[next]);
|
|
1073
|
+
next += 1;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const snippetBlock: CustomPartialBlock = {
|
|
1077
|
+
type: "snippet",
|
|
1078
|
+
props: {
|
|
1079
|
+
snippetId,
|
|
1080
|
+
snippetTitle: "",
|
|
1081
|
+
snippetData: innerLines.join("\n").trim(),
|
|
1082
|
+
snippetExpectedResult: "",
|
|
1083
|
+
},
|
|
1084
|
+
children: [],
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
block: snippetBlock,
|
|
1089
|
+
nextIndex: next,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1050
1093
|
export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
1051
1094
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1052
1095
|
const lines = normalized.split("\n");
|
|
@@ -1060,17 +1103,17 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1060
1103
|
continue;
|
|
1061
1104
|
}
|
|
1062
1105
|
|
|
1063
|
-
const
|
|
1064
|
-
if (
|
|
1065
|
-
blocks.push(
|
|
1066
|
-
index =
|
|
1106
|
+
const snippetWrapper = parseSnippetWrapper(lines, index);
|
|
1107
|
+
if (snippetWrapper) {
|
|
1108
|
+
blocks.push(snippetWrapper.block);
|
|
1109
|
+
index = snippetWrapper.nextIndex;
|
|
1067
1110
|
continue;
|
|
1068
1111
|
}
|
|
1069
1112
|
|
|
1070
|
-
const
|
|
1071
|
-
if (
|
|
1072
|
-
blocks.push(
|
|
1073
|
-
index =
|
|
1113
|
+
const stepLikeBlock = parseTestStep(lines, index);
|
|
1114
|
+
if (stepLikeBlock) {
|
|
1115
|
+
blocks.push(stepLikeBlock.block);
|
|
1116
|
+
index = stepLikeBlock.nextIndex;
|
|
1074
1117
|
continue;
|
|
1075
1118
|
}
|
|
1076
1119
|
|