testomatio-editor-blocks 0.4.29 → 0.4.31

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,5 @@
1
1
  import { isLinkInlineContent, isStyledTextInlineContent, } from "@blocknote/core";
2
+ import { resolveFileDisplayUrl } from "./fileDisplayUrl";
2
3
  import { isStepsHeading } from "./blocks/step";
3
4
  const BASE_BLOCK_PROPS = {
4
5
  textAlignment: "left",
@@ -266,6 +267,15 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
266
267
  }
267
268
  return flattenWithBlankLine(lines, true);
268
269
  }
270
+ case "file": {
271
+ const url = block.props.url || "";
272
+ const name = block.props.name || "";
273
+ if (url) {
274
+ const displayUrl = resolveFileDisplayUrl(name, url);
275
+ lines.push(`[![${name}](${displayUrl})](${url})`);
276
+ }
277
+ return flattenWithBlankLine(lines, true);
278
+ }
269
279
  case "testStep":
270
280
  case "snippet": {
271
281
  const isSnippet = block.type === "snippet";
@@ -1214,6 +1224,19 @@ export function markdownToBlocks(markdown) {
1214
1224
  index = nextIndex;
1215
1225
  continue;
1216
1226
  }
1227
+ const fileMatch = line.trim().match(/^\[!\[([^\]]*)\]\(([^)]*)\)\]\(([^)]+)\)$/);
1228
+ if (fileMatch) {
1229
+ blocks.push({
1230
+ type: "file",
1231
+ props: {
1232
+ name: fileMatch[1] || "",
1233
+ url: fileMatch[3],
1234
+ },
1235
+ children: [],
1236
+ });
1237
+ index += 1;
1238
+ continue;
1239
+ }
1217
1240
  const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
