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.
Files changed (41) hide show
  1. package/README.md +13 -11
  2. package/package/editor/blocks/markdown.d.ts +5 -0
  3. package/package/editor/blocks/markdown.js +160 -0
  4. package/package/editor/blocks/snippet.d.ts +38 -0
  5. package/package/editor/blocks/snippet.js +65 -0
  6. package/package/editor/blocks/step.d.ts +32 -0
  7. package/package/editor/blocks/step.js +97 -0
  8. package/package/editor/blocks/stepField.d.ts +26 -0
  9. package/package/editor/blocks/stepField.js +316 -0
  10. package/package/editor/customMarkdownConverter.js +111 -80
  11. package/package/editor/customSchema.d.ts +31 -45
  12. package/package/editor/customSchema.js +6 -616
  13. package/package/editor/snippetAutocomplete.d.ts +28 -0
  14. package/package/editor/snippetAutocomplete.js +94 -0
  15. package/package/editor/stepAutocomplete.d.ts +1 -1
  16. package/package/editor/stepAutocomplete.js +15 -2
  17. package/package/editor/stepImageUpload.d.ts +1 -1
  18. package/package/editor/stepImageUpload.js +4 -9
  19. package/package/index.d.ts +2 -2
  20. package/package/index.js +2 -2
  21. package/package/styles.css +57 -0
  22. package/package.json +1 -1
  23. package/src/App.tsx +161 -45
  24. package/src/editor/blocks/blocks.test.ts +22 -0
  25. package/src/editor/blocks/markdown.ts +199 -0
  26. package/src/editor/blocks/snippet.tsx +109 -0
  27. package/src/editor/blocks/step.tsx +175 -0
  28. package/src/editor/blocks/stepField.tsx +487 -0
  29. package/src/editor/customMarkdownConverter.test.ts +121 -36
  30. package/src/editor/customMarkdownConverter.ts +128 -85
  31. package/src/editor/customSchema.tsx +6 -935
  32. package/src/editor/snippetAutocomplete.test.ts +54 -0
  33. package/src/editor/snippetAutocomplete.ts +133 -0
  34. package/src/editor/stepAutocomplete.test.ts +20 -0
  35. package/src/editor/stepAutocomplete.tsx +15 -2
  36. package/src/editor/stepImageUpload.test.ts +25 -0
  37. package/src/editor/stepImageUpload.ts +11 -0
  38. package/src/editor/styles.css +57 -0
  39. package/src/index.ts +2 -2
  40. package/src/editor/customSchema.test.ts +0 -47
  41. 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 "testCase": {
308
- const status = (block.props as any).status ?? "draft";
309
- const reference = (block.props as any).reference;
310
- const attrs = [`status="${status}"`];
311
- if (reference) {
312
- attrs.push(`reference="${escapeMarkdown(reference)}"`);
313
- }
314
- lines.push(`:::test-case ${attrs.join(" ")}`.trimEnd());
315
- const body = inlineToMarkdown(block.content);
316
- if (body.length > 0) {
317
- lines.push(body);
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(lines: string[], index: number): { block: CustomPartialBlock; nextIndex: number } | null {
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
- const rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
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 = /^step\b/i.test(titleWithPlaceholders) || titleImages.length > 0;
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
- return {
853
- block: {
854
- type: "testStep",
855
- props: {
856
- stepTitle: titleWithPlaceholders,
857
- stepData: stepDataWithImages,
858
- expectedResult,
859
- },
860
- children: [],
861
- },
862
- nextIndex: next,
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 testCase = parseTestCase(lines, index);
1064
- if (testCase) {
1065
- blocks.push(testCase.block);
1066
- index = testCase.nextIndex;
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 testStep = parseTestStep(lines, index);
1071
- if (testStep) {
1072
- blocks.push(testStep.block);
1073
- index = testStep.nextIndex;
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