testomatio-editor-blocks 0.4.0 → 0.4.6

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)) {
@@ -173,8 +190,8 @@ function flattenWithBlankLine(lines, appendBlank = false) {
173
190
  }
174
191
  return lines;
175
192
  }
176
- function serializeBlock(block, ctx, orderedIndex) {
177
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
193
+ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
194
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
178
195
  const lines = [];
179
196
  const indent = ctx.listDepth > 0 ? " ".repeat(ctx.listDepth) : "";
180
197
  switch (block.type) {
@@ -275,7 +292,9 @@ function serializeBlock(block, ctx, orderedIndex) {
275
292
  .filter((segment) => segment.length > 0)
276
293
  .join(" ");
277
294
  if (normalizedTitle.length > 0) {
278
- lines.push(`* ${normalizedTitle}`);
295
+ const listStyle = (_m = block.props.listStyle) !== null && _m !== void 0 ? _m : "bullet";
296
+ const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
297
+ lines.push(`${prefix} ${normalizedTitle}`);
279
298
  }
280
299
  }
281
300
  if (stepData.length > 0) {
@@ -331,7 +350,7 @@ function serializeBlock(block, ctx, orderedIndex) {
331
350
  return flattenWithBlankLine(lines, true);
332
351
  }
333
352
  const headerRowCount = rows.length
334
- ? Math.min(rows.length, Math.max((_m = tableContent.headerRows) !== null && _m !== void 0 ? _m : 1, 1))
353
+ ? Math.min(rows.length, Math.max((_o = tableContent.headerRows) !== null && _o !== void 0 ? _o : 1, 1))
335
354
  : 0;
336
355
  const columnAlignments = new Array(columnCount).fill("left");
337
356
  const getCellAlignment = (cell) => {
@@ -408,6 +427,7 @@ function serializeBlock(block, ctx, orderedIndex) {
408
427
  function serializeBlocks(blocks, ctx) {
409
428
  const lines = [];
410
429
  let orderedIndex = null;
430
+ let stepIndex = 0;
411
431
  for (const block of blocks) {
412
432
  if (block.type === "numberedListItem") {
413
433
  if (typeof block.props.start === "number") {
@@ -420,6 +440,13 @@ function serializeBlocks(blocks, ctx) {
420
440
  orderedIndex += 1;
421
441
  continue;
422
442
  }
443
+ if (block.type === "testStep") {
444
+ lines.push(...serializeBlock(block, ctx, undefined, stepIndex));
445
+ stepIndex += 1;
446
+ orderedIndex = null;
447
+ continue;
448
+ }
449
+ stepIndex = 0;
423
450
  orderedIndex = null;
424
451
  lines.push(...serializeBlock(block, ctx));
425
452
  }
@@ -497,10 +524,13 @@ function parseInlineMarkdown(text) {
497
524
  pushPlain();
498
525
  const label = cleaned.slice(i + 1, endLabel);
499
526
  const href = cleaned.slice(startLink + 1, endLink);
527
+ const parsedLabel = parseInlineMarkdown(label);
528
+ // Ensure link content is never undefined - if empty, add empty text
529
+ const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
500
530
  result.push({
501
531
  type: "link",
502
532
  href: unescapeMarkdown(href),
503
- content: parseInlineMarkdown(label),
533
+ content: linkContent,
504
534
  });
505
535
  i = endLink + 1;
506
536
  continue;
@@ -593,14 +623,21 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
593
623
  if (detectedType !== listType) {
594
624
  break;
595
625
  }
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;
626
+ // Only try to parse as testStep for top-level items (indentLevel === 0)
627
+ // when we're under a Steps heading AND the list type is bullet
628
+ // Numbered lists under Steps heading are only parsed as test steps if they look like test steps
629
+ if (indentLevel === 0 && (allowEmptySteps || listType === "bullet")) {
630
+ // For bullet lists, always try to parse as test steps
631
+ // For numbered lists, only try if they have step-like characteristics
632
+ const looksLikeTestStep = listType === "bullet" ||
633
+ (listType === "numbered" && (isLikelyStep(lines, index)));
634
+ if (looksLikeTestStep) {
635
+ const nextStep = parseTestStep(lines, index, allowEmptySteps);
636
+ if (nextStep) {
637
+ items.push(nextStep.block);
638
+ index = nextStep.nextIndex;
639
+ continue;
640
+ }
604
641
  }
605
642
  }
606
643
  if (listType === "check") {
@@ -639,13 +676,46 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
639
676
  }
640
677
  return { items, nextIndex: index };
641
678
  }
679
+ function isLikelyStep(lines, index) {
680
+ // Look ahead to see if there's indented content or expected result
681
+ if (index + 1 >= lines.length)
682
+ return false;
683
+ const nextLine = lines[index + 1];
684
+ const hasIndent = /^\s{2,}/.test(nextLine);
685
+ // Check if the next line contains expected result markers
686
+ const nextTrimmed = nextLine.trim();
687
+ const hasExpectedResult = EXPECTED_LABEL_REGEX.test(nextTrimmed);
688
+ // Only consider it a test step if:
689
+ // 1. It has an expected result, OR
690
+ // 2. The next line is indented but doesn't start with a numbered or bullet list
691
+ if (hasExpectedResult)
692
+ return true;
693
+ if (hasIndent && !/^\d+[.)]/.test(nextTrimmed) && !/^[-*+]/.test(nextTrimmed))
694
+ return true;
695
+ return false;
696
+ }
642
697
  function parseTestStep(lines, index, allowEmpty = false, snippetId) {
643
698
  const current = lines[index];
644
699
  const trimmed = current.trim();
645
- if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
700
+ const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
701
+ const isNumbered = /^\d+[.)]\s+/.test(trimmed);
702
+ if (!isBullet && !isNumbered) {
703
+ return null;
704
+ }
705
+ // For numbered lists, only parse as test steps if called from parseList with allowEmpty=true
706
+ // The first call to parseTestStep from markdownToBlocks uses allowEmpty=stepsHeadingLevel !== null
707
+ // which should be false unless we're under a Steps heading
708
+ if (isNumbered && !allowEmpty) {
646
709
  return null;
647
710
  }
648
- let rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
711
+ let rawTitle;
712
+ if (isBullet) {
713
+ rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
714
+ }
715
+ else {
716
+ // For numbered lists, remove the number and delimiter
717
+ rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
718
+ }
649
719
  let blockType = "testStep";
650
720
  const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
651
721
  if (snippetMatch) {
@@ -667,7 +737,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
667
737
  let expectedResult = "";
668
738
  let next = index + 1;
669
739
  let inExpectedResult = false;
670
- let foundFirstExpected = false;
671
740
  while (next < lines.length) {
672
741
  const line = lines[next];
673
742
  const hasIndent = /^\s{2,}/.test(line);
@@ -701,7 +770,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
701
770
  const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
702
771
  rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
703
772
  if (expectedMatch || expectedStarMatch) {
704
- foundFirstExpected = true;
705
773
  inExpectedResult = true;
706
774
  const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
707
775
  let content = rawTrimmed.slice(label.length).trim();
@@ -720,7 +788,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
720
788
  }
721
789
  // Check for lines that start with * and contain Expected (but don't match the above patterns)
722
790
  if (rawTrimmed.match(/^\*[^*]*expected/i)) {
723
- foundFirstExpected = true;
724
791
  inExpectedResult = true;
725
792
  // Remove the leading * and trim
726
793
  let content = rawTrimmed.slice(1).trim();
@@ -776,17 +843,6 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
776
843
  expectedResult = expectedContent;
777
844
  }
778
845
  }
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
846
  next += 1;
791
847
  continue;
792
848
  }
@@ -809,7 +865,8 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
809
865
  .map((line) => line.trimEnd())
810
866
  .join("\n")
811
867
  .trim();
812
- if (!isLikelyStep &&
868
+ if (!isBullet &&
869
+ !isLikelyStep &&
813
870
  !expectedResult &&
814
871
  stepDataLines.length === 0 &&
815
872
  !(allowEmpty && titleWithPlaceholders.length > 0)) {
@@ -829,6 +886,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
829
886
  stepTitle: titleWithPlaceholders,
830
887
  stepData: stepDataWithImages,
831
888
  expectedResult,
889
+ listStyle: isNumbered ? "ordered" : "bullet",
832
890
  };
833
891
  const parsedBlock = {
834
892
  type: blockType,
@@ -1014,6 +1072,60 @@ function parseSnippetWrapper(lines, index) {
1014
1072
  nextIndex: next,
1015
1073
  };
1016
1074
  }
1075
+ // Post-process blocks to fix malformed image blocks
1076
+ export function fixMalformedImageBlocks(blocks) {
1077
+ var _a, _b, _c, _d;
1078
+ const result = [];
1079
+ let i = 0;
1080
+ while (i < blocks.length) {
1081
+ const current = blocks[i];
1082
+ const next = blocks[i + 1];
1083
+ // Skip empty paragraphs
1084
+ if (current.type === "paragraph" &&
1085
+ (!current.content || !Array.isArray(current.content) || current.content.length === 0)) {
1086
+ i += 1;
1087
+ continue;
1088
+ }
1089
+ // Check if current is a paragraph with just "!" - this is definitely a malformed image
1090
+ if (current.type === "paragraph" &&
1091
+ current.content &&
1092
+ Array.isArray(current.content) &&
1093
+ current.content.length === 1 &&
1094
+ ((_a = current.content[0]) === null || _a === void 0 ? void 0 : _a.type) === "text" &&
1095
+ ((_b = current.content[0]) === null || _b === void 0 ? void 0 : _b.text) === "!") {
1096
+ // This is a malformed image block, skip it entirely
1097
+ // The full image was likely parsed as ![](...) but got corrupted
1098
+ i += 1;
1099
+ continue;
1100
+ }
1101
+ // Check if current is a paragraph with just "!" and next is an empty paragraph
1102
+ if (current.type === "paragraph" &&
1103
+ (next === null || next === void 0 ? void 0 : next.type) === "paragraph" &&
1104
+ current.content &&
1105
+ Array.isArray(current.content) &&
1106
+ current.content.length === 1 &&
1107
+ ((_c = current.content[0]) === null || _c === void 0 ? void 0 : _c.type) === "text" &&
1108
+ ((_d = current.content[0]) === null || _d === void 0 ? void 0 : _d.text) === "!" &&
1109
+ (!next.content || !Array.isArray(next.content) || next.content.length === 0)) {
1110
+ // This looks like a malformed image, skip both blocks
1111
+ i += 2;
1112
+ continue;
1113
+ }
1114
+ // Check if current has "!" but no link
1115
+ if (current.type === "paragraph" &&
1116
+ current.content &&
1117
+ Array.isArray(current.content) &&
1118
+ current.content.some((item) => item.type === "text" && item.text === "!") &&
1119
+ !current.content.some((item) => item.type === "link")) {
1120
+ // Skip malformed image block
1121
+ i += 1;
1122
+ continue;
1123
+ }
1124
+ result.push(current);
1125
+ i += 1;
1126
+ }
1127
+ return result;
1128
+ }
1017
1129
  export function markdownToBlocks(markdown) {
1018
1130
  var _a, _b;
1019
1131
  const normalized = markdown.replace(/\r\n/g, "\n");
@@ -1051,7 +1163,7 @@ export function markdownToBlocks(markdown) {
1051
1163
  const headingLevel = (_b = (_a = headingBlock.props) === null || _a === void 0 ? void 0 : _a.level) !== null && _b !== void 0 ? _b : 3;
1052
1164
  const headingText = inlineContentToPlainText(headingBlock.content);
1053
1165
  const normalizedHeading = headingText.trim().toLowerCase();
1054
- if (normalizedHeading === "steps") {
1166
+ if (normalizedHeading.replace(/[:\-–—]$/, "") === "steps") {
1055
1167
  stepsHeadingLevel = headingLevel;
1056
1168
  }
1057
1169
  else if (stepsHeadingLevel !== null &&
@@ -1086,7 +1198,7 @@ export function markdownToBlocks(markdown) {
1086
1198
  blocks.push(paragraph.block);
1087
1199
  index = paragraph.nextIndex;
1088
1200
  }
1089
- return blocks;
1201
+ return fixMalformedImageBlocks(blocks);
1090
1202
  }
1091
1203
  function splitTableRow(line) {
1092
1204
  let value = line.trim();
@@ -15,6 +15,9 @@ export declare const customSchema: BlockNoteSchema<import("@blocknote/core").Blo
15
15
  readonly expectedResult: {
16
16
  readonly default: "";
17
17
  };
18
+ readonly listStyle: {
19
+ readonly default: "bullet";
20
+ };
18
21
  };
19
22
  };
20
23
  implementation: import("@blocknote/core").TiptapBlockImplementation<{
@@ -30,6 +33,9 @@ export declare const customSchema: BlockNoteSchema<import("@blocknote/core").Blo
30
33
  readonly expectedResult: {
31
34
  readonly default: "";
32
35
  };
36
+ readonly listStyle: {
37
+ readonly default: "bullet";
38
+ };
33
39
  };
34
40
  }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
35
41
  };