testomatio-editor-blocks 0.4.30 → 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 -2
- package/package/editor/customSchema.d.ts +40 -40
- package/package/editor/customSchema.js +2 -0
- package/package/editor/fileDisplayUrl.d.ts +3 -0
- package/package/editor/fileDisplayUrl.js +10 -0
- package/package/index.d.ts +1 -0
- package/package/index.js +1 -0
- package/package.json +1 -1
- package/src/editor/blocks/fileBlock.tsx +87 -0
- package/src/editor/customMarkdownConverter.test.ts +45 -9
- package/src/editor/customMarkdownConverter.ts +3 -2
- package/src/editor/customSchema.tsx +2 -0
- package/src/editor/fileDisplayUrl.ts +14 -0
- package/src/index.ts +6 -0
|
@@ -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
|
+
});
|
|
@@ -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",
|
|
@@ -271,7 +272,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
271
272
|
const name = block.props.name || "";
|
|
272
273
|
const caption = block.props.caption || "";
|
|
273
274
|
if (url) {
|
|
274
|
-
const displayUrl = caption || url;
|
|
275
|
+
const displayUrl = caption || resolveFileDisplayUrl(url);
|
|
275
276
|
lines.push(`[](${url})`);
|
|
276
277
|
}
|
|
277
278
|
return flattenWithBlankLine(lines, true);
|
|
@@ -1230,8 +1231,8 @@ export function markdownToBlocks(markdown) {
|
|
|
1230
1231
|
type: "file",
|
|
1231
1232
|
props: {
|
|
1232
1233
|
name: fileMatch[1] || "",
|
|
1233
|
-
caption: fileMatch[2] || "",
|
|
1234
1234
|
url: fileMatch[3],
|
|
1235
|
+
caption: fileMatch[2] || "",
|
|
1235
1236
|
},
|
|
1236
1237
|
children: [],
|
|
1237
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
|
},
|
package/package/index.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
+
});
|
|
@@ -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,
|
|
@@ -1515,7 +1516,7 @@ describe("markdownToBlocks", () => {
|
|
|
1515
1516
|
});
|
|
1516
1517
|
|
|
1517
1518
|
describe("file block serialization", () => {
|
|
1518
|
-
it("serializes a file block
|
|
1519
|
+
it("serializes a file block using caption as display url", () => {
|
|
1519
1520
|
const blocks: CustomEditorBlock[] = [
|
|
1520
1521
|
{
|
|
1521
1522
|
id: "1",
|
|
@@ -1534,7 +1535,7 @@ describe("file block serialization", () => {
|
|
|
1534
1535
|
expect(md).toBe("[](https://example.com/file.pdf)");
|
|
1535
1536
|
});
|
|
1536
1537
|
|
|
1537
|
-
it("
|
|
1538
|
+
it("derives icon from url when no caption is set", () => {
|
|
1538
1539
|
const blocks: CustomEditorBlock[] = [
|
|
1539
1540
|
{
|
|
1540
1541
|
id: "1",
|
|
@@ -1542,7 +1543,30 @@ describe("file block serialization", () => {
|
|
|
1542
1543
|
props: {
|
|
1543
1544
|
...baseProps,
|
|
1544
1545
|
url: "https://example.com/file.pdf",
|
|
1545
|
-
name: "
|
|
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)");
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
it("uses custom resolver when set", () => {
|
|
1558
|
+
setFileDisplayUrlResolver(() => {
|
|
1559
|
+
return `/custom-icons/file.svg`;
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
const blocks: CustomEditorBlock[] = [
|
|
1563
|
+
{
|
|
1564
|
+
id: "1",
|
|
1565
|
+
type: "file",
|
|
1566
|
+
props: {
|
|
1567
|
+
...baseProps,
|
|
1568
|
+
url: "https://example.com/file.pdf",
|
|
1569
|
+
name: "",
|
|
1546
1570
|
caption: "",
|
|
1547
1571
|
},
|
|
1548
1572
|
content: undefined as any,
|
|
@@ -1550,7 +1574,9 @@ describe("file block serialization", () => {
|
|
|
1550
1574
|
},
|
|
1551
1575
|
];
|
|
1552
1576
|
const md = blocksToMarkdown(blocks);
|
|
1553
|
-
expect(md).toBe("[](https://example.com/file.pdf)");
|
|
1578
|
+
|
|
1579
|
+
setFileDisplayUrlResolver(null);
|
|
1554
1580
|
});
|
|
1555
1581
|
|
|
1556
1582
|
it("outputs nothing when url is empty", () => {
|
|
@@ -1574,23 +1600,24 @@ describe("file block serialization", () => {
|
|
|
1574
1600
|
});
|
|
1575
1601
|
|
|
1576
1602
|
describe("file block parsing", () => {
|
|
1577
|
-
it("parses file markdown
|
|
1603
|
+
it("parses file markdown and stores display url in caption", () => {
|
|
1578
1604
|
const markdown = "[](https://example.com/file.pdf)";
|
|
1579
1605
|
const blocks = markdownToBlocks(markdown);
|
|
1580
1606
|
expect(blocks).toHaveLength(1);
|
|
1581
1607
|
expect(blocks[0].type).toBe("file");
|
|
1582
1608
|
expect((blocks[0].props as any).name).toBe("report.pdf");
|
|
1583
|
-
expect((blocks[0].props as any).caption).toBe("/images/file-type-icons/pdf.svg");
|
|
1584
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");
|
|
1585
1611
|
});
|
|
1586
1612
|
|
|
1587
1613
|
it("parses file markdown with empty name", () => {
|
|
1588
|
-
const markdown = "[](https://example.com/file.json)";
|
|
1589
1615
|
const blocks = markdownToBlocks(markdown);
|
|
1590
1616
|
expect(blocks).toHaveLength(1);
|
|
1591
1617
|
expect(blocks[0].type).toBe("file");
|
|
1592
1618
|
expect((blocks[0].props as any).name).toBe("");
|
|
1593
|
-
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");
|
|
1594
1621
|
});
|
|
1595
1622
|
|
|
1596
1623
|
it("does not confuse file blocks with image blocks", () => {
|
|
@@ -1600,7 +1627,7 @@ describe("file block parsing", () => {
|
|
|
1600
1627
|
expect(blocks[0].type).toBe("image");
|
|
1601
1628
|
});
|
|
1602
1629
|
|
|
1603
|
-
it("round-trips file blocks
|
|
1630
|
+
it("round-trips file blocks preserving icon url in caption", () => {
|
|
1604
1631
|
const blocks: CustomEditorBlock[] = [
|
|
1605
1632
|
{
|
|
1606
1633
|
id: "1",
|
|
@@ -1616,6 +1643,8 @@ describe("file block parsing", () => {
|
|
|
1616
1643
|
},
|
|
1617
1644
|
];
|
|
1618
1645
|
const md = blocksToMarkdown(blocks);
|
|
1646
|
+
expect(md).toBe("[](https://example.com/doc.xlsx)");
|
|
1647
|
+
|
|
1619
1648
|
const parsed = markdownToBlocks(md);
|
|
1620
1649
|
expect(parsed).toHaveLength(1);
|
|
1621
1650
|
expect(parsed[0].type).toBe("file");
|
|
@@ -1623,4 +1652,11 @@ describe("file block parsing", () => {
|
|
|
1623
1652
|
expect((parsed[0].props as any).name).toBe("doc.xlsx");
|
|
1624
1653
|
expect((parsed[0].props as any).caption).toBe("/images/file-type-icons/xlsx.svg");
|
|
1625
1654
|
});
|
|
1655
|
+
|
|
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);
|
|
1661
|
+
});
|
|
1626
1662
|
});
|
|
@@ -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
|
|
|
@@ -349,7 +350,7 @@ function serializeBlock(
|
|
|
349
350
|
const name = (block.props as any).name || "";
|
|
350
351
|
const caption = (block.props as any).caption || "";
|
|
351
352
|
if (url) {
|
|
352
|
-
const displayUrl = caption || url;
|
|
353
|
+
const displayUrl = caption || resolveFileDisplayUrl(url);
|
|
353
354
|
lines.push(`[](${url})`);
|
|
354
355
|
}
|
|
355
356
|
return flattenWithBlankLine(lines, true);
|
|
@@ -1472,8 +1473,8 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1472
1473
|
type: "file",
|
|
1473
1474
|
props: {
|
|
1474
1475
|
name: fileMatch[1] || "",
|
|
1475
|
-
caption: fileMatch[2] || "",
|
|
1476
1476
|
url: fileMatch[3],
|
|
1477
|
+
caption: fileMatch[2] || "",
|
|
1477
1478
|
},
|
|
1478
1479
|
children: [],
|
|
1479
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
|
},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type FileDisplayUrlResolver = (fileUrl: 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(fileUrl: string): string {
|
|
10
|
+
if (resolver) {
|
|
11
|
+
return resolver(fileUrl);
|
|
12
|
+
}
|
|
13
|
+
return `/images/file-type-icons/file.svg`;
|
|
14
|
+
}
|
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";
|