testomatio-editor-blocks 0.4.32 → 0.4.34
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/markdown.js +1 -1
- package/package/editor/customMarkdownConverter.d.ts +5 -1
- package/package/editor/customMarkdownConverter.js +12 -3
- package/package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/editor/blocks/markdown.ts +1 -1
- package/src/editor/customMarkdownConverter.test.ts +163 -0
- package/src/editor/customMarkdownConverter.ts +17 -3
- package/src/index.ts +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)
|
|
1
|
+
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+?)(?:\s+=\d+x(?:\d+|\*))?\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
3
|
const INLINE_SEGMENT_REGEX = /(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
4
4
|
export function escapeHtml(text) {
|
|
@@ -5,5 +5,9 @@ export type CustomEditorBlock = Block<Schema["blockSchema"], Schema["inlineConte
|
|
|
5
5
|
export type CustomPartialBlock = PartialBlock<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>;
|
|
6
6
|
export declare function blocksToMarkdown(blocks: CustomEditorBlock[]): string;
|
|
7
7
|
export declare function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPartialBlock[];
|
|
8
|
-
export
|
|
8
|
+
export interface MarkdownToBlocksOptions {
|
|
9
|
+
/** When true, every blank line produces an empty paragraph block. */
|
|
10
|
+
preserveBlankLines?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function markdownToBlocks(markdown: string, options?: MarkdownToBlocksOptions): CustomPartialBlock[];
|
|
9
13
|
export {};
|
|
@@ -262,8 +262,10 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
262
262
|
case "image": {
|
|
263
263
|
const url = block.props.url || "";
|
|
264
264
|
const caption = block.props.caption || "";
|
|
265
|
+
const width = block.props.previewWidth;
|
|
265
266
|
if (url) {
|
|
266
|
-
|
|
267
|
+
const size = width ? ` =${width}x*` : "";
|
|
268
|
+
lines.push(``);
|
|
267
269
|
}
|
|
268
270
|
return flattenWithBlankLine(lines, true);
|
|
269
271
|
}
|
|
@@ -1145,7 +1147,7 @@ export function fixMalformedImageBlocks(blocks) {
|
|
|
1145
1147
|
}
|
|
1146
1148
|
return result;
|
|
1147
1149
|
}
|
|
1148
|
-
export function markdownToBlocks(markdown) {
|
|
1150
|
+
export function markdownToBlocks(markdown, options) {
|
|
1149
1151
|
var _a, _b, _c;
|
|
1150
1152
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1151
1153
|
const lines = normalized.split("\n");
|
|
@@ -1155,6 +1157,11 @@ export function markdownToBlocks(markdown) {
|
|
|
1155
1157
|
while (index < lines.length) {
|
|
1156
1158
|
const line = lines[index];
|
|
1157
1159
|
if (!line.trim()) {
|
|
1160
|
+
if (options === null || options === void 0 ? void 0 : options.preserveBlankLines) {
|
|
1161
|
+
blocks.push({ type: "paragraph", content: [], children: [] });
|
|
1162
|
+
index += 1;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1158
1165
|
index += 1;
|
|
1159
1166
|
// Count consecutive blank lines
|
|
1160
1167
|
let blankCount = 1;
|
|
@@ -1239,14 +1246,16 @@ export function markdownToBlocks(markdown) {
|
|
|
1239
1246
|
index += 1;
|
|
1240
1247
|
continue;
|
|
1241
1248
|
}
|
|
1242
|
-
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
1249
|
+
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+?)(?:\s+=(\d+)x(\d+|\*))?\)$/);
|
|
1243
1250
|
if (imageMatch) {
|
|
1251
|
+
const width = imageMatch[3] ? parseInt(imageMatch[3], 10) : undefined;
|
|
1244
1252
|
blocks.push({
|
|
1245
1253
|
type: "image",
|
|
1246
1254
|
props: {
|
|
1247
1255
|
url: imageMatch[2],
|
|
1248
1256
|
caption: imageMatch[1] || "",
|
|
1249
1257
|
name: "",
|
|
1258
|
+
...(width ? { previewWidth: width } : {}),
|
|
1250
1259
|
},
|
|
1251
1260
|
children: [],
|
|
1252
1261
|
});
|
package/package/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, }
|
|
|
2
2
|
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
|
-
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
|
|
5
|
+
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
|
|
6
6
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
|
|
7
7
|
export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
|
|
8
8
|
export { setFileDisplayUrlResolver, resolveFileDisplayUrl, type FileDisplayUrlResolver, } from "./editor/fileDisplayUrl";
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)
|
|
1
|
+
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+?)(?:\s+=\d+x(?:\d+|\*))?\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
3
|
const INLINE_SEGMENT_REGEX =
|
|
4
4
|
/(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
@@ -995,6 +995,105 @@ describe("markdownToBlocks", () => {
|
|
|
995
995
|
});
|
|
996
996
|
});
|
|
997
997
|
|
|
998
|
+
it("parses all bullet items as steps when blank line follows Steps heading", () => {
|
|
999
|
+
const markdown = [
|
|
1000
|
+
"### Requirements",
|
|
1001
|
+
"",
|
|
1002
|
+
"### Steps",
|
|
1003
|
+
"",
|
|
1004
|
+
"* next",
|
|
1005
|
+
" *Expected*: expected",
|
|
1006
|
+
"* next 22",
|
|
1007
|
+
"* next 3",
|
|
1008
|
+
].join("\n");
|
|
1009
|
+
|
|
1010
|
+
const blocks = markdownToBlocks(markdown);
|
|
1011
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
1012
|
+
|
|
1013
|
+
expect(stepBlocks).toHaveLength(3);
|
|
1014
|
+
expect(stepBlocks[0].props).toMatchObject({ stepTitle: "next", expectedResult: "expected" });
|
|
1015
|
+
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "next 22" });
|
|
1016
|
+
expect(stepBlocks[2].props).toMatchObject({ stepTitle: "next 3" });
|
|
1017
|
+
|
|
1018
|
+
// Ensure no bullet list items leaked
|
|
1019
|
+
const bulletBlocks = blocks.filter((block) => block.type === "bulletListItem");
|
|
1020
|
+
expect(bulletBlocks).toHaveLength(0);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it("parses all bullet items as steps WITHOUT blank line after Steps heading", () => {
|
|
1024
|
+
const markdown = [
|
|
1025
|
+
"### Requirements",
|
|
1026
|
+
"",
|
|
1027
|
+
"### Steps",
|
|
1028
|
+
"* next",
|
|
1029
|
+
" *Expected*: expected",
|
|
1030
|
+
"* next 22",
|
|
1031
|
+
"* next 3",
|
|
1032
|
+
].join("\n");
|
|
1033
|
+
|
|
1034
|
+
const blocks = markdownToBlocks(markdown);
|
|
1035
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
1036
|
+
|
|
1037
|
+
expect(stepBlocks).toHaveLength(3);
|
|
1038
|
+
expect(stepBlocks[0].props).toMatchObject({ stepTitle: "next", expectedResult: "expected" });
|
|
1039
|
+
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "next 22" });
|
|
1040
|
+
expect(stepBlocks[2].props).toMatchObject({ stepTitle: "next 3" });
|
|
1041
|
+
|
|
1042
|
+
const bulletBlocks = blocks.filter((block) => block.type === "bulletListItem");
|
|
1043
|
+
expect(bulletBlocks).toHaveLength(0);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it("round-trips steps with blank line after Steps heading", () => {
|
|
1047
|
+
const markdown = [
|
|
1048
|
+
"### Requirements",
|
|
1049
|
+
"",
|
|
1050
|
+
"### Steps",
|
|
1051
|
+
"",
|
|
1052
|
+
"* next",
|
|
1053
|
+
" *Expected*: expected",
|
|
1054
|
+
"* next 22",
|
|
1055
|
+
"* next 3",
|
|
1056
|
+
].join("\n");
|
|
1057
|
+
|
|
1058
|
+
const blocks = markdownToBlocks(markdown);
|
|
1059
|
+
const md = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
1060
|
+
const blocks2 = markdownToBlocks(md);
|
|
1061
|
+
const stepBlocks2 = blocks2.filter((block) => block.type === "testStep");
|
|
1062
|
+
expect(stepBlocks2).toHaveLength(3);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it("preserveBlankLines: creates empty paragraphs for each blank line", () => {
|
|
1066
|
+
const markdown = [
|
|
1067
|
+
"### Requirements",
|
|
1068
|
+
"",
|
|
1069
|
+
"### Steps",
|
|
1070
|
+
"",
|
|
1071
|
+
"* next",
|
|
1072
|
+
" *Expected*: expected",
|
|
1073
|
+
"* next 22",
|
|
1074
|
+
"* next 3",
|
|
1075
|
+
].join("\n");
|
|
1076
|
+
|
|
1077
|
+
const blocks = markdownToBlocks(markdown, { preserveBlankLines: true });
|
|
1078
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
1079
|
+
const emptyParas = blocks.filter(
|
|
1080
|
+
(block) => block.type === "paragraph" && (!block.content || (block.content as any[]).length === 0),
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
// All 3 items should be test steps
|
|
1084
|
+
expect(stepBlocks).toHaveLength(3);
|
|
1085
|
+
expect(stepBlocks[0].props).toMatchObject({ stepTitle: "next" });
|
|
1086
|
+
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "next 22" });
|
|
1087
|
+
expect(stepBlocks[2].props).toMatchObject({ stepTitle: "next 3" });
|
|
1088
|
+
|
|
1089
|
+
// Each blank line should produce an empty paragraph
|
|
1090
|
+
expect(emptyParas.length).toBeGreaterThanOrEqual(2);
|
|
1091
|
+
|
|
1092
|
+
// No bullet list items
|
|
1093
|
+
const bulletBlocks = blocks.filter((block) => block.type === "bulletListItem");
|
|
1094
|
+
expect(bulletBlocks).toHaveLength(0);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
998
1097
|
it("round-trips simple blocks", () => {
|
|
999
1098
|
const blocks: CustomEditorBlock[] = [
|
|
1000
1099
|
{
|
|
@@ -1430,6 +1529,70 @@ describe("markdownToBlocks", () => {
|
|
|
1430
1529
|
expect(roundTripMarkdown).toContain("");
|
|
1431
1530
|
});
|
|
1432
1531
|
|
|
1532
|
+
it("parses image with size suffix =WIDTHxHEIGHT", () => {
|
|
1533
|
+
const markdown = "";
|
|
1534
|
+
const blocks = markdownToBlocks(markdown);
|
|
1535
|
+
|
|
1536
|
+
const imageBlocks = blocks.filter(block => block.type === "image");
|
|
1537
|
+
expect(imageBlocks.length).toBe(1);
|
|
1538
|
+
expect((imageBlocks[0].props as any).url).toBe("http://localhost:3000/attachments/img.png");
|
|
1539
|
+
expect((imageBlocks[0].props as any).caption).toBe("caption");
|
|
1540
|
+
expect((imageBlocks[0].props as any).previewWidth).toBe(200);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
it("parses image with size suffix =WIDTHx*", () => {
|
|
1544
|
+
const markdown = "";
|
|
1545
|
+
const blocks = markdownToBlocks(markdown);
|
|
1546
|
+
|
|
1547
|
+
const imageBlocks = blocks.filter(block => block.type === "image");
|
|
1548
|
+
expect(imageBlocks.length).toBe(1);
|
|
1549
|
+
expect((imageBlocks[0].props as any).url).toBe("http://localhost:3000/attachments/img.png");
|
|
1550
|
+
expect((imageBlocks[0].props as any).previewWidth).toBe(150);
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
it("serializes image block with previewWidth to =WIDTHx*", () => {
|
|
1554
|
+
const blocks: any[] = [
|
|
1555
|
+
{
|
|
1556
|
+
type: "image",
|
|
1557
|
+
props: {
|
|
1558
|
+
url: "/attachments/test.png",
|
|
1559
|
+
caption: "test",
|
|
1560
|
+
name: "",
|
|
1561
|
+
previewWidth: 300,
|
|
1562
|
+
},
|
|
1563
|
+
content: [],
|
|
1564
|
+
children: [],
|
|
1565
|
+
},
|
|
1566
|
+
];
|
|
1567
|
+
const markdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
1568
|
+
expect(markdown).toContain("");
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
it("serializes image block without previewWidth normally", () => {
|
|
1572
|
+
const blocks: any[] = [
|
|
1573
|
+
{
|
|
1574
|
+
type: "image",
|
|
1575
|
+
props: {
|
|
1576
|
+
url: "/attachments/test.png",
|
|
1577
|
+
caption: "",
|
|
1578
|
+
name: "",
|
|
1579
|
+
},
|
|
1580
|
+
content: [],
|
|
1581
|
+
children: [],
|
|
1582
|
+
},
|
|
1583
|
+
];
|
|
1584
|
+
const markdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
1585
|
+
expect(markdown).toContain("");
|
|
1586
|
+
expect(markdown).not.toContain("=");
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
it("round-trips image with size preserving width", () => {
|
|
1590
|
+
const markdown = "";
|
|
1591
|
+
const blocks = markdownToBlocks(markdown);
|
|
1592
|
+
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
1593
|
+
expect(roundTripMarkdown).toContain("");
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1433
1596
|
it("removes malformed image blocks through post-processing", () => {
|
|
1434
1597
|
// Simulate the malformed blocks you're seeing
|
|
1435
1598
|
const malformedBlocks: any[] = [
|
|
@@ -340,8 +340,10 @@ function serializeBlock(
|
|
|
340
340
|
case "image": {
|
|
341
341
|
const url = (block.props as any).url || "";
|
|
342
342
|
const caption = (block.props as any).caption || "";
|
|
343
|
+
const width = (block.props as any).previewWidth;
|
|
343
344
|
if (url) {
|
|
344
|
-
|
|
345
|
+
const size = width ? ` =${width}x*` : "";
|
|
346
|
+
lines.push(``);
|
|
345
347
|
}
|
|
346
348
|
return flattenWithBlankLine(lines, true);
|
|
347
349
|
}
|
|
@@ -1370,7 +1372,12 @@ export function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPar
|
|
|
1370
1372
|
return result;
|
|
1371
1373
|
}
|
|
1372
1374
|
|
|
1373
|
-
export
|
|
1375
|
+
export interface MarkdownToBlocksOptions {
|
|
1376
|
+
/** When true, every blank line produces an empty paragraph block. */
|
|
1377
|
+
preserveBlankLines?: boolean;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
export function markdownToBlocks(markdown: string, options?: MarkdownToBlocksOptions): CustomPartialBlock[] {
|
|
1374
1381
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1375
1382
|
const lines = normalized.split("\n");
|
|
1376
1383
|
const blocks: CustomPartialBlock[] = [];
|
|
@@ -1380,6 +1387,11 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1380
1387
|
while (index < lines.length) {
|
|
1381
1388
|
const line = lines[index];
|
|
1382
1389
|
if (!line.trim()) {
|
|
1390
|
+
if (options?.preserveBlankLines) {
|
|
1391
|
+
blocks.push({ type: "paragraph", content: [], children: [] } as CustomPartialBlock);
|
|
1392
|
+
index += 1;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1383
1395
|
index += 1;
|
|
1384
1396
|
// Count consecutive blank lines
|
|
1385
1397
|
let blankCount = 1;
|
|
@@ -1482,14 +1494,16 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1482
1494
|
continue;
|
|
1483
1495
|
}
|
|
1484
1496
|
|
|
1485
|
-
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
1497
|
+
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+?)(?:\s+=(\d+)x(\d+|\*))?\)$/);
|
|
1486
1498
|
if (imageMatch) {
|
|
1499
|
+
const width = imageMatch[3] ? parseInt(imageMatch[3], 10) : undefined;
|
|
1487
1500
|
blocks.push({
|
|
1488
1501
|
type: "image",
|
|
1489
1502
|
props: {
|
|
1490
1503
|
url: imageMatch[2],
|
|
1491
1504
|
caption: imageMatch[1] || "",
|
|
1492
1505
|
name: "",
|
|
1506
|
+
...(width ? { previewWidth: width } : {}),
|
|
1493
1507
|
},
|
|
1494
1508
|
children: [],
|
|
1495
1509
|
} as CustomPartialBlock);
|