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.
@@ -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*)?(?:\s*[:\-–—]?\s*)/i;
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
- return (content as EditorInline[])
189
- .map((item) => {
190
- if (isStyledTextInlineContent(item)) {
191
- return applyTextStyles(escapeMarkdown(item.text), item.styles);
192
- }
188
+ const result: string[] = [];
189
+ let i = 0;
193
190
 
194
- if (isLinkInlineContent(item)) {
195
- const inner = inlineToMarkdown(item.content);
196
- const safeHref = escapeMarkdown(item.href);
197
- return `[${inner}](${safeHref})`;
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(`![${inner}](${safeHref})`);
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
- if (Array.isArray((item as any).content)) {
201
- return inlineToMarkdown((item as any).content);
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
- return "";
205
- })
206
- .join("");
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: parseInlineMarkdown(label),
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 bullet items (indentLevel === 0)
745
- // Nested bullets within numbered lists should remain as regular bulletListItem
746
- if (listType === "bullet" && indentLevel === 0) {
747
- const nextStep = parseTestStep(lines, index, allowEmptySteps);
748
- if (nextStep) {
749
- items.push(nextStep.block);
750
- index = nextStep.nextIndex;
751
- continue;
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
- if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
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 = unescapeMarkdown(trimmed.slice(2)).trim();
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[] {