testomatio-editor-blocks 0.4.61 → 0.4.63
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.
|
@@ -93,13 +93,8 @@ export function canInsertStepOrSnippet(editor, referenceBlockId) {
|
|
|
93
93
|
* Returns the inserted step's block ID (for focusing), or null.
|
|
94
94
|
*/
|
|
95
95
|
export function addStepsBlock(editor) {
|
|
96
|
-
var _a, _b, _c, _d;
|
|
96
|
+
var _a, _b, _c, _d, _e, _f;
|
|
97
97
|
const allBlocks = editor.document;
|
|
98
|
-
const emptyStep = {
|
|
99
|
-
type: "testStep",
|
|
100
|
-
props: { stepTitle: "", stepData: "", expectedResult: "" },
|
|
101
|
-
children: [],
|
|
102
|
-
};
|
|
103
98
|
let stepsHeadingIndex = -1;
|
|
104
99
|
for (let i = 0; i < allBlocks.length; i++) {
|
|
105
100
|
const b = allBlocks[i];
|
|
@@ -129,8 +124,17 @@ export function addStepsBlock(editor) {
|
|
|
129
124
|
continue;
|
|
130
125
|
break;
|
|
131
126
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
127
|
+
const previousStep = allBlocks[lastIndex];
|
|
128
|
+
const inheritedListStyle = (previousStep === null || previousStep === void 0 ? void 0 : previousStep.type) === "testStep"
|
|
129
|
+
? ((_b = (_a = previousStep.props) === null || _a === void 0 ? void 0 : _a.listStyle) !== null && _b !== void 0 ? _b : "bullet")
|
|
130
|
+
: "bullet";
|
|
131
|
+
const emptyStep = {
|
|
132
|
+
type: "testStep",
|
|
133
|
+
props: { stepTitle: "", stepData: "", expectedResult: "", listStyle: inheritedListStyle },
|
|
134
|
+
children: [],
|
|
135
|
+
};
|
|
136
|
+
const inserted = editor.insertBlocks([emptyStep], previousStep.id, "after");
|
|
137
|
+
return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[0]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
134
138
|
}
|
|
135
139
|
const lastBlock = allBlocks[allBlocks.length - 1];
|
|
136
140
|
const stepsHeading = {
|
|
@@ -139,8 +143,13 @@ export function addStepsBlock(editor) {
|
|
|
139
143
|
content: [{ type: "text", text: "Steps" }],
|
|
140
144
|
children: [],
|
|
141
145
|
};
|
|
146
|
+
const emptyStep = {
|
|
147
|
+
type: "testStep",
|
|
148
|
+
props: { stepTitle: "", stepData: "", expectedResult: "" },
|
|
149
|
+
children: [],
|
|
150
|
+
};
|
|
142
151
|
const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
|
|
143
|
-
return (
|
|
152
|
+
return (_f = (_e = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _e === void 0 ? void 0 : _e.id) !== null && _f !== void 0 ? _f : null;
|
|
144
153
|
}
|
|
145
154
|
/**
|
|
146
155
|
* Programmatically add an empty snippet block to the editor.
|
|
@@ -346,12 +355,14 @@ export const stepBlock = createReactBlockSpec({
|
|
|
346
355
|
});
|
|
347
356
|
}, [editor, block.id, expectedResult]);
|
|
348
357
|
const handleInsertNextStep = useCallback(() => {
|
|
358
|
+
var _a;
|
|
349
359
|
const allBlocks = editor.document;
|
|
350
360
|
const idx = allBlocks.findIndex((b) => b.id === block.id);
|
|
351
361
|
const next = idx >= 0 ? allBlocks[idx + 1] : null;
|
|
352
362
|
if (next && isEmptyParagraph(next)) {
|
|
353
363
|
editor.removeBlocks([next.id]);
|
|
354
364
|
}
|
|
365
|
+
const currentListStyle = (_a = block.props.listStyle) !== null && _a !== void 0 ? _a : "bullet";
|
|
355
366
|
editor.insertBlocks([
|
|
356
367
|
{
|
|
357
368
|
type: "testStep",
|
|
@@ -359,11 +370,12 @@ export const stepBlock = createReactBlockSpec({
|
|
|
359
370
|
stepTitle: "",
|
|
360
371
|
stepData: "",
|
|
361
372
|
expectedResult: "",
|
|
373
|
+
listStyle: currentListStyle,
|
|
362
374
|
},
|
|
363
375
|
children: [],
|
|
364
376
|
},
|
|
365
377
|
], block.id, "after");
|
|
366
|
-
}, [editor, block.id]);
|
|
378
|
+
}, [editor, block.id, block.props]);
|
|
367
379
|
const handleFieldFocus = useCallback(() => {
|
|
368
380
|
var _a, _b, _c;
|
|
369
381
|
const selection = editor.getSelection();
|
|
@@ -698,6 +698,11 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
698
698
|
var _a, _b, _c, _d;
|
|
699
699
|
const items = [];
|
|
700
700
|
let index = startIndex;
|
|
701
|
+
// The minimum leading-space count for items at this list level. Initialized to
|
|
702
|
+
// the parent's expected indent, but updated to the first item's actual indent
|
|
703
|
+
// so a uniformly indented list stays flat instead of nesting under itself.
|
|
704
|
+
let baseIndent = indentLevel * 2;
|
|
705
|
+
let firstItemSeen = false;
|
|
701
706
|
while (index < lines.length) {
|
|
702
707
|
const rawLine = lines[index];
|
|
703
708
|
const trimmed = rawLine.trim();
|
|
@@ -715,7 +720,7 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
715
720
|
}
|
|
716
721
|
const nextLine = lines[lookahead];
|
|
717
722
|
const nextIndent = countIndent(nextLine);
|
|
718
|
-
if (nextIndent <
|
|
723
|
+
if (nextIndent < baseIndent) {
|
|
719
724
|
break;
|
|
720
725
|
}
|
|
721
726
|
const nextType = detectListType(nextLine.trim());
|
|
@@ -726,13 +731,15 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
726
731
|
continue;
|
|
727
732
|
}
|
|
728
733
|
let indent = countIndent(rawLine);
|
|
729
|
-
if (indent <
|
|
734
|
+
if (indent < baseIndent) {
|
|
730
735
|
break;
|
|
731
736
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
737
|
+
if (!firstItemSeen) {
|
|
738
|
+
baseIndent = indent;
|
|
739
|
+
firstItemSeen = true;
|
|
740
|
+
}
|
|
741
|
+
// Only go deeper if indent is at least 2 more than this list's base indent
|
|
742
|
+
if (indent >= baseIndent + 2 && items.length > 0) {
|
|
736
743
|
const lastItem = items.at(-1);
|
|
737
744
|
if (!lastItem) {
|
|
738
745
|
break;
|
|
@@ -880,11 +887,18 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
880
887
|
rawTrimmed.startsWith("|")) {
|
|
881
888
|
break;
|
|
882
889
|
}
|
|
883
|
-
// Check for expected result labels with different formatting
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
890
|
+
// Check for expected result labels with different formatting.
|
|
891
|
+
// Strip an optional leading bullet marker so patterns like
|
|
892
|
+
// "* *Expected*: ..." (a sub-bullet with an italic Expected label) are
|
|
893
|
+
// recognized via the same matchers as the bare label forms.
|
|
894
|
+
const bulletPrefixMatch = rawTrimmed.match(/^[*-]\s+/);
|
|
895
|
+
const lineForExpectedCheck = bulletPrefixMatch
|
|
896
|
+
? rawTrimmed.slice(bulletPrefixMatch[0].length)
|
|
897
|
+
: rawTrimmed;
|
|
898
|
+
const expectedMatch = lineForExpectedCheck.match(EXPECTED_LABEL_REGEX);
|
|
899
|
+
const expectedStarMatch = lineForExpectedCheck.match(/^\*expected\s*\*:\s*(.*)$/i) ||
|
|
900
|
+
lineForExpectedCheck.match(/^\*expected\*:\s*(.*)$/i) ||
|
|
901
|
+
lineForExpectedCheck.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
|
|
888
902
|
if (expectedMatch || expectedStarMatch) {
|
|
889
903
|
inExpectedResult = true;
|
|
890
904
|
// Prefer the star match (more specific about formatting) to avoid leaking markers
|
|
@@ -893,7 +907,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
893
907
|
content = (expectedStarMatch[1] || '').trim();
|
|
894
908
|
}
|
|
895
909
|
else {
|
|
896
|
-
content =
|
|
910
|
+
content = lineForExpectedCheck.slice(expectedMatch[0].length).trim();
|
|
897
911
|
}
|
|
898
912
|
// Add the content (if any) from this line
|
|
899
913
|
if (content) {
|
|
@@ -1057,7 +1071,23 @@ function parseCodeBlock(lines, index) {
|
|
|
1057
1071
|
if (!trimmed.startsWith("```")) {
|
|
1058
1072
|
return null;
|
|
1059
1073
|
}
|
|
1060
|
-
const
|
|
1074
|
+
const afterOpening = trimmed.slice(3);
|
|
1075
|
+
const closeMatch = afterOpening.match(/```\s*$/);
|
|
1076
|
+
if (closeMatch) {
|
|
1077
|
+
const content = afterOpening.slice(0, afterOpening.length - closeMatch[0].length);
|
|
1078
|
+
return {
|
|
1079
|
+
block: {
|
|
1080
|
+
type: "codeBlock",
|
|
1081
|
+
props: { language: "" },
|
|
1082
|
+
content: content.length
|
|
1083
|
+
? [{ type: "text", text: content, styles: {} }]
|
|
1084
|
+
: undefined,
|
|
1085
|
+
children: [],
|
|
1086
|
+
},
|
|
1087
|
+
nextIndex: index + 1,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
const language = afterOpening.trim();
|
|
1061
1091
|
const body = [];
|
|
1062
1092
|
let next = index + 1;
|
|
1063
1093
|
while (next < lines.length && !lines[next].startsWith("```")) {
|
package/package.json
CHANGED
|
@@ -108,11 +108,6 @@ export function addStepsBlock(editor: {
|
|
|
108
108
|
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
109
109
|
}): string | null {
|
|
110
110
|
const allBlocks = editor.document;
|
|
111
|
-
const emptyStep = {
|
|
112
|
-
type: "testStep" as const,
|
|
113
|
-
props: { stepTitle: "", stepData: "", expectedResult: "" },
|
|
114
|
-
children: [],
|
|
115
|
-
};
|
|
116
111
|
|
|
117
112
|
let stepsHeadingIndex = -1;
|
|
118
113
|
for (let i = 0; i < allBlocks.length; i++) {
|
|
@@ -142,7 +137,17 @@ export function addStepsBlock(editor: {
|
|
|
142
137
|
if (isEmptyParagraph(b)) continue;
|
|
143
138
|
break;
|
|
144
139
|
}
|
|
145
|
-
const
|
|
140
|
+
const previousStep = allBlocks[lastIndex];
|
|
141
|
+
const inheritedListStyle =
|
|
142
|
+
previousStep?.type === "testStep"
|
|
143
|
+
? ((previousStep.props as any)?.listStyle ?? "bullet")
|
|
144
|
+
: "bullet";
|
|
145
|
+
const emptyStep = {
|
|
146
|
+
type: "testStep" as const,
|
|
147
|
+
props: { stepTitle: "", stepData: "", expectedResult: "", listStyle: inheritedListStyle },
|
|
148
|
+
children: [],
|
|
149
|
+
};
|
|
150
|
+
const inserted = editor.insertBlocks([emptyStep], previousStep.id, "after");
|
|
146
151
|
return inserted?.[0]?.id ?? null;
|
|
147
152
|
}
|
|
148
153
|
|
|
@@ -153,6 +158,11 @@ export function addStepsBlock(editor: {
|
|
|
153
158
|
content: [{ type: "text" as const, text: "Steps" }],
|
|
154
159
|
children: [],
|
|
155
160
|
};
|
|
161
|
+
const emptyStep = {
|
|
162
|
+
type: "testStep" as const,
|
|
163
|
+
props: { stepTitle: "", stepData: "", expectedResult: "" },
|
|
164
|
+
children: [],
|
|
165
|
+
};
|
|
156
166
|
const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
|
|
157
167
|
return inserted?.[1]?.id ?? null;
|
|
158
168
|
}
|
|
@@ -399,6 +409,7 @@ export const stepBlock = createReactBlockSpec(
|
|
|
399
409
|
if (next && isEmptyParagraph(next)) {
|
|
400
410
|
editor.removeBlocks([next.id]);
|
|
401
411
|
}
|
|
412
|
+
const currentListStyle = (block.props as any).listStyle ?? "bullet";
|
|
402
413
|
editor.insertBlocks(
|
|
403
414
|
[
|
|
404
415
|
{
|
|
@@ -407,6 +418,7 @@ export const stepBlock = createReactBlockSpec(
|
|
|
407
418
|
stepTitle: "",
|
|
408
419
|
stepData: "",
|
|
409
420
|
expectedResult: "",
|
|
421
|
+
listStyle: currentListStyle,
|
|
410
422
|
},
|
|
411
423
|
children: [],
|
|
412
424
|
},
|
|
@@ -414,7 +426,7 @@ export const stepBlock = createReactBlockSpec(
|
|
|
414
426
|
block.id,
|
|
415
427
|
"after",
|
|
416
428
|
);
|
|
417
|
-
}, [editor, block.id]);
|
|
429
|
+
}, [editor, block.id, block.props]);
|
|
418
430
|
|
|
419
431
|
const handleFieldFocus = useCallback(() => {
|
|
420
432
|
const selection = editor.getSelection();
|
|
@@ -849,6 +849,86 @@ describe("markdownToBlocks", () => {
|
|
|
849
849
|
]);
|
|
850
850
|
});
|
|
851
851
|
|
|
852
|
+
it("parses sub-bulleted *Expected*: lines as the step's expected result", () => {
|
|
853
|
+
const markdown = [
|
|
854
|
+
"### Steps",
|
|
855
|
+
"",
|
|
856
|
+
"* Navigate to the “Transfer Funds” page.",
|
|
857
|
+
" * *Expected*: The Transfer Funds page loads, showing fields for source account, destination account, and amount.",
|
|
858
|
+
"",
|
|
859
|
+
"* Select the user’s own source account from the dropdown list.",
|
|
860
|
+
" * *Expected*: The selected account is displayed and its current balance is shown.",
|
|
861
|
+
].join("\n");
|
|
862
|
+
|
|
863
|
+
const blocks = markdownToBlocks(markdown);
|
|
864
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
865
|
+
expect(stepBlocks).toEqual([
|
|
866
|
+
{
|
|
867
|
+
type: "testStep",
|
|
868
|
+
props: {
|
|
869
|
+
stepTitle: "Navigate to the “Transfer Funds” page.",
|
|
870
|
+
stepData: "",
|
|
871
|
+
expectedResult:
|
|
872
|
+
"The Transfer Funds page loads, showing fields for source account, destination account, and amount.\n",
|
|
873
|
+
listStyle: "bullet",
|
|
874
|
+
},
|
|
875
|
+
children: [],
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
type: "testStep",
|
|
879
|
+
props: {
|
|
880
|
+
stepTitle: "Select the user’s own source account from the dropdown list.",
|
|
881
|
+
stepData: "",
|
|
882
|
+
expectedResult:
|
|
883
|
+
"The selected account is displayed and its current balance is shown.",
|
|
884
|
+
listStyle: "bullet",
|
|
885
|
+
},
|
|
886
|
+
children: [],
|
|
887
|
+
},
|
|
888
|
+
]);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("parses sub-bulleted **Expected**: (bold) lines as the step's expected result", () => {
|
|
892
|
+
const markdown = [
|
|
893
|
+
"### Steps",
|
|
894
|
+
"",
|
|
895
|
+
"* Open the Login page.",
|
|
896
|
+
" * **Expected**: The Login page loads successfully.",
|
|
897
|
+
].join("\n");
|
|
898
|
+
|
|
899
|
+
const blocks = markdownToBlocks(markdown);
|
|
900
|
+
const stepBlocks = blocks.filter((b) => b.type === "testStep");
|
|
901
|
+
expect(stepBlocks).toEqual([
|
|
902
|
+
{
|
|
903
|
+
type: "testStep",
|
|
904
|
+
props: {
|
|
905
|
+
stepTitle: "Open the Login page.",
|
|
906
|
+
stepData: "",
|
|
907
|
+
expectedResult: "The Login page loads successfully.",
|
|
908
|
+
listStyle: "bullet",
|
|
909
|
+
},
|
|
910
|
+
children: [],
|
|
911
|
+
},
|
|
912
|
+
]);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("round-trips sub-bulleted *Expected*: lines through the canonical serialized form", () => {
|
|
916
|
+
const markdown = [
|
|
917
|
+
"### Steps",
|
|
918
|
+
"",
|
|
919
|
+
"* Navigate to the “Transfer Funds” page.",
|
|
920
|
+
" * *Expected*: The Transfer Funds page loads.",
|
|
921
|
+
].join("\n");
|
|
922
|
+
|
|
923
|
+
const firstPass = markdownToBlocks(markdown);
|
|
924
|
+
const serialized = blocksToMarkdown(firstPass as CustomEditorBlock[]);
|
|
925
|
+
const secondPass = markdownToBlocks(serialized);
|
|
926
|
+
|
|
927
|
+
const firstSteps = firstPass.filter((b) => b.type === "testStep");
|
|
928
|
+
const secondSteps = secondPass.filter((b) => b.type === "testStep");
|
|
929
|
+
expect(secondSteps).toEqual(firstSteps);
|
|
930
|
+
});
|
|
931
|
+
|
|
852
932
|
it("parses a step with empty title but with step data", () => {
|
|
853
933
|
const markdown = ["### Steps", "", "* ", " Navigate to the page"].join("\n");
|
|
854
934
|
|
|
@@ -1332,6 +1412,25 @@ describe("markdownToBlocks", () => {
|
|
|
1332
1412
|
expect(nestedChildren.some((child) => child.type === "bulletListItem")).toBe(true);
|
|
1333
1413
|
});
|
|
1334
1414
|
|
|
1415
|
+
it("parses a uniformly indented list as a flat top-level list", () => {
|
|
1416
|
+
const markdown = [
|
|
1417
|
+
"# Requirements",
|
|
1418
|
+
"",
|
|
1419
|
+
" * User has an active account on the platform.",
|
|
1420
|
+
" * User has sufficient funds in the source account.",
|
|
1421
|
+
" * QR code contains valid transfer details and is scannable.",
|
|
1422
|
+
" * The device has camera access and QR scanning capability.",
|
|
1423
|
+
" * The user is authenticated and authorized to perform transfers.",
|
|
1424
|
+
].join("\n");
|
|
1425
|
+
|
|
1426
|
+
const blocks = markdownToBlocks(markdown);
|
|
1427
|
+
const bulletItems = blocks.filter((b) => b.type === "bulletListItem");
|
|
1428
|
+
expect(bulletItems).toHaveLength(5);
|
|
1429
|
+
for (const item of bulletItems) {
|
|
1430
|
+
expect(item.children ?? []).toEqual([]);
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1335
1434
|
it("does not freeze on indented list items without a parent", () => {
|
|
1336
1435
|
const markdown = [
|
|
1337
1436
|
"### Requirements",
|
|
@@ -1597,6 +1696,36 @@ describe("markdownToBlocks", () => {
|
|
|
1597
1696
|
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "Second step", listStyle: "ordered" });
|
|
1598
1697
|
});
|
|
1599
1698
|
|
|
1699
|
+
it("serializes a new ordered step appended to a numbered list as N.", () => {
|
|
1700
|
+
const orderedSteps: CustomEditorBlock[] = [
|
|
1701
|
+
{
|
|
1702
|
+
id: "s1",
|
|
1703
|
+
type: "testStep",
|
|
1704
|
+
props: { stepTitle: "First step", stepData: "", expectedResult: "", listStyle: "ordered" },
|
|
1705
|
+
content: undefined as any,
|
|
1706
|
+
children: [],
|
|
1707
|
+
} as any,
|
|
1708
|
+
{
|
|
1709
|
+
id: "s2",
|
|
1710
|
+
type: "testStep",
|
|
1711
|
+
props: { stepTitle: "Second step", stepData: "", expectedResult: "", listStyle: "ordered" },
|
|
1712
|
+
content: undefined as any,
|
|
1713
|
+
children: [],
|
|
1714
|
+
} as any,
|
|
1715
|
+
{
|
|
1716
|
+
id: "s3",
|
|
1717
|
+
type: "testStep",
|
|
1718
|
+
props: { stepTitle: "Newly added step", stepData: "", expectedResult: "", listStyle: "ordered" },
|
|
1719
|
+
content: undefined as any,
|
|
1720
|
+
children: [],
|
|
1721
|
+
} as any,
|
|
1722
|
+
];
|
|
1723
|
+
|
|
1724
|
+
expect(blocksToMarkdown(orderedSteps)).toBe(
|
|
1725
|
+
["1. First step", "2. Second step", "3. Newly added step"].join("\n"),
|
|
1726
|
+
);
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1600
1729
|
it("parses steps under an h4 'step' heading (lowercase)", () => {
|
|
1601
1730
|
const markdown = ["#### step", "", "* Do something"].join("\n");
|
|
1602
1731
|
const blocks = markdownToBlocks(markdown);
|
|
@@ -2503,6 +2632,59 @@ describe("markdownToBlocks", () => {
|
|
|
2503
2632
|
// Most importantly: should not have a standalone "!" at the end
|
|
2504
2633
|
expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
|
|
2505
2634
|
});
|
|
2635
|
+
|
|
2636
|
+
it("parses a single-line fenced code block", () => {
|
|
2637
|
+
const markdown = "```{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two```";
|
|
2638
|
+
const blocks = markdownToBlocks(markdown);
|
|
2639
|
+
expect(blocks).toEqual([
|
|
2640
|
+
{
|
|
2641
|
+
type: "codeBlock",
|
|
2642
|
+
props: { language: "" },
|
|
2643
|
+
content: [
|
|
2644
|
+
{
|
|
2645
|
+
type: "text",
|
|
2646
|
+
text: "{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
|
|
2647
|
+
styles: {},
|
|
2648
|
+
},
|
|
2649
|
+
],
|
|
2650
|
+
children: [],
|
|
2651
|
+
},
|
|
2652
|
+
]);
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2655
|
+
it("parses an empty single-line fence without swallowing following lines", () => {
|
|
2656
|
+
const markdown = ["``````", "next line"].join("\n");
|
|
2657
|
+
const blocks = markdownToBlocks(markdown);
|
|
2658
|
+
expect(blocks).toHaveLength(2);
|
|
2659
|
+
expect(blocks[0]).toEqual({
|
|
2660
|
+
type: "codeBlock",
|
|
2661
|
+
props: { language: "" },
|
|
2662
|
+
content: undefined,
|
|
2663
|
+
children: [],
|
|
2664
|
+
});
|
|
2665
|
+
expect(blocks[1].type).toBe("paragraph");
|
|
2666
|
+
});
|
|
2667
|
+
|
|
2668
|
+
it("normalizes a single-line fenced code block to multi-line on round-trip", () => {
|
|
2669
|
+
const markdown = "```hello world```";
|
|
2670
|
+
const blocks = markdownToBlocks(markdown);
|
|
2671
|
+
expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(
|
|
2672
|
+
["```", "hello world", "```"].join("\n"),
|
|
2673
|
+
);
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
it("still treats an opening fence with a language identifier as multi-line", () => {
|
|
2677
|
+
const markdown = ["```js", "const x = 1;", "```"].join("\n");
|
|
2678
|
+
const blocks = markdownToBlocks(markdown);
|
|
2679
|
+
expect(blocks).toEqual([
|
|
2680
|
+
{
|
|
2681
|
+
type: "codeBlock",
|
|
2682
|
+
props: { language: "js" },
|
|
2683
|
+
content: [{ type: "text", text: "const x = 1;", styles: {} }],
|
|
2684
|
+
children: [],
|
|
2685
|
+
},
|
|
2686
|
+
]);
|
|
2687
|
+
});
|
|
2506
2688
|
});
|
|
2507
2689
|
|
|
2508
2690
|
describe("file block serialization", () => {
|
|
@@ -845,6 +845,11 @@ function parseList(
|
|
|
845
845
|
): ListParseResult {
|
|
846
846
|
const items: CustomPartialBlock[] = [];
|
|
847
847
|
let index = startIndex;
|
|
848
|
+
// The minimum leading-space count for items at this list level. Initialized to
|
|
849
|
+
// the parent's expected indent, but updated to the first item's actual indent
|
|
850
|
+
// so a uniformly indented list stays flat instead of nesting under itself.
|
|
851
|
+
let baseIndent = indentLevel * 2;
|
|
852
|
+
let firstItemSeen = false;
|
|
848
853
|
|
|
849
854
|
while (index < lines.length) {
|
|
850
855
|
const rawLine = lines[index];
|
|
@@ -864,7 +869,7 @@ function parseList(
|
|
|
864
869
|
}
|
|
865
870
|
const nextLine = lines[lookahead];
|
|
866
871
|
const nextIndent = countIndent(nextLine);
|
|
867
|
-
if (nextIndent <
|
|
872
|
+
if (nextIndent < baseIndent) {
|
|
868
873
|
break;
|
|
869
874
|
}
|
|
870
875
|
const nextType = detectListType(nextLine.trim());
|
|
@@ -877,14 +882,17 @@ function parseList(
|
|
|
877
882
|
|
|
878
883
|
let indent = countIndent(rawLine);
|
|
879
884
|
|
|
880
|
-
if (indent <
|
|
885
|
+
if (indent < baseIndent) {
|
|
881
886
|
break;
|
|
882
887
|
}
|
|
883
888
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
889
|
+
if (!firstItemSeen) {
|
|
890
|
+
baseIndent = indent;
|
|
891
|
+
firstItemSeen = true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Only go deeper if indent is at least 2 more than this list's base indent
|
|
895
|
+
if (indent >= baseIndent + 2 && items.length > 0) {
|
|
888
896
|
const lastItem = items.at(-1);
|
|
889
897
|
if (!lastItem) {
|
|
890
898
|
break;
|
|
@@ -1063,11 +1071,18 @@ function parseTestStep(
|
|
|
1063
1071
|
break;
|
|
1064
1072
|
}
|
|
1065
1073
|
|
|
1066
|
-
// Check for expected result labels with different formatting
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1074
|
+
// Check for expected result labels with different formatting.
|
|
1075
|
+
// Strip an optional leading bullet marker so patterns like
|
|
1076
|
+
// "* *Expected*: ..." (a sub-bullet with an italic Expected label) are
|
|
1077
|
+
// recognized via the same matchers as the bare label forms.
|
|
1078
|
+
const bulletPrefixMatch = rawTrimmed.match(/^[*-]\s+/);
|
|
1079
|
+
const lineForExpectedCheck = bulletPrefixMatch
|
|
1080
|
+
? rawTrimmed.slice(bulletPrefixMatch[0].length)
|
|
1081
|
+
: rawTrimmed;
|
|
1082
|
+
const expectedMatch = lineForExpectedCheck.match(EXPECTED_LABEL_REGEX);
|
|
1083
|
+
const expectedStarMatch = lineForExpectedCheck.match(/^\*expected\s*\*:\s*(.*)$/i) ||
|
|
1084
|
+
lineForExpectedCheck.match(/^\*expected\*:\s*(.*)$/i) ||
|
|
1085
|
+
lineForExpectedCheck.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
|
|
1071
1086
|
|
|
1072
1087
|
if (expectedMatch || expectedStarMatch) {
|
|
1073
1088
|
inExpectedResult = true;
|
|
@@ -1076,7 +1091,7 @@ function parseTestStep(
|
|
|
1076
1091
|
if (expectedStarMatch) {
|
|
1077
1092
|
content = (expectedStarMatch[1] || '').trim();
|
|
1078
1093
|
} else {
|
|
1079
|
-
content =
|
|
1094
|
+
content = lineForExpectedCheck.slice(expectedMatch![0].length).trim();
|
|
1080
1095
|
}
|
|
1081
1096
|
|
|
1082
1097
|
// Add the content (if any) from this line
|
|
@@ -1258,7 +1273,24 @@ function parseCodeBlock(lines: string[], index: number): { block: CustomPartialB
|
|
|
1258
1273
|
return null;
|
|
1259
1274
|
}
|
|
1260
1275
|
|
|
1261
|
-
const
|
|
1276
|
+
const afterOpening = trimmed.slice(3);
|
|
1277
|
+
const closeMatch = afterOpening.match(/```\s*$/);
|
|
1278
|
+
if (closeMatch) {
|
|
1279
|
+
const content = afterOpening.slice(0, afterOpening.length - closeMatch[0].length);
|
|
1280
|
+
return {
|
|
1281
|
+
block: {
|
|
1282
|
+
type: "codeBlock",
|
|
1283
|
+
props: { language: "" },
|
|
1284
|
+
content: content.length
|
|
1285
|
+
? [{ type: "text", text: content, styles: {} }]
|
|
1286
|
+
: undefined,
|
|
1287
|
+
children: [],
|
|
1288
|
+
},
|
|
1289
|
+
nextIndex: index + 1,
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const language = afterOpening.trim();
|
|
1262
1294
|
const body: string[] = [];
|
|
1263
1295
|
let next = index + 1;
|
|
1264
1296
|
while (next < lines.length && !lines[next].startsWith("```") ) {
|