testomatio-editor-blocks 0.4.31 → 0.4.32
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/fileBlock.d.ts +40 -0
- package/package/editor/blocks/fileBlock.js +35 -0
- package/package/editor/customMarkdownConverter.js +3 -1
- package/package/editor/customSchema.d.ts +40 -40
- package/package/editor/customSchema.js +2 -0
- package/package/editor/fileDisplayUrl.d.ts +2 -2
- package/package/editor/fileDisplayUrl.js +3 -8
- package/package.json +1 -1
- package/src/editor/blocks/fileBlock.tsx +87 -0
- package/src/editor/customMarkdownConverter.test.ts +45 -22
- package/src/editor/customMarkdownConverter.ts +3 -1
- package/src/editor/customSchema.tsx +2 -0
- package/src/editor/fileDisplayUrl.ts +4 -8
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const fileBlock: {
|
|
2
|
+
config: {
|
|
3
|
+
type: "file";
|
|
4
|
+
propSchema: {
|
|
5
|
+
backgroundColor: {
|
|
6
|
+
default: "default";
|
|
7
|
+
};
|
|
8
|
+
name: {
|
|
9
|
+
default: "";
|
|
10
|
+
};
|
|
11
|
+
url: {
|
|
12
|
+
default: "";
|
|
13
|
+
};
|
|
14
|
+
caption: {
|
|
15
|
+
default: "";
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
content: "none";
|
|
19
|
+
isFileBlock: true;
|
|
20
|
+
};
|
|
21
|
+
implementation: import("@blocknote/core").TiptapBlockImplementation<{
|
|
22
|
+
type: "file";
|
|
23
|
+
propSchema: {
|
|
24
|
+
backgroundColor: {
|
|
25
|
+
default: "default";
|
|
26
|
+
};
|
|
27
|
+
name: {
|
|
28
|
+
default: "";
|
|
29
|
+
};
|
|
30
|
+
url: {
|
|
31
|
+
default: "";
|
|
32
|
+
};
|
|
33
|
+
caption: {
|
|
34
|
+
default: "";
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
content: "none";
|
|
38
|
+
isFileBlock: true;
|
|
39
|
+
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
40
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { fileBlockConfig, fileParse } from "@blocknote/core";
|
|
3
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
import { resolveFileDisplayUrl } from "../fileDisplayUrl";
|
|
6
|
+
function FileIcon(props) {
|
|
7
|
+
const { caption, url } = props.block.props;
|
|
8
|
+
const iconUrl = caption || (url ? resolveFileDisplayUrl(url) : "");
|
|
9
|
+
if (iconUrl) {
|
|
10
|
+
return _jsx("img", { src: iconUrl, alt: "", width: 24, height: 24, style: { display: "block" } });
|
|
11
|
+
}
|
|
12
|
+
return (_jsx("svg", { width: 24, height: 24, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z" }) }));
|
|
13
|
+
}
|
|
14
|
+
function FileBlockRender(props) {
|
|
15
|
+
const addFileButtonMouseDownHandler = useCallback((event) => event.preventDefault(), []);
|
|
16
|
+
const addFileButtonClickHandler = useCallback(() => {
|
|
17
|
+
props.editor.transact((tr) => tr.setMeta(props.editor.filePanel.plugins[0], {
|
|
18
|
+
block: props.block,
|
|
19
|
+
}));
|
|
20
|
+
}, [props.block, props.editor]);
|
|
21
|
+
if (props.block.props.url === "") {
|
|
22
|
+
return (_jsx("div", { className: "bn-file-block-content-wrapper", children: _jsxs("div", { className: "bn-add-file-button", onMouseDown: addFileButtonMouseDownHandler, onClick: addFileButtonClickHandler, children: [_jsx("div", { className: "bn-add-file-button-icon", children: _jsx(FileIcon, { block: props.block }) }), _jsx("div", { className: "bn-add-file-button-text", children: "Add file" })] }) }));
|
|
23
|
+
}
|
|
24
|
+
return (_jsx("div", { className: "bn-file-block-content-wrapper", children: _jsxs("div", { className: "bn-file-name-with-icon", contentEditable: false, draggable: false, children: [_jsx("div", { className: "bn-file-icon", children: _jsx(FileIcon, { block: props.block }) }), _jsx("p", { className: "bn-file-name", children: props.block.props.name || props.block.props.url })] }) }));
|
|
25
|
+
}
|
|
26
|
+
export const fileBlock = createReactBlockSpec(fileBlockConfig, {
|
|
27
|
+
render: FileBlockRender,
|
|
28
|
+
parse: fileParse,
|
|
29
|
+
toExternalHTML: (props) => {
|
|
30
|
+
if (!props.block.props.url) {
|
|
31
|
+
return _jsx("p", { children: "Add file" });
|
|
32
|
+
}
|
|
33
|
+
return (_jsx("a", { href: props.block.props.url, children: props.block.props.name || props.block.props.url }));
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -270,8 +270,9 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
270
270
|
case "file": {
|
|
271
271
|
const url = block.props.url || "";
|
|
272
272
|
const name = block.props.name || "";
|
|
273
|
+
const caption = block.props.caption || "";
|
|
273
274
|
if (url) {
|
|
274
|
-
const displayUrl = resolveFileDisplayUrl(
|
|
275
|
+
const displayUrl = caption || resolveFileDisplayUrl(url);
|
|
275
276
|
lines.push(`[](${url})`);
|
|
276
277
|
}
|
|
277
278
|
return flattenWithBlankLine(lines, true);
|
|
@@ -1231,6 +1232,7 @@ export function markdownToBlocks(markdown) {
|
|
|
1231
1232
|
props: {
|
|
1232
1233
|
name: fileMatch[1] || "",
|
|
1233
1234
|
url: fileMatch[3],
|
|
1235
|
+
caption: fileMatch[2] || "",
|
|
1234
1236
|
},
|
|
1235
1237
|
children: [],
|
|
1236
1238
|
});
|
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { BlockNoteSchema } from "@blocknote/core";
|
|
2
2
|
import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
|
|
3
3
|
export declare const customSchema: BlockNoteSchema<import("@blocknote/core").BlockSchemaFromSpecs<{
|
|
4
|
+
file: {
|
|
5
|
+
config: {
|
|
6
|
+
type: "file";
|
|
7
|
+
propSchema: {
|
|
8
|
+
backgroundColor: {
|
|
9
|
+
default: "default";
|
|
10
|
+
};
|
|
11
|
+
name: {
|
|
12
|
+
default: "";
|
|
13
|
+
};
|
|
14
|
+
url: {
|
|
15
|
+
default: "";
|
|
16
|
+
};
|
|
17
|
+
caption: {
|
|
18
|
+
default: "";
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
content: "none";
|
|
22
|
+
isFileBlock: true;
|
|
23
|
+
};
|
|
24
|
+
implementation: import("@blocknote/core").TiptapBlockImplementation<{
|
|
25
|
+
type: "file";
|
|
26
|
+
propSchema: {
|
|
27
|
+
backgroundColor: {
|
|
28
|
+
default: "default";
|
|
29
|
+
};
|
|
30
|
+
name: {
|
|
31
|
+
default: "";
|
|
32
|
+
};
|
|
33
|
+
url: {
|
|
34
|
+
default: "";
|
|
35
|
+
};
|
|
36
|
+
caption: {
|
|
37
|
+
default: "";
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
content: "none";
|
|
41
|
+
isFileBlock: true;
|
|
42
|
+
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
43
|
+
};
|
|
4
44
|
testStep: {
|
|
5
45
|
config: {
|
|
6
46
|
readonly type: "testStep";
|
|
@@ -343,46 +383,6 @@ export declare const customSchema: BlockNoteSchema<import("@blocknote/core").Blo
|
|
|
343
383
|
};
|
|
344
384
|
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
345
385
|
};
|
|
346
|
-
file: {
|
|
347
|
-
config: {
|
|
348
|
-
type: "file";
|
|
349
|
-
propSchema: {
|
|
350
|
-
backgroundColor: {
|
|
351
|
-
default: "default";
|
|
352
|
-
};
|
|
353
|
-
name: {
|
|
354
|
-
default: "";
|
|
355
|
-
};
|
|
356
|
-
url: {
|
|
357
|
-
default: "";
|
|
358
|
-
};
|
|
359
|
-
caption: {
|
|
360
|
-
default: "";
|
|
361
|
-
};
|
|
362
|
-
};
|
|
363
|
-
content: "none";
|
|
364
|
-
isFileBlock: true;
|
|
365
|
-
};
|
|
366
|
-
implementation: import("@blocknote/core").TiptapBlockImplementation<{
|
|
367
|
-
type: "file";
|
|
368
|
-
propSchema: {
|
|
369
|
-
backgroundColor: {
|
|
370
|
-
default: "default";
|
|
371
|
-
};
|
|
372
|
-
name: {
|
|
373
|
-
default: "";
|
|
374
|
-
};
|
|
375
|
-
url: {
|
|
376
|
-
default: "";
|
|
377
|
-
};
|
|
378
|
-
caption: {
|
|
379
|
-
default: "";
|
|
380
|
-
};
|
|
381
|
-
};
|
|
382
|
-
content: "none";
|
|
383
|
-
isFileBlock: true;
|
|
384
|
-
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
385
|
-
};
|
|
386
386
|
image: {
|
|
387
387
|
config: {
|
|
388
388
|
type: "image";
|
|
@@ -2,10 +2,12 @@ import { defaultBlockSpecs } from "@blocknote/core";
|
|
|
2
2
|
import { BlockNoteSchema } from "@blocknote/core";
|
|
3
3
|
import { stepBlock } from "./blocks/step";
|
|
4
4
|
import { snippetBlock } from "./blocks/snippet";
|
|
5
|
+
import { fileBlock } from "./blocks/fileBlock";
|
|
5
6
|
import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
|
|
6
7
|
export const customSchema = BlockNoteSchema.create({
|
|
7
8
|
blockSpecs: {
|
|
8
9
|
...defaultBlockSpecs,
|
|
10
|
+
file: fileBlock,
|
|
9
11
|
testStep: stepBlock,
|
|
10
12
|
snippet: snippetBlock,
|
|
11
13
|
},
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type FileDisplayUrlResolver = (
|
|
1
|
+
export type FileDisplayUrlResolver = (fileUrl: string) => string;
|
|
2
2
|
export declare function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null): void;
|
|
3
|
-
export declare function resolveFileDisplayUrl(
|
|
3
|
+
export declare function resolveFileDisplayUrl(fileUrl: string): string;
|
|
@@ -2,14 +2,9 @@ let resolver = null;
|
|
|
2
2
|
export function setFileDisplayUrlResolver(fn) {
|
|
3
3
|
resolver = fn;
|
|
4
4
|
}
|
|
5
|
-
export function resolveFileDisplayUrl(
|
|
6
|
-
var _a;
|
|
5
|
+
export function resolveFileDisplayUrl(fileUrl) {
|
|
7
6
|
if (resolver) {
|
|
8
|
-
return resolver(
|
|
7
|
+
return resolver(fileUrl);
|
|
9
8
|
}
|
|
10
|
-
|
|
11
|
-
if (ext) {
|
|
12
|
-
return `/images/file-type-icons/${ext}.svg`;
|
|
13
|
-
}
|
|
14
|
-
return fallbackUrl;
|
|
9
|
+
return `/images/file-type-icons/file.svg`;
|
|
15
10
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { fileBlockConfig, fileParse } from "@blocknote/core";
|
|
2
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
3
|
+
import type { ReactCustomBlockRenderProps } from "@blocknote/react";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
import { resolveFileDisplayUrl } from "../fileDisplayUrl";
|
|
6
|
+
|
|
7
|
+
function FileIcon(props: { block: { props: { caption?: string; url?: string } } }) {
|
|
8
|
+
const { caption, url } = props.block.props;
|
|
9
|
+
const iconUrl = caption || (url ? resolveFileDisplayUrl(url) : "");
|
|
10
|
+
|
|
11
|
+
if (iconUrl) {
|
|
12
|
+
return <img src={iconUrl} alt="" width={24} height={24} style={{ display: "block" }} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<svg width={24} height={24} viewBox="0 0 24 24" fill="currentColor">
|
|
17
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function FileBlockRender(
|
|
23
|
+
props: ReactCustomBlockRenderProps<typeof fileBlockConfig, any, any>,
|
|
24
|
+
) {
|
|
25
|
+
const addFileButtonMouseDownHandler = useCallback(
|
|
26
|
+
(event: React.MouseEvent) => event.preventDefault(),
|
|
27
|
+
[],
|
|
28
|
+
);
|
|
29
|
+
const addFileButtonClickHandler = useCallback(() => {
|
|
30
|
+
props.editor.transact((tr: any) =>
|
|
31
|
+
tr.setMeta(props.editor.filePanel!.plugins[0], {
|
|
32
|
+
block: props.block,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}, [props.block, props.editor]);
|
|
36
|
+
|
|
37
|
+
if (props.block.props.url === "") {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className="bn-file-block-content-wrapper"
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
className="bn-add-file-button"
|
|
44
|
+
onMouseDown={addFileButtonMouseDownHandler}
|
|
45
|
+
onClick={addFileButtonClickHandler}
|
|
46
|
+
>
|
|
47
|
+
<div className="bn-add-file-button-icon">
|
|
48
|
+
<FileIcon block={props.block} />
|
|
49
|
+
</div>
|
|
50
|
+
<div className="bn-add-file-button-text">Add file</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="bn-file-block-content-wrapper">
|
|
58
|
+
<div
|
|
59
|
+
className="bn-file-name-with-icon"
|
|
60
|
+
contentEditable={false}
|
|
61
|
+
draggable={false}
|
|
62
|
+
>
|
|
63
|
+
<div className="bn-file-icon">
|
|
64
|
+
<FileIcon block={props.block} />
|
|
65
|
+
</div>
|
|
66
|
+
<p className="bn-file-name">
|
|
67
|
+
{props.block.props.name || props.block.props.url}
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const fileBlock = createReactBlockSpec(fileBlockConfig, {
|
|
75
|
+
render: FileBlockRender,
|
|
76
|
+
parse: fileParse,
|
|
77
|
+
toExternalHTML: (props) => {
|
|
78
|
+
if (!props.block.props.url) {
|
|
79
|
+
return <p>Add file</p>;
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<a href={props.block.props.url}>
|
|
83
|
+
{props.block.props.name || props.block.props.url}
|
|
84
|
+
</a>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -1516,12 +1516,7 @@ describe("markdownToBlocks", () => {
|
|
|
1516
1516
|
});
|
|
1517
1517
|
|
|
1518
1518
|
describe("file block serialization", () => {
|
|
1519
|
-
it("serializes a file block using display url
|
|
1520
|
-
setFileDisplayUrlResolver((name: string) => {
|
|
1521
|
-
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
1522
|
-
return `/images/file-type-icons/${ext}.svg`;
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1519
|
+
it("serializes a file block using caption as display url", () => {
|
|
1525
1520
|
const blocks: CustomEditorBlock[] = [
|
|
1526
1521
|
{
|
|
1527
1522
|
id: "1",
|
|
@@ -1530,7 +1525,7 @@ describe("file block serialization", () => {
|
|
|
1530
1525
|
...baseProps,
|
|
1531
1526
|
url: "https://example.com/file.pdf",
|
|
1532
1527
|
name: "report.pdf",
|
|
1533
|
-
caption: "",
|
|
1528
|
+
caption: "/images/file-type-icons/pdf.svg",
|
|
1534
1529
|
},
|
|
1535
1530
|
content: undefined as any,
|
|
1536
1531
|
children: [],
|
|
@@ -1538,11 +1533,32 @@ describe("file block serialization", () => {
|
|
|
1538
1533
|
];
|
|
1539
1534
|
const md = blocksToMarkdown(blocks);
|
|
1540
1535
|
expect(md).toBe("[](https://example.com/file.pdf)");
|
|
1536
|
+
});
|
|
1541
1537
|
|
|
1542
|
-
|
|
1538
|
+
it("derives icon from url when no caption is set", () => {
|
|
1539
|
+
const blocks: CustomEditorBlock[] = [
|
|
1540
|
+
{
|
|
1541
|
+
id: "1",
|
|
1542
|
+
type: "file",
|
|
1543
|
+
props: {
|
|
1544
|
+
...baseProps,
|
|
1545
|
+
url: "https://example.com/file.pdf",
|
|
1546
|
+
name: "",
|
|
1547
|
+
caption: "",
|
|
1548
|
+
},
|
|
1549
|
+
content: undefined as any,
|
|
1550
|
+
children: [],
|
|
1551
|
+
},
|
|
1552
|
+
];
|
|
1553
|
+
const md = blocksToMarkdown(blocks);
|
|
1554
|
+
expect(md).toBe("[](https://example.com/file.pdf)");
|
|
1543
1555
|
});
|
|
1544
1556
|
|
|
1545
|
-
it("
|
|
1557
|
+
it("uses custom resolver when set", () => {
|
|
1558
|
+
setFileDisplayUrlResolver(() => {
|
|
1559
|
+
return `/custom-icons/file.svg`;
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1546
1562
|
const blocks: CustomEditorBlock[] = [
|
|
1547
1563
|
{
|
|
1548
1564
|
id: "1",
|
|
@@ -1550,7 +1566,7 @@ describe("file block serialization", () => {
|
|
|
1550
1566
|
props: {
|
|
1551
1567
|
...baseProps,
|
|
1552
1568
|
url: "https://example.com/file.pdf",
|
|
1553
|
-
name: "
|
|
1569
|
+
name: "",
|
|
1554
1570
|
caption: "",
|
|
1555
1571
|
},
|
|
1556
1572
|
content: undefined as any,
|
|
@@ -1558,7 +1574,9 @@ describe("file block serialization", () => {
|
|
|
1558
1574
|
},
|
|
1559
1575
|
];
|
|
1560
1576
|
const md = blocksToMarkdown(blocks);
|
|
1561
|
-
expect(md).toBe("[](https://example.com/file.pdf)");
|
|
1578
|
+
|
|
1579
|
+
setFileDisplayUrlResolver(null);
|
|
1562
1580
|
});
|
|
1563
1581
|
|
|
1564
1582
|
it("outputs nothing when url is empty", () => {
|
|
@@ -1582,22 +1600,24 @@ describe("file block serialization", () => {
|
|
|
1582
1600
|
});
|
|
1583
1601
|
|
|
1584
1602
|
describe("file block parsing", () => {
|
|
1585
|
-
it("parses file markdown
|
|
1603
|
+
it("parses file markdown and stores display url in caption", () => {
|
|
1586
1604
|
const markdown = "[](https://example.com/file.pdf)";
|
|
1587
1605
|
const blocks = markdownToBlocks(markdown);
|
|
1588
1606
|
expect(blocks).toHaveLength(1);
|
|
1589
1607
|
expect(blocks[0].type).toBe("file");
|
|
1590
1608
|
expect((blocks[0].props as any).name).toBe("report.pdf");
|
|
1591
1609
|
expect((blocks[0].props as any).url).toBe("https://example.com/file.pdf");
|
|
1610
|
+
expect((blocks[0].props as any).caption).toBe("/images/file-type-icons/pdf.svg");
|
|
1592
1611
|
});
|
|
1593
1612
|
|
|
1594
1613
|
it("parses file markdown with empty name", () => {
|
|
1595
|
-
const markdown = "[](https://example.com/file.json)";
|
|
1596
1615
|
const blocks = markdownToBlocks(markdown);
|
|
1597
1616
|
expect(blocks).toHaveLength(1);
|
|
1598
1617
|
expect(blocks[0].type).toBe("file");
|
|
1599
1618
|
expect((blocks[0].props as any).name).toBe("");
|
|
1600
|
-
expect((blocks[0].props as any).url).toBe("https://example.com/
|
|
1619
|
+
expect((blocks[0].props as any).url).toBe("https://example.com/file.json");
|
|
1620
|
+
expect((blocks[0].props as any).caption).toBe("/images/file-type-icons/json.svg");
|
|
1601
1621
|
});
|
|
1602
1622
|
|
|
1603
1623
|
it("does not confuse file blocks with image blocks", () => {
|
|
@@ -1607,12 +1627,7 @@ describe("file block parsing", () => {
|
|
|
1607
1627
|
expect(blocks[0].type).toBe("image");
|
|
1608
1628
|
});
|
|
1609
1629
|
|
|
1610
|
-
it("round-trips file blocks
|
|
1611
|
-
setFileDisplayUrlResolver((name: string) => {
|
|
1612
|
-
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
1613
|
-
return `/images/file-type-icons/${ext}.svg`;
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1630
|
+
it("round-trips file blocks preserving icon url in caption", () => {
|
|
1616
1631
|
const blocks: CustomEditorBlock[] = [
|
|
1617
1632
|
{
|
|
1618
1633
|
id: "1",
|
|
@@ -1621,19 +1636,27 @@ describe("file block parsing", () => {
|
|
|
1621
1636
|
...baseProps,
|
|
1622
1637
|
url: "https://example.com/doc.xlsx",
|
|
1623
1638
|
name: "doc.xlsx",
|
|
1624
|
-
caption: "",
|
|
1639
|
+
caption: "/images/file-type-icons/xlsx.svg",
|
|
1625
1640
|
},
|
|
1626
1641
|
content: undefined as any,
|
|
1627
1642
|
children: [],
|
|
1628
1643
|
},
|
|
1629
1644
|
];
|
|
1630
1645
|
const md = blocksToMarkdown(blocks);
|
|
1646
|
+
expect(md).toBe("[](https://example.com/doc.xlsx)");
|
|
1647
|
+
|
|
1631
1648
|
const parsed = markdownToBlocks(md);
|
|
1632
1649
|
expect(parsed).toHaveLength(1);
|
|
1633
1650
|
expect(parsed[0].type).toBe("file");
|
|
1634
1651
|
expect((parsed[0].props as any).url).toBe("https://example.com/doc.xlsx");
|
|
1635
1652
|
expect((parsed[0].props as any).name).toBe("doc.xlsx");
|
|
1653
|
+
expect((parsed[0].props as any).caption).toBe("/images/file-type-icons/xlsx.svg");
|
|
1654
|
+
});
|
|
1636
1655
|
|
|
1637
|
-
|
|
1656
|
+
it("round-trips file blocks without name", () => {
|
|
1657
|
+
const markdown = "[](https://example.com/data.json)";
|
|
1658
|
+
const parsed = markdownToBlocks(markdown);
|
|
1659
|
+
const md = blocksToMarkdown(parsed as CustomEditorBlock[]);
|
|
1660
|
+
expect(md).toBe(markdown);
|
|
1638
1661
|
});
|
|
1639
1662
|
});
|
|
@@ -348,8 +348,9 @@ function serializeBlock(
|
|
|
348
348
|
case "file": {
|
|
349
349
|
const url = (block.props as any).url || "";
|
|
350
350
|
const name = (block.props as any).name || "";
|
|
351
|
+
const caption = (block.props as any).caption || "";
|
|
351
352
|
if (url) {
|
|
352
|
-
const displayUrl = resolveFileDisplayUrl(
|
|
353
|
+
const displayUrl = caption || resolveFileDisplayUrl(url);
|
|
353
354
|
lines.push(`[](${url})`);
|
|
354
355
|
}
|
|
355
356
|
return flattenWithBlankLine(lines, true);
|
|
@@ -1473,6 +1474,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1473
1474
|
props: {
|
|
1474
1475
|
name: fileMatch[1] || "",
|
|
1475
1476
|
url: fileMatch[3],
|
|
1477
|
+
caption: fileMatch[2] || "",
|
|
1476
1478
|
},
|
|
1477
1479
|
children: [],
|
|
1478
1480
|
} as CustomPartialBlock);
|
|
@@ -2,11 +2,13 @@ import { defaultBlockSpecs } from "@blocknote/core";
|
|
|
2
2
|
import { BlockNoteSchema } from "@blocknote/core";
|
|
3
3
|
import { stepBlock } from "./blocks/step";
|
|
4
4
|
import { snippetBlock } from "./blocks/snippet";
|
|
5
|
+
import { fileBlock } from "./blocks/fileBlock";
|
|
5
6
|
import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
|
|
6
7
|
|
|
7
8
|
export const customSchema = BlockNoteSchema.create({
|
|
8
9
|
blockSpecs: {
|
|
9
10
|
...defaultBlockSpecs,
|
|
11
|
+
file: fileBlock,
|
|
10
12
|
testStep: stepBlock,
|
|
11
13
|
snippet: snippetBlock,
|
|
12
14
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FileDisplayUrlResolver = (
|
|
1
|
+
export type FileDisplayUrlResolver = (fileUrl: string) => string;
|
|
2
2
|
|
|
3
3
|
let resolver: FileDisplayUrlResolver | null = null;
|
|
4
4
|
|
|
@@ -6,13 +6,9 @@ export function setFileDisplayUrlResolver(fn: FileDisplayUrlResolver | null) {
|
|
|
6
6
|
resolver = fn;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function resolveFileDisplayUrl(
|
|
9
|
+
export function resolveFileDisplayUrl(fileUrl: string): string {
|
|
10
10
|
if (resolver) {
|
|
11
|
-
return resolver(
|
|
11
|
+
return resolver(fileUrl);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
if (ext) {
|
|
15
|
-
return `/images/file-type-icons/${ext}.svg`;
|
|
16
|
-
}
|
|
17
|
-
return fallbackUrl;
|
|
13
|
+
return `/images/file-type-icons/file.svg`;
|
|
18
14
|
}
|