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.
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ type StepHorizontalViewProps = {
3
+ blockId: string;
4
+ stepNumber: number;
5
+ stepValue: string;
6
+ expectedResult: string;
7
+ onStepChange: (next: string) => void;
8
+ onExpectedChange: (next: string) => void;
9
+ onInsertNextStep: () => void;
10
+ onFieldFocus: () => void;
11
+ viewToggle?: ReactNode;
12
+ };
13
+ export declare function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }: StepHorizontalViewProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StepField } from "./stepField";
3
+ const STEP_PLACEHOLDER = "Enter step name";
4
+ const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result";
5
+ export function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }) {
6
+ return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsxs("div", { className: "bn-teststep__horizontal-col", children: [_jsx("div", { className: "bn-teststep__header", children: _jsx("span", { className: "bn-teststep__title", children: "Step" }) }), _jsx(StepField, { label: "Step", showLabel: false, value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true })] }), _jsxs("div", { className: "bn-teststep__horizontal-col", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Expected result" }), viewToggle] }), _jsx(StepField, { label: "Expected result", showLabel: false, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus })] })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
7
+ }
@@ -4,5 +4,6 @@ type Schema = typeof customSchema;
4
4
  export type CustomEditorBlock = Block<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>;
5
5
  export type CustomPartialBlock = PartialBlock<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>;
6
6
  export declare function blocksToMarkdown(blocks: CustomEditorBlock[]): string;
7
+ export declare function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPartialBlock[];
7
8
  export declare function markdownToBlocks(markdown: string): CustomPartialBlock[];
8
9
  export {};
@@ -23,7 +23,7 @@ const headingPrefixes = {
23
23
  const SPECIAL_CHAR_REGEX = /([*_`~\[\]()<>\\])/g;
24
24
  const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
25
25
  const HTML_UNDERLINE_REGEX = /<\/?u>/g;
26
- const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?(?:\s*[:\-–—]?\s*)/i;
26
+ const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?\s*[:\-–—]\s*/i;
27
27
  // Matches any non-empty line that falls between the step title and the expected result line.
28
28
  const STEP_DATA_LINE_REGEX = /^(?!\s*(?:[*_`]*\s*)?(?:expected(?:\s+result)?)\b).+/i;
29
29
  const NUMBERED_STEP_REGEX = /^\d+[.)]\s+/;
@@ -124,22 +124,39 @@ function inlineToMarkdown(content) {
124
124
  if (!content || !Array.isArray(content)) {
125
125
  return "";
126
126
  }
127
- return content
128
- .map((item) => {
127
+ const result = [];
128
+ let i = 0;
129
+ while (i < content.length) {
130
+ const item = content[i];
129
131
  if (isStyledTextInlineContent(item)) {
130
- return applyTextStyles(escapeMarkdown(item.text), item.styles);
132
+ // Check if this is a "!" followed by a link (image syntax)
133
+ if (item.text === "!" && i + 1 < content.length && isLinkInlineContent(content[i + 1])) {
134
+ const link = content[i + 1];
135
+ const inner = inlineToMarkdown(link.content);
136
+ const safeHref = escapeMarkdown(link.href);
137
+ result.push(`![${inner}](${safeHref})`);
138
+ i += 2;
139
+ continue;
140
+ }
141
+ result.push(applyTextStyles(escapeMarkdown(item.text), item.styles));
142
+ i += 1;
143
+ continue;
131
144
  }
132
145
  if (isLinkInlineContent(item)) {
133
146
  const inner = inlineToMarkdown(item.content);
134
147
  const safeHref = escapeMarkdown(item.href);
135
- return `[${inner}](${safeHref})`;
148
+ result.push(`[${inner}](${safeHref})`);
149
+ i += 1;
150
+ continue;
136
151
  }
137
152
  if (Array.isArray(item.content)) {
138
- return inlineToMarkdown(item.content);
153
+ result.push(inlineToMarkdown(item.content));
154
+ i += 1;
155
+ continue;
139
156
  }
140
- return "";
141
- })
142
- .join("");
157
+ i += 1;
158
+ }
159
+ return result.join("");
143
160
  }
144
161
  function inlineContentToPlainText(content) {
145
162
  if (!Array.isArray(content)) {
@@ -497,10 +514,13 @@ function parseInlineMarkdown(text) {
497
514
  pushPlain();
498
515
  const label = cleaned.slice(i + 1, endLabel);
499
516
  const href = cleaned.slice(startLink + 1, endLink);
517
+ const parsedLabel = parseInlineMarkdown(label);
518
+ // Ensure link content is never undefined - if empty, add empty text
519
+ const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
500
520
  result.push({
501
521
  type: "link",
502
522
  href: unescapeMarkdown(href),
503
- content: parseInlineMarkdown(label),
523
+ content: linkContent,
504
524
  });
505
525
  i = endLink + 1;
506
526
  continue;
@@ -593,14 +613,21 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
593
613
  if (detectedType !== listType) {
594
614
  break;
595
615
  }
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;
616
+ // Only try to parse as testStep for top-level items (indentLevel === 0)
617
+ // when we're under a Steps heading AND the list type is bullet
618
+ // Numbered lists under Steps heading are only parsed as test steps if they look like test steps
619
+ if (indentLevel === 0 && allowEmptySteps) {
620
+ // For bullet lists, always try to parse as test steps
621
+ // For numbered lists, only try if they have step-like characteristics
622
+ const looksLikeTestStep = listType === "bullet" ||
623
+ (listType === "numbered" && (isLikelyStep(lines, index)));
624
+ if (looksLikeTestStep) {
625
+ const nextStep = parseTestStep(lines, index, allowEmptySteps);
626
+ if (nextStep) {
627
+ items.push(nextStep.block);
628
+ index = nextStep.nextIndex;
629
+ continue;
630
+ }
604
631
  }
605
632
  }
606
633
  if (listType === "check") {
@@ -639,13 +666,46 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
639
666
  }
640
667
  return { items, nextIndex: index };
641
668
  }
669
+ function isLikelyStep(lines, index) {
670
+ // Look ahead to see if there's indented content or expected result
671
+ if (index + 1 >= lines.length)
672
+ return false;
673
+ const nextLine = lines[index + 1];
674
+ const hasIndent = /^\s{2,}/.test(nextLine);
675
+ // Check if the next line contains expected result markers
676
+ const nextTrimmed = nextLine.trim();
677
+ const hasExpectedResult = EXPECTED_LABEL_REGEX.test(nextTrimmed);
678
+ // Only consider it a test step if:
679
+ // 1. It has an expected result, OR
680
+ // 2. The next line is indented but doesn't start with a numbered or bullet list
681
+ if (hasExpectedResult)
682
+ return true;
683
+ if (hasIndent && !/^\d+[.)]/.test(nextTrimmed) && !/^[-*+]/.test(nextTrimmed))
684
+ return true;
685
+ return false;
686
+ }
642
687
  function parseTestStep(lines, index, allowEmpty = false, snippetId) {
643
688
  const current = lines[index];
644
689
  const trimmed = current.trim();
645
- if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
690
+ const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
691
+ const isNumbered = /^\d+[.)]\s+/.test(trimmed);
692
+ if (!isBullet && !isNumbered) {
693
+ return null;
694
+ }
695
+ // For numbered lists, only parse as test steps if called from parseList with allowEmpty=true
696
+ // The first call to parseTestStep from markdownToBlocks uses allowEmpty=stepsHeadingLevel !== null
697
+ // which should be false unless we're under a Steps heading
698
+ if (isNumbered && !allowEmpty) {
646
699
  return null;
647
700
  }
648
- let rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
701
+ let rawTitle;
702
+ if (isBullet) {
703
+ rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
704
+ }
705
+ else {
706
+ // For numbered lists, remove the number and delimiter
707
+ rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
708
+ }
649
709
  let blockType = "testStep";
650
710
  const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
651
711
  if (snippetMatch) {
@@ -667,7 +727,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
667
727
  let expectedResult = "";
668
728
  let next = index + 1;
669
729
  let inExpectedResult = false;
670
- let foundFirstExpected = false;
671
730
  while (next < lines.length) {
672
731
  const line = lines[next];
673
732
  const hasIndent = /^\s{2,}/.test(line);
@@ -701,7 +760,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
701
760
  const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
702
761
  rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
703
762
  if (expectedMatch || expectedStarMatch) {
704
- foundFirstExpected = true;
705
763
  inExpectedResult = true;
706
764
  const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
707
765
  let content = rawTrimmed.slice(label.length).trim();
@@ -720,7 +778,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
720
778
  }
721
779
  // Check for lines that start with * and contain Expected (but don't match the above patterns)
722
780
  if (rawTrimmed.match(/^\*[^*]*expected/i)) {
723
- foundFirstExpected = true;
724
781
  inExpectedResult = true;
725
782
  // Remove the leading * and trim
726
783
  let content = rawTrimmed.slice(1).trim();
@@ -776,17 +833,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
776
833
  expectedResult = expectedContent;
777
834
  }
778
835
  }
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
- }
790
836
  next += 1;
791
837
  continue;
792
838
  }
@@ -1014,6 +1060,60 @@ function parseSnippetWrapper(lines, index) {
1014
1060
  nextIndex: next,
1015
1061
  };
1016
1062
  }
