testomatio-editor-blocks 0.4.60 → 0.4.62
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.
|
@@ -22,7 +22,7 @@ const headingPrefixes = {
|
|
|
22
22
|
5: "#####",
|
|
23
23
|
6: "######",
|
|
24
24
|
};
|
|
25
|
-
const SPECIAL_CHAR_REGEX = /([*_`~()
|
|
25
|
+
const SPECIAL_CHAR_REGEX = /([*_`~()\\])/g;
|
|
26
26
|
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
27
27
|
const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
|
|
28
28
|
const HTML_UNDERLINE_REGEX = /<\/?u>/g;
|
|
@@ -157,6 +157,7 @@ function applyTextStyles(text, styles) {
|
|
|
157
157
|
return wrapped.join("\n");
|
|
158
158
|
}
|
|
159
159
|
function inlineToMarkdown(content) {
|
|
160
|
+
var _a;
|
|
160
161
|
if (!content || !Array.isArray(content)) {
|
|
161
162
|
return "";
|
|
162
163
|
}
|
|
@@ -174,7 +175,9 @@ function inlineToMarkdown(content) {
|
|
|
174
175
|
i += 2;
|
|
175
176
|
continue;
|
|
176
177
|
}
|
|
177
|
-
|
|
178
|
+
const isCode = ((_a = item.styles) === null || _a === void 0 ? void 0 : _a.code) === true;
|
|
179
|
+
const rendered = isCode ? item.text : escapeMarkdown(item.text);
|
|
180
|
+
result.push(applyTextStyles(rendered, item.styles));
|
|
178
181
|
i += 1;
|
|
179
182
|
continue;
|
|
180
183
|
}
|
|
@@ -261,7 +264,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
261
264
|
case "codeBlock": {
|
|
262
265
|
const language = block.props.language || "";
|
|
263
266
|
const fence = "```" + language;
|
|
264
|
-
const body =
|
|
267
|
+
const body = inlineContentToPlainText(block.content);
|
|
265
268
|
lines.push(fence);
|
|
266
269
|
if (body.length > 0) {
|
|
267
270
|
lines.push(body);
|
|
@@ -695,6 +698,11 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
695
698
|
var _a, _b, _c, _d;
|
|
696
699
|
const items = [];
|
|
697
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;
|
|
698
706
|
while (index < lines.length) {
|
|
699
707
|
const rawLine = lines[index];
|
|
700
708
|
const trimmed = rawLine.trim();
|
|
@@ -712,7 +720,7 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
712
720
|
}
|
|
713
721
|
const nextLine = lines[lookahead];
|
|
714
722
|
const nextIndent = countIndent(nextLine);
|
|
715
|
-
if (nextIndent <
|
|
723
|
+
if (nextIndent < baseIndent) {
|
|
716
724
|
break;
|
|
717
725
|
}
|
|
718
726
|
const nextType = detectListType(nextLine.trim());
|
|
@@ -723,13 +731,15 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
723
731
|
continue;
|
|
724
732
|
}
|
|
725
733
|
let indent = countIndent(rawLine);
|
|
726
|
-
if (indent <
|
|
734
|
+
if (indent < baseIndent) {
|
|
727
735
|
break;
|
|
728
736
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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) {
|
|
733
743
|
const lastItem = items.at(-1);
|
|
734
744
|
if (!lastItem) {
|
|
735
745
|
break;
|
|
@@ -877,11 +887,18 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
877
887
|
rawTrimmed.startsWith("|")) {
|
|
878
888
|
break;
|
|
879
889
|
}
|
|
880
|
-
// Check for expected result labels with different formatting
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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);
|
|
885
902
|
if (expectedMatch || expectedStarMatch) {
|
|
886
903
|
inExpectedResult = true;
|
|
887
904
|
// Prefer the star match (more specific about formatting) to avoid leaking markers
|
|
@@ -890,7 +907,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
890
907
|
content = (expectedStarMatch[1] || '').trim();
|
|
891
908
|
}
|
|
892
909
|
else {
|
|
893
|
-
content =
|
|
910
|
+
content = lineForExpectedCheck.slice(expectedMatch[0].length).trim();
|
|
894
911
|
}
|
|
895
912
|
// Add the content (if any) from this line
|
|
896
913
|
if (content) {
|
|
@@ -1054,7 +1071,23 @@ function parseCodeBlock(lines, index) {
|
|
|
1054
1071
|
if (!trimmed.startsWith("```")) {
|
|
1055
1072
|
return null;
|
|
1056
1073
|
}
|
|
1057
|
-
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();
|
|
1058
1091
|
const body = [];
|
|
1059
1092
|
let next = index + 1;
|
|
1060
1093
|
while (next < lines.length && !lines[next].startsWith("```")) {
|
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@ describe("blocksToMarkdown", () => {
|
|
|
62
62
|
expect(blocksToMarkdown(blocks)).toBe("<!-- ai/agent generated description -->");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("preserves HTML comments inline among text
|
|
65
|
+
it("preserves HTML comments inline among text without escaping stray angle brackets", () => {
|
|
66
66
|
const blocks: CustomEditorBlock[] = [
|
|
67
67
|
{
|
|
68
68
|
id: "c2",
|
|
@@ -75,7 +75,98 @@ describe("blocksToMarkdown", () => {
|
|
|
75
75
|
},
|
|
76
76
|
];
|
|
77
77
|
|
|
78
|
-
expect(blocksToMarkdown(blocks)).toBe("before <!-- note --> after
|
|
78
|
+
expect(blocksToMarkdown(blocks)).toBe("before <!-- note --> after <div>");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not escape `<` in plain text (e.g. comparison operators)", () => {
|
|
82
|
+
const blocks: CustomEditorBlock[] = [
|
|
83
|
+
{
|
|
84
|
+
id: "p_lt",
|
|
85
|
+
type: "paragraph",
|
|
86
|
+
props: baseProps,
|
|
87
|
+
content: [
|
|
88
|
+
{ type: "text", text: "< 768px is mobile", styles: {} },
|
|
89
|
+
],
|
|
90
|
+
children: [],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
expect(blocksToMarkdown(blocks)).toBe("< 768px is mobile");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not escape `<` in table cells (viewport breakpoints case)", () => {
|
|
98
|
+
const markdown = [
|
|
99
|
+
"| Viewport Width | Layout Expected | Nav Behavior |",
|
|
100
|
+
"| --- | --- | --- |",
|
|
101
|
+
"| < 768px | Single column | Hamburger menu |",
|
|
102
|
+
"| 768px – 1024px | Two column | Collapsed sidebar |",
|
|
103
|
+
"| > 1024px | Full desktop layout | Full nav visible |",
|
|
104
|
+
].join("\n");
|
|
105
|
+
|
|
106
|
+
const blocks = markdownToBlocks(markdown);
|
|
107
|
+
const out = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
108
|
+
expect(out).toBe(markdown);
|
|
109
|
+
expect(out).not.toContain("\\<");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("does not escape markdown chars inside inline code", () => {
|
|
113
|
+
const blocks: CustomEditorBlock[] = [
|
|
114
|
+
{
|
|
115
|
+
id: "p_code",
|
|
116
|
+
type: "paragraph",
|
|
117
|
+
props: baseProps,
|
|
118
|
+
content: [
|
|
119
|
+
{ type: "text", text: "**bold**", styles: { code: true } },
|
|
120
|
+
],
|
|
121
|
+
children: [],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
expect(blocksToMarkdown(blocks)).toBe("`**bold**`");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not escape markdown chars inside inline code in a table (syntax/rendered case)", () => {
|
|
129
|
+
const markdown = [
|
|
130
|
+
"| Syntax | Rendered As |",
|
|
131
|
+
"| --- | --- |",
|
|
132
|
+
"| `**bold**` | **bold** |",
|
|
133
|
+
"| `*italic*` | _italic_ |",
|
|
134
|
+
"| `~~strike~~` | ~~strike~~ |",
|
|
135
|
+
].join("\n");
|
|
136
|
+
|
|
137
|
+
const blocks = markdownToBlocks(markdown);
|
|
138
|
+
const out = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
139
|
+
expect(out).toBe(markdown);
|
|
140
|
+
expect(out).not.toMatch(/\\[*_~]/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("does not escape markdown chars inside fenced code blocks", () => {
|
|
144
|
+
const markdown = [
|
|
145
|
+
"```",
|
|
146
|
+
"**bold** _italic_ ~~strike~~ <div>",
|
|
147
|
+
"```",
|
|
148
|
+
].join("\n");
|
|
149
|
+
|
|
150
|
+
const blocks = markdownToBlocks(markdown);
|
|
151
|
+
const out = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
152
|
+
expect(out).toBe(markdown);
|
|
153
|
+
expect(out).not.toMatch(/\\[*_~<]/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("still escapes literal backticks inside inline code", () => {
|
|
157
|
+
const blocks: CustomEditorBlock[] = [
|
|
158
|
+
{
|
|
159
|
+
id: "p_tick",
|
|
160
|
+
type: "paragraph",
|
|
161
|
+
props: baseProps,
|
|
162
|
+
content: [
|
|
163
|
+
{ type: "text", text: "a`b", styles: { code: true } },
|
|
164
|
+
],
|
|
165
|
+
children: [],
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
expect(blocksToMarkdown(blocks)).toBe("`a\\`b`");
|
|
79
170
|
});
|
|
80
171
|
|
|
81
172
|
it("places bold markers outside leading/trailing spaces", () => {
|
|
@@ -758,6 +849,86 @@ describe("markdownToBlocks", () => {
|
|
|
758
849
|
]);
|
|
759
850
|
});
|
|
760
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
|
+
|
|
761
932
|
it("parses a step with empty title but with step data", () => {
|
|
762
933
|
const markdown = ["### Steps", "", "* ", " Navigate to the page"].join("\n");
|
|
763
934
|
|
|
@@ -1241,6 +1412,25 @@ describe("markdownToBlocks", () => {
|
|
|
1241
1412
|
expect(nestedChildren.some((child) => child.type === "bulletListItem")).toBe(true);
|
|
1242
1413
|
});
|
|
1243
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
|
+
|
|
1244
1434
|
it("does not freeze on indented list items without a parent", () => {
|
|
1245
1435
|
const markdown = [
|
|
1246
1436
|
"### Requirements",
|
|
@@ -2412,6 +2602,59 @@ describe("markdownToBlocks", () => {
|
|
|
2412
2602
|
// Most importantly: should not have a standalone "!" at the end
|
|
2413
2603
|
expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
|
|
2414
2604
|
});
|
|
2605
|
+
|
|
2606
|
+
it("parses a single-line fenced code block", () => {
|
|
2607
|
+
const markdown = "```{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two```";
|
|
2608
|
+
const blocks = markdownToBlocks(markdown);
|
|
2609
|
+
expect(blocks).toEqual([
|
|
2610
|
+
{
|
|
2611
|
+
type: "codeBlock",
|
|
2612
|
+
props: { language: "" },
|
|
2613
|
+
content: [
|
|
2614
|
+
{
|
|
2615
|
+
type: "text",
|
|
2616
|
+
text: "{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
|
|
2617
|
+
styles: {},
|
|
2618
|
+
},
|
|
2619
|
+
],
|
|
2620
|
+
children: [],
|
|
2621
|
+
},
|
|
2622
|
+
]);
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
it("parses an empty single-line fence without swallowing following lines", () => {
|
|
2626
|
+
const markdown = ["``````", "next line"].join("\n");
|
|
2627
|
+
const blocks = markdownToBlocks(markdown);
|
|
2628
|
+
expect(blocks).toHaveLength(2);
|
|
2629
|
+
expect(blocks[0]).toEqual({
|
|
2630
|
+
type: "codeBlock",
|
|
2631
|
+
props: { language: "" },
|
|
2632
|
+
content: undefined,
|
|
2633
|
+
children: [],
|
|
2634
|
+
});
|
|
2635
|
+
expect(blocks[1].type).toBe("paragraph");
|
|
2636
|
+
});
|
|
2637
|
+
|
|
2638
|
+
it("normalizes a single-line fenced code block to multi-line on round-trip", () => {
|
|
2639
|
+
const markdown = "```hello world```";
|
|
2640
|
+
const blocks = markdownToBlocks(markdown);
|
|
2641
|
+
expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(
|
|
2642
|
+
["```", "hello world", "```"].join("\n"),
|
|
2643
|
+
);
|
|
2644
|
+
});
|
|
2645
|
+
|
|
2646
|
+
it("still treats an opening fence with a language identifier as multi-line", () => {
|
|
2647
|
+
const markdown = ["```js", "const x = 1;", "```"].join("\n");
|
|
2648
|
+
const blocks = markdownToBlocks(markdown);
|
|
2649
|
+
expect(blocks).toEqual([
|
|
2650
|
+
{
|
|
2651
|
+
type: "codeBlock",
|
|
2652
|
+
props: { language: "js" },
|
|
2653
|
+
content: [{ type: "text", text: "const x = 1;", styles: {} }],
|
|
2654
|
+
children: [],
|
|
2655
|
+
},
|
|
2656
|
+
]);
|
|
2657
|
+
});
|
|
2415
2658
|
});
|
|
2416
2659
|
|
|
2417
2660
|
describe("file block serialization", () => {
|
|
@@ -60,7 +60,7 @@ const headingPrefixes: Record<number, string> = {
|
|
|
60
60
|
6: "######",
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
const SPECIAL_CHAR_REGEX = /([*_`~()
|
|
63
|
+
const SPECIAL_CHAR_REGEX = /([*_`~()\\])/g;
|
|
64
64
|
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
65
65
|
const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
|
|
66
66
|
const HTML_UNDERLINE_REGEX = /<\/?u>/g;
|
|
@@ -234,7 +234,9 @@ function inlineToMarkdown(content: CustomEditorBlock["content"]): string {
|
|
|
234
234
|
i += 2;
|
|
235
235
|
continue;
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
const isCode = (item.styles as any)?.code === true;
|
|
238
|
+
const rendered = isCode ? item.text : escapeMarkdown(item.text);
|
|
239
|
+
result.push(applyTextStyles(rendered, item.styles));
|
|
238
240
|
i += 1;
|
|
239
241
|
continue;
|
|
240
242
|
}
|
|
@@ -334,7 +336,7 @@ function serializeBlock(
|
|
|
334
336
|
case "codeBlock": {
|
|
335
337
|
const language = (block.props as any).language || "";
|
|
336
338
|
const fence = "```" + language;
|
|
337
|
-
const body =
|
|
339
|
+
const body = inlineContentToPlainText(block.content);
|
|
338
340
|
lines.push(fence);
|
|
339
341
|
if (body.length > 0) {
|
|
340
342
|
lines.push(body);
|
|
@@ -843,6 +845,11 @@ function parseList(
|
|
|
843
845
|
): ListParseResult {
|
|
844
846
|
const items: CustomPartialBlock[] = [];
|
|
845
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;
|
|
846
853
|
|
|
847
854
|
while (index < lines.length) {
|
|
848
855
|
const rawLine = lines[index];
|
|
@@ -862,7 +869,7 @@ function parseList(
|
|
|
862
869
|
}
|
|
863
870
|
const nextLine = lines[lookahead];
|
|
864
871
|
const nextIndent = countIndent(nextLine);
|
|
865
|
-
if (nextIndent <
|
|
872
|
+
if (nextIndent < baseIndent) {
|
|
866
873
|
break;
|
|
867
874
|
}
|
|
868
875
|
const nextType = detectListType(nextLine.trim());
|
|
@@ -875,14 +882,17 @@ function parseList(
|
|
|
875
882
|
|
|
876
883
|
let indent = countIndent(rawLine);
|
|
877
884
|
|
|
878
|
-
if (indent <
|
|
885
|
+
if (indent < baseIndent) {
|
|
879
886
|
break;
|
|
880
887
|
}
|
|
881
888
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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) {
|
|
886
896
|
const lastItem = items.at(-1);
|
|
887
897
|
if (!lastItem) {
|
|
888
898
|
break;
|
|
@@ -1061,11 +1071,18 @@ function parseTestStep(
|
|
|
1061
1071
|
break;
|
|
1062
1072
|
}
|
|
1063
1073
|
|
|
1064
|
-
// Check for expected result labels with different formatting
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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);
|
|
1069
1086
|
|
|
1070
1087
|
if (expectedMatch || expectedStarMatch) {
|
|
1071
1088
|
inExpectedResult = true;
|
|
@@ -1074,7 +1091,7 @@ function parseTestStep(
|
|
|
1074
1091
|
if (expectedStarMatch) {
|
|
1075
1092
|
content = (expectedStarMatch[1] || '').trim();
|
|
1076
1093
|
} else {
|
|
1077
|
-
content =
|
|
1094
|
+
content = lineForExpectedCheck.slice(expectedMatch![0].length).trim();
|
|
1078
1095
|
}
|
|
1079
1096
|
|
|
1080
1097
|
// Add the content (if any) from this line
|
|
@@ -1256,7 +1273,24 @@ function parseCodeBlock(lines: string[], index: number): { block: CustomPartialB
|
|
|
1256
1273
|
return null;
|
|
1257
1274
|
}
|
|
1258
1275
|
|
|
1259
|
-
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();
|
|
1260
1294
|
const body: string[] = [];
|
|
1261
1295
|
let next = index + 1;
|
|
1262
1296
|
while (next < lines.length && !lines[next].startsWith("```") ) {
|