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.
@@ -1,4 +1,4 @@
1
- const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
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 declare function markdownToBlocks(markdown: string): CustomPartialBlock[];
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
- lines.push(`![${caption}](${url})`);
267
+ const size = width ? ` =${width}x*` : "";
268
+ lines.push(`![${caption}](${url}${size})`);
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
  });
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.32",
3
+ "version": "0.4.34",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
@@ -1,4 +1,4 @@
1
- const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
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("![](/attachments/test.png)");
1431
1530
  });
1432
1531
 
1532
+ it("parses image with size suffix =WIDTHxHEIGHT", () => {
1533
+ const markdown = "![caption](http://localhost:3000/attachments/img.png =200x100)";
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 = "![](http://localhost:3000/attachments/img.png =150x*)";
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("![test](/attachments/test.png =300x*)");
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("![](/attachments/test.png)");
1586
+ expect(markdown).not.toContain("=");
1587
+ });
1588
+
1589
+ it("round-trips image with size preserving width", () => {
1590
+ const markdown = "![photo](/attachments/photo.png =400x250)";
1591
+ const blocks = markdownToBlocks(markdown);
1592
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1593
+ expect(roundTripMarkdown).toContain("![photo](/attachments/photo.png =400x*)");
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
- lines.push(`![${caption}](${url})`);
345
+ const size = width ? ` =${width}x*` : "";
346
+ lines.push(`![${caption}](${url}${size})`);
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 function markdownToBlocks(markdown: string): CustomPartialBlock[] {
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);
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  markdownToBlocks,
14
14
  type CustomEditorBlock,
15
15
  type CustomPartialBlock,
16
+ type MarkdownToBlocksOptions,
16
17
  } from "./editor/customMarkdownConverter";
17
18
 
18
19
  export {