1063
+ // Post-process blocks to fix malformed image blocks
1064
+ export function fixMalformedImageBlocks(blocks) {
1065
+ var _a, _b, _c, _d;
1066
+ const result = [];
1067
+ let i = 0;
1068
+ while (i < blocks.length) {
1069
+ const current = blocks[i];
1070
+ const next = blocks[i + 1];
1071
+ // Skip empty paragraphs
1072
+ if (current.type === "paragraph" &&
1073
+ (!current.content || !Array.isArray(current.content) || current.content.length === 0)) {
1074
+ i += 1;
1075
+ continue;
1076
+ }
1077
+ // Check if current is a paragraph with just "!" - this is definitely a malformed image
1078
+ if (current.type === "paragraph" &&
1079
+ current.content &&
1080
+ Array.isArray(current.content) &&
1081
+ current.content.length === 1 &&
1082
+ ((_a = current.content[0]) === null || _a === void 0 ? void 0 : _a.type) === "text" &&
1083
+ ((_b = current.content[0]) === null || _b === void 0 ? void 0 : _b.text) === "!") {
1084
+ // This is a malformed image block, skip it entirely
1085
+ // The full image was likely parsed as ![](...) but got corrupted
1086
+ i += 1;
1087
+ continue;
1088
+ }
1089
+ // Check if current is a paragraph with just "!" and next is an empty paragraph
1090
+ if (current.type === "paragraph" &&
1091
+ (next === null || next === void 0 ? void 0 : next.type) === "paragraph" &&
1092
+ current.content &&
1093
+ Array.isArray(current.content) &&
1094
+ current.content.length === 1 &&
1095
+ ((_c = current.content[0]) === null || _c === void 0 ? void 0 : _c.type) === "text" &&
1096
+ ((_d = current.content[0]) === null || _d === void 0 ? void 0 : _d.text) === "!" &&
1097
+ (!next.content || !Array.isArray(next.content) || next.content.length === 0)) {
1098
+ // This looks like a malformed image, skip both blocks
1099
+ i += 2;
1100
+ continue;
1101
+ }
1102
+ // Check if current has "!" but no link
1103
+ if (current.type === "paragraph" &&
1104
+ current.content &&
1105
+ Array.isArray(current.content) &&
1106
+ current.content.some((item) => item.type === "text" && item.text === "!") &&
1107
+ !current.content.some((item) => item.type === "link")) {
1108
+ // Skip malformed image block
1109
+ i += 1;
1110
+ continue;
1111
+ }
1112
+ result.push(current);
1113
+ i += 1;
1114
+ }
1115
+ return result;
1116
+ }
1017
1117
  export function markdownToBlocks(markdown) {
1018
1118
  var _a, _b;
1019
1119
  const normalized = markdown.replace(/\r\n/g, "\n");
@@ -1051,7 +1151,7 @@ export function markdownToBlocks(markdown) {
1051
1151
  const headingLevel = (_b = (_a = headingBlock.props) === null || _a === void 0 ? void 0 : _a.level) !== null && _b !== void 0 ? _b : 3;
1052
1152
  const headingText = inlineContentToPlainText(headingBlock.content);
1053
1153
  const normalizedHeading = headingText.trim().toLowerCase();
1054
- if (normalizedHeading === "steps") {
1154
+ if (normalizedHeading.replace(/[:\-–—]$/, "") === "steps") {
1055
1155
  stepsHeadingLevel = headingLevel;
1056
1156
  }
1057
1157
  else if (stepsHeadingLevel !== null &&
@@ -1086,7 +1186,7 @@ export function markdownToBlocks(markdown) {
1086
1186
  blocks.push(paragraph.block);
1087
1187
  index = paragraph.nextIndex;
1088
1188
  }
1089
- return blocks;
1189
+ return fixMalformedImageBlocks(blocks);
1090
1190
  }
1091
1191
  function splitTableRow(line) {
1092
1192
  let value = line.trim();