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.
- package/package/editor/customMarkdownConverter.js +23 -0
- package/package/editor/fileDisplayUrl.d.ts +3 -0
- package/package/editor/fileDisplayUrl.js +15 -0
- package/package/index.d.ts +1 -0
- package/package/index.js +1 -0
- package/package.json +1 -1
- package/src/App.tsx +3 -0
- package/src/editor/customMarkdownConverter.test.ts +124 -0
- package/src/editor/customMarkdownConverter.ts +24 -0
- package/src/editor/fileDisplayUrl.ts +18 -0
- package/src/index.ts +6 -0
|
@@ -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(`[](${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,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
|
+
}
|
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
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("[](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("[](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 = "[](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)";
|
|
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 = "";
|
|
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(`[](${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";
|