1218
1241
  if (imageMatch) {
1219
1242
  blocks.push({
@@ -0,0 +1,3 @@
1
+ export type FileDisplayUrlResolver = (fileName: string) => string;
2
+ export declare function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null): void;
3
+ export declare function resolveFileDisplayUrl(fileName: string, fallbackUrl: string): string;
@@ -0,0 +1,15 @@
1
+ let resolver = null;
2
+ export function setFileDisplayUrlResolver(fn) {
3
+ resolver = fn;
4
+ }
5
+ export function resolveFileDisplayUrl(fileName, fallbackUrl) {
6
+ var _a;
7
+ if (resolver) {
8
+ return resolver(fileName);
9
+ }
10
+ const ext = ((_a = fileName.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || "";
11
+ if (ext) {
12
+ return `/images/file-type-icons/${ext}.svg`;
13
+ }
14
+ return fallbackUrl;
15
+ }
@@ -5,5 +5,6 @@ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
5
  export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } 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
+ export { setFileDisplayUrlResolver, resolveFileDisplayUrl, type FileDisplayUrlResolver, } from "./editor/fileDisplayUrl";
8
9
  export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
9
10
  export declare const testomatioEditorClassName = "markdown testomatio-editor";
package/package/index.js CHANGED
@@ -5,5 +5,6 @@ export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
5
  export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
6
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
7
7
  export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
8
+ export { setFileDisplayUrlResolver, resolveFileDisplayUrl, } from "./editor/fileDisplayUrl";
8
9
  export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
9
10
  export const testomatioEditorClassName = "markdown testomatio-editor";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
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",
package/src/App.tsx CHANGED
@@ -349,6 +349,9 @@ function App() {
349
349
  const editor = useCreateBlockNote({
350
350
  schema: customSchema,
351
351
  pasteHandler: createMarkdownPasteHandler(markdownToBlocks),
352
+ uploadFile: async (file: File) => {
353
+ return `https://placehold.co/600x400?text=${encodeURIComponent(file.name)}`;
354
+ },
352
355
  });
353
356
  const [markdown, setMarkdown] = useState("");
354
357
  const [conversionError, setConversionError] = useState<string | null>(null);
@@ -6,6 +6,7 @@ import {
6
6
  type CustomEditorBlock,
7
7
  type CustomPartialBlock,
8
8
  } from "./customMarkdownConverter";
9
+ import { setFileDisplayUrlResolver } from "./fileDisplayUrl";
9
10
 
10
11
  const baseProps = {
11
12
  textAlignment: "left" as const,
@@ -1513,3 +1514,126 @@ describe("markdownToBlocks", () => {
1513
1514
  expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
1514
1515
  });
1515
1516
  });
1517
+
1518
+ describe("file block serialization", () => {
1519
+ it("serializes a file block using display url resolver", () => {
1520
+ setFileDisplayUrlResolver((name: string) => {
1521
+ const ext = name.split(".").pop()?.toLowerCase() || "";
1522
+ return `/images/file-type-icons/${ext}.svg`;
1523
+ });
1524
+
1525
+ const blocks: CustomEditorBlock[] = [
1526
+ {
1527
+ id: "1",
1528
+ type: "file",
1529
+ props: {
1530
+ ...baseProps,
1531
+ url: "https://example.com/file.pdf",
1532
+ name: "report.pdf",
1533
+ caption: "",
1534
+ },
1535
+ content: undefined as any,
1536
+ children: [],
1537
+ },
1538
+ ];
1539
+ const md = blocksToMarkdown(blocks);
1540
+ expect(md).toBe("[![report.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)");
1541
+
1542
+ setFileDisplayUrlResolver(null);
1543
+ });
1544
+
1545
+ it("falls back to url when no resolver is set", () => {
1546
+ const blocks: CustomEditorBlock[] = [
1547
+ {
1548
+ id: "1",
1549
+ type: "file",
1550
+ props: {
1551
+ ...baseProps,
1552
+ url: "https://example.com/file.pdf",
1553
+ name: "file.pdf",
1554
+ caption: "",
1555
+ },
1556
+ content: undefined as any,
1557
+ children: [],
1558
+ },
1559
+ ];
1560
+ const md = blocksToMarkdown(blocks);
1561
+ expect(md).toBe("[![file.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)");
1562
+ });
1563
+
1564
+ it("outputs nothing when url is empty", () => {
1565
+ const blocks: CustomEditorBlock[] = [
1566
+ {
1567
+ id: "1",
1568
+ type: "file",
1569
+ props: {
1570
+ ...baseProps,
1571
+ url: "",
1572
+ name: "file.pdf",
1573
+ caption: "",
1574
+ },
1575
+ content: undefined as any,
1576
+ children: [],
1577
+ },
1578
+ ];
1579
+ const md = blocksToMarkdown(blocks);
1580
+ expect(md.trim()).toBe("");
1581
+ });
1582
+ });
1583
+
1584
+ describe("file block parsing", () => {
1585
+ it("parses file markdown into a file block", () => {
1586
+ const markdown = "[![report.pdf](/images/file-type-icons/pdf.svg)](https://example.com/file.pdf)";
1587
+ const blocks = markdownToBlocks(markdown);
1588
+ expect(blocks).toHaveLength(1);
1589
+ expect(blocks[0].type).toBe("file");
1590
+ expect((blocks[0].props as any).name).toBe("report.pdf");
1591
+ expect((blocks[0].props as any).url).toBe("https://example.com/file.pdf");
1592
+ });
1593
+
1594
+ it("parses file markdown with empty name", () => {
1595
+ const markdown = "[![](https://example.com/url)](https://example.com/url)";
1596
+ const blocks = markdownToBlocks(markdown);
1597
+ expect(blocks).toHaveLength(1);
1598
+ expect(blocks[0].type).toBe("file");
1599
+ expect((blocks[0].props as any).name).toBe("");
1600
+ expect((blocks[0].props as any).url).toBe("https://example.com/url");
1601
+ });
1602
+
1603
+ it("does not confuse file blocks with image blocks", () => {
1604
+ const markdown = "![caption](https://example.com/image.png)";
1605
+ const blocks = markdownToBlocks(markdown);
1606
+ expect(blocks).toHaveLength(1);
1607
+ expect(blocks[0].type).toBe("image");
1608
+ });
1609
+
1610
+ it("round-trips file blocks through serialize and parse", () => {
1611
+ setFileDisplayUrlResolver((name: string) => {
1612
+ const ext = name.split(".").pop()?.toLowerCase() || "";
1613
+ return `/images/file-type-icons/${ext}.svg`;
1614
+ });
1615
+
1616
+ const blocks: CustomEditorBlock[] = [
1617
+ {
1618
+ id: "1",
1619
+ type: "file",
1620
+ props: {
1621
+ ...baseProps,
1622
+ url: "https://example.com/doc.xlsx",
1623
+ name: "doc.xlsx",
1624
+ caption: "",
1625
+ },
1626
+ content: undefined as any,
1627
+ children: [],
1628
+ },
1629
+ ];
1630
+ const md = blocksToMarkdown(blocks);
1631
+ const parsed = markdownToBlocks(md);
1632
+ expect(parsed).toHaveLength(1);
1633
+ expect(parsed[0].type).toBe("file");
1634
+ expect((parsed[0].props as any).url).toBe("https://example.com/doc.xlsx");
1635
+ expect((parsed[0].props as any).name).toBe("doc.xlsx");
1636
+
1637
+ setFileDisplayUrlResolver(null);
1638
+ });
1639
+ });
@@ -8,6 +8,7 @@ import type {
8
8
  PartialBlock,
9
9
  Styles,
10
10
  } from "@blocknote/core";
11
+ import { resolveFileDisplayUrl } from "./fileDisplayUrl";
11
12
  import type { customSchema } from "./customSchema";
12
13
  import { isStepsHeading } from "./blocks/step";
13
14
 
@@ -344,6 +345,15 @@ function serializeBlock(
344
345
  }
345
346
  return flattenWithBlankLine(lines, true);
346
347
  }
348
+ case "file": {
349
+ const url = (block.props as any).url || "";
350
+ const name = (block.props as any).name || "";
351
+ if (url) {
352
+ const displayUrl = resolveFileDisplayUrl(name, url);
353
+ lines.push(`[![${name}](${displayUrl})](${url})`);
354
+ }
355
+ return flattenWithBlankLine(lines, true);
356
+ }
347
357
  case "testStep":
348
358
  case "snippet": {
349
359
  const isSnippet = block.type === "snippet";
@@ -1456,6 +1466,20 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1456
1466
  continue;
1457
1467
  }
1458
1468
 
1469
+ const fileMatch = line.trim().match(/^\[!\[([^\]]*)\]\(([^)]*)\)\]\(([^)]+)\)$/);
1470
+ if (fileMatch) {
1471
+ blocks.push({
1472
+ type: "file",
1473
+ props: {
1474
+ name: fileMatch[1] || "",
1475
+ url: fileMatch[3],
1476
+ },
1477
+ children: [],
1478
+ } as CustomPartialBlock);
1479
+ index += 1;
1480
+ continue;
1481
+ }
1482
+
1459
1483
  const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
1460
1484
  if (imageMatch) {
1461
1485
  blocks.push({
@@ -0,0 +1,18 @@
1
+ export type FileDisplayUrlResolver = (fileName: string) => string;
2
+
3
+ let resolver: FileDisplayUrlResolver | null = null;
4
+
5
+ export function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null) {
6
+ resolver = fn;
7
+ }
8
+
9
+ export function resolveFileDisplayUrl(fileName: string, fallbackUrl: string): string {
10
+ if (resolver) {
11
+ return resolver(fileName);
12
+ }
13
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
14
+ if (ext) {
15
+ return `/images/file-type-icons/${ext}.svg`;
16
+ }
17
+ return fallbackUrl;
18
+ }
package/src/index.ts CHANGED
@@ -30,6 +30,12 @@ export {
30
30
  type StepImageUploadHandler,
31
31
  } from "./editor/stepImageUpload";
32
32
 
33
+ export {
34
+ setFileDisplayUrlResolver,
35
+ resolveFileDisplayUrl,
36
+ type FileDisplayUrlResolver,
37
+ } from "./editor/fileDisplayUrl";
38
+
33
39
  export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
34
40
 
35
41
  export const testomatioEditorClassName = "markdown testomatio-editor";