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.
- package/package/editor/blocks/snippet.js +32 -138
- package/package/editor/blocks/step.js +116 -52
- package/package/editor/blocks/stepField.d.ts +4 -1
- package/package/editor/blocks/stepField.js +651 -39
- package/package/editor/blocks/stepHorizontalView.d.ts +14 -0
- package/package/editor/blocks/stepHorizontalView.js +7 -0
- package/package/editor/customMarkdownConverter.d.ts +1 -0
- package/package/editor/customMarkdownConverter.js +136 -36
- package/package/styles.css +565 -122
- package/package.json +5 -1
- package/src/editor/blocks/snippet.tsx +92 -212
- package/src/editor/blocks/step.tsx +265 -123
- package/src/editor/blocks/stepField.tsx +907 -95
- package/src/editor/blocks/stepHorizontalView.tsx +90 -0
- package/src/editor/customMarkdownConverter.test.ts +443 -1
- package/src/editor/customMarkdownConverter.ts +168 -41
- package/src/editor/styles.css +561 -133
|
@@ -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*)
|
|
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
|
-
|
|
128
|
-
|
|
127
|
+
const result = [];
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < content.length) {
|
|
130
|
+
const item = content[i];
|
|
129
131
|
if (isStyledTextInlineContent(item)) {
|
|
130
|
-
|
|
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(``);
|
|
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
|
-
|
|
148
|
+
result.push(`[${inner}](${safeHref})`);
|
|
149
|
+
i += 1;
|
|
150
|
+
continue;
|
|
136
151
|
}
|
|
137
152
|
if (Array.isArray(item.content)) {
|
|
138
|
-
|
|
153
|
+
result.push(inlineToMarkdown(item.content));
|
|
154
|
+
i += 1;
|
|
155
|
+
continue;
|
|
139
156
|
}
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
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:
|
|
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
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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
|
|
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();
|