diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json b/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json index a98734f2a6..6115e659a5 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json @@ -5,6 +5,7 @@ "tags": ["Interoperability"], "dependencies": { "@blocknote/xl-pdf-exporter": "latest", + "@blocknote/xl-multi-column": "latest", "@react-pdf/renderer": "^4.3.0" }, "pro": true diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/App.tsx b/examples/05-interoperability/05-converting-blocks-to-pdf/App.tsx index 386649d509..5c3da5e063 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/App.tsx +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/App.tsx @@ -4,6 +4,7 @@ import { filterSuggestionItems, withPageBreak, } from "@blocknote/core"; +import * as locales from "@blocknote/core/locales"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; @@ -17,6 +18,12 @@ import { PDFExporter, pdfDefaultSchemaMappings, } from "@blocknote/xl-pdf-exporter"; +import { + getMultiColumnSlashMenuItems, + multiColumnDropCursor, + locales as multiColumnLocales, + withMultiColumn, +} from "@blocknote/xl-multi-column"; import { PDFViewer } from "@react-pdf/renderer"; import { useEffect, useMemo, useState } from "react"; @@ -28,7 +35,12 @@ export default function App() { // Creates a new editor instance with some initial content. const editor = useCreateBlockNote({ - schema: withPageBreak(BlockNoteSchema.create()), + schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + dropCursor: multiColumnDropCursor, + dictionary: { + ...locales.en, + multi_column: multiColumnLocales.en, + }, tables: { splitCells: true, cellBackgroundColor: true, @@ -313,9 +325,72 @@ export default function App() { console.log("Hello World", message); };`, }, + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", + }, + ], + }, + { + type: "column", + props: { + width: 1.4, + }, + children: [ + { + type: "heading", + content: "So is this heading!", + }, + ], + }, + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, ], }); - + const getSlashMenuItems = useMemo(() => { + return async (query: string) => + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + getMultiColumnSlashMenuItems(editor), + ), + query, + ); + }, [editor]); const onChange = async () => { const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings); // Converts the editor's contents from Block objects to HTML and store to state. @@ -330,13 +405,6 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const slashMenuItems = useMemo(() => { - return combineByGroup( - getDefaultReactSlashMenuItems(editor), - getPageBreakReactSlashMenuItems(editor), - ); - }, [editor]); - // Renders the editor instance, and its contents as HTML below. return (
@@ -344,9 +412,7 @@ export default function App() { - filterSuggestionItems(slashMenuItems, query) - } + getItems={getSlashMenuItems} />
diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json index a20a4011c4..7207a6045c 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json @@ -18,6 +18,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "@blocknote/xl-pdf-exporter": "latest", + "@blocknote/xl-multi-column": "latest", "@react-pdf/renderer": "^4.3.0" }, "devDependencies": { diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json b/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json index 61ea12521a..e823472a1b 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json +++ b/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json @@ -5,6 +5,7 @@ "tags": [""], "dependencies": { "@blocknote/xl-docx-exporter": "latest", + "@blocknote/xl-multi-column": "latest", "docx": "^9.0.2" }, "pro": true diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx b/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx index 8839243558..597f3793b7 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx +++ b/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx @@ -4,6 +4,7 @@ import { filterSuggestionItems, withPageBreak, } from "@blocknote/core"; +import * as locales from "@blocknote/core/locales"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; @@ -17,6 +18,12 @@ import { DOCXExporter, docxDefaultSchemaMappings, } from "@blocknote/xl-docx-exporter"; +import { + getMultiColumnSlashMenuItems, + multiColumnDropCursor, + locales as multiColumnLocales, + withMultiColumn, +} from "@blocknote/xl-multi-column"; import { useMemo } from "react"; import "./styles.css"; @@ -24,7 +31,12 @@ import "./styles.css"; export default function App() { // Creates a new editor instance with some initial content. const editor = useCreateBlockNote({ - schema: withPageBreak(BlockNoteSchema.create()), + schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + dropCursor: multiColumnDropCursor, + dictionary: { + ...locales.en, + multi_column: multiColumnLocales.en, + }, tables: { splitCells: true, cellBackgroundColor: true, @@ -309,9 +321,73 @@ export default function App() { console.log("Hello World", message); };`, }, + + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", + }, + ], + }, + { + type: "column", + props: { + width: 1.4, + }, + children: [ + { + type: "heading", + content: "So is this heading!", + }, + ], + }, + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, ], }); - + const getSlashMenuItems = useMemo(() => { + return async (query: string) => + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + getMultiColumnSlashMenuItems(editor), + ), + query, + ); + }, [editor]); const onDownloadClick = async () => { const exporter = new DOCXExporter(editor.schema, docxDefaultSchemaMappings); @@ -332,13 +408,6 @@ export default function App() { window.URL.revokeObjectURL(link.href); }; - const slashMenuItems = useMemo(() => { - return combineByGroup( - getDefaultReactSlashMenuItems(editor), - getPageBreakReactSlashMenuItems(editor), - ); - }, [editor]); - // Renders the editor instance, and its contents as HTML below. return (
@@ -351,9 +420,7 @@ export default function App() { - filterSuggestionItems(slashMenuItems, query) - } + getItems={getSlashMenuItems} />
diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/package.json b/examples/05-interoperability/06-converting-blocks-to-docx/package.json index 092a4761c2..7dc4f636b0 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/package.json +++ b/examples/05-interoperability/06-converting-blocks-to-docx/package.json @@ -18,6 +18,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "@blocknote/xl-docx-exporter": "latest", + "@blocknote/xl-multi-column": "latest", "docx": "^9.0.2" }, "devDependencies": { diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json b/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json index b17ea0f26b..7e3174aeea 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json +++ b/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json @@ -4,7 +4,8 @@ "author": "areknawo", "tags": [""], "dependencies": { - "@blocknote/xl-odt-exporter": "latest" + "@blocknote/xl-odt-exporter": "latest", + "@blocknote/xl-multi-column": "latest" }, "pro": true } diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/App.tsx b/examples/05-interoperability/07-converting-blocks-to-odt/App.tsx index cb99a040e1..b346e939a1 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/App.tsx +++ b/examples/05-interoperability/07-converting-blocks-to-odt/App.tsx @@ -4,6 +4,7 @@ import { filterSuggestionItems, withPageBreak, } from "@blocknote/core"; +import * as locales from "@blocknote/core/locales"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; @@ -17,6 +18,12 @@ import { ODTExporter, odtDefaultSchemaMappings, } from "@blocknote/xl-odt-exporter"; +import { + getMultiColumnSlashMenuItems, + multiColumnDropCursor, + locales as multiColumnLocales, + withMultiColumn, +} from "@blocknote/xl-multi-column"; import { useMemo } from "react"; import "./styles.css"; @@ -24,7 +31,12 @@ import "./styles.css"; export default function App() { // Creates a new editor instance with some initial content. const editor = useCreateBlockNote({ - schema: withPageBreak(BlockNoteSchema.create()), + schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + dropCursor: multiColumnDropCursor, + dictionary: { + ...locales.en, + multi_column: multiColumnLocales.en, + }, tables: { splitCells: true, cellTextColor: true, @@ -308,9 +320,72 @@ export default function App() { console.log("Hello World", message); };`, }, + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", + }, + ], + }, + { + type: "column", + props: { + width: 1.4, + }, + children: [ + { + type: "heading", + content: "So is this heading!", + }, + ], + }, + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, ], }); - + const getSlashMenuItems = useMemo(() => { + return async (query: string) => + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + getMultiColumnSlashMenuItems(editor), + ), + query, + ); + }, [editor]); const onDownloadClick = async () => { const exporter = new ODTExporter(editor.schema, odtDefaultSchemaMappings); @@ -331,13 +406,6 @@ export default function App() { window.URL.revokeObjectURL(link.href); }; - const slashMenuItems = useMemo(() => { - return combineByGroup( - getDefaultReactSlashMenuItems(editor), - getPageBreakReactSlashMenuItems(editor), - ); - }, [editor]); - // Renders the editor instance, and its contents as HTML below. return (
@@ -350,9 +418,7 @@ export default function App() { - filterSuggestionItems(slashMenuItems, query) - } + getItems={getSlashMenuItems} />
diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/package.json b/examples/05-interoperability/07-converting-blocks-to-odt/package.json index 4318d50d5b..d1092da393 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/package.json +++ b/examples/05-interoperability/07-converting-blocks-to-odt/package.json @@ -17,7 +17,8 @@ "@blocknote/shadcn": "latest", "react": "^18.3.1", "react-dom": "^18.3.1", - "@blocknote/xl-odt-exporter": "latest" + "@blocknote/xl-odt-exporter": "latest", + "@blocknote/xl-multi-column": "latest" }, "devDependencies": { "@types/react": "^18.0.25", diff --git a/packages/core/src/exporter/Exporter.ts b/packages/core/src/exporter/Exporter.ts index 75cf9a1705..2b1aa65050 100644 --- a/packages/core/src/exporter/Exporter.ts +++ b/packages/core/src/exporter/Exporter.ts @@ -90,12 +90,14 @@ export abstract class Exporter< block: BlockFromConfig, nestingLevel: number, numberedListIndex: number, + children?: Array>, ) { return this.mappings.blockMapping[block.type]( block, this, nestingLevel, numberedListIndex, + children, ); } } diff --git a/packages/core/src/exporter/mapping.ts b/packages/core/src/exporter/mapping.ts index 2700d279ea..6790747169 100644 --- a/packages/core/src/exporter/mapping.ts +++ b/packages/core/src/exporter/mapping.ts @@ -27,6 +27,7 @@ export type BlockMapping< exporter: Exporter, nestingLevel: number, numberedListIndex?: number, + children?: Array>, ) => RB | Promise; }; diff --git a/packages/xl-docx-exporter/package.json b/packages/xl-docx-exporter/package.json index 5975fcec5d..7734081304 100644 --- a/packages/xl-docx-exporter/package.json +++ b/packages/xl-docx-exporter/package.json @@ -58,6 +58,7 @@ }, "dependencies": { "@blocknote/core": "0.33.0", + "@blocknote/xl-multi-column": "0.33.0", "buffer": "^6.0.3", "docx": "^9.0.2", "image-meta": "^0.2.1" diff --git a/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml b/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml index 3e8d279cc8..06e915d628 100644 --- a/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml +++ b/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml @@ -418,7 +418,7 @@ - + @@ -471,7 +471,7 @@ - + diff --git a/packages/xl-docx-exporter/src/docx/__snapshots__/withCustomOptions/document.xml.rels b/packages/xl-docx-exporter/src/docx/__snapshots__/withCustomOptions/document.xml.rels index df89e73844..04d8b5e497 100644 --- a/packages/xl-docx-exporter/src/docx/__snapshots__/withCustomOptions/document.xml.rels +++ b/packages/xl-docx-exporter/src/docx/__snapshots__/withCustomOptions/document.xml.rels @@ -7,7 +7,6 @@ - diff --git a/packages/xl-docx-exporter/src/docx/__snapshots__/withMultiColumn/document.xml b/packages/xl-docx-exporter/src/docx/__snapshots__/withMultiColumn/document.xml new file mode 100644 index 0000000000..4c3b378ced --- /dev/null +++ b/packages/xl-docx-exporter/src/docx/__snapshots__/withMultiColumn/document.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This paragraph is in a column! + + + + + + + + + + + + + So is this heading! + + + + + + + + + + + + + + + + You can have multiple blocks in a column too + + + + + + + + + + + + Block 1 + + + + + + + + + + + + Block 2 + + + + + + + + + + + + Block 3 + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/xl-docx-exporter/src/docx/__snapshots__/withMultiColumn/styles.xml b/packages/xl-docx-exporter/src/docx/__snapshots__/withMultiColumn/styles.xml new file mode 100644 index 0000000000..d23b5bb2ff --- /dev/null +++ b/packages/xl-docx-exporter/src/docx/__snapshots__/withMultiColumn/styles.xml @@ -0,0 +1,960 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts b/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts index 7d5cce4c1c..f1f914a336 100644 --- a/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts +++ b/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts @@ -18,9 +18,12 @@ import { Paragraph, ParagraphChild, ShadingType, + TableCell, + TableRow, TextRun, } from "docx"; import { Table } from "../util/Table.js"; +import { multiColumnSchema } from "@blocknote/xl-multi-column"; function blockPropsToStyles( props: Partial, @@ -58,7 +61,9 @@ function blockPropsToStyles( }; } export const docxBlockMappingForDefaultSchema: BlockMapping< - DefaultBlockSchema & typeof pageBreakSchema.blockSchema, + DefaultBlockSchema & + typeof pageBreakSchema.blockSchema & + typeof multiColumnSchema.blockSchema, any, any, | Promise @@ -187,6 +192,55 @@ export const docxBlockMappingForDefaultSchema: BlockMapping< children: [new PageBreak()], }); }, + column: (block, _exporter, _nestingLevel, _numberedListIndex, children) => { + return new TableCell({ + width: { + size: `${block.props.width * 100}%`, + type: "pct", + }, + children: (children || []).flatMap((child) => { + if (Array.isArray(child)) { + return child; + } + + return [child]; + }), + }) as any; + }, + columnList: ( + _block, + _exporter, + _nestingLevel, + _numberedListIndex, + children, + ) => { + return new DocxTable({ + layout: "autofit", + borders: { + bottom: { style: "nil" }, + top: { style: "nil" }, + left: { style: "nil" }, + right: { style: "nil" }, + insideHorizontal: { style: "nil" }, + insideVertical: { style: "nil" }, + }, + rows: [ + new TableRow({ + children: (children as unknown as TableCell[]).map( + (cell, _index, children) => { + return new TableCell({ + width: { + size: `${(parseFloat(`${cell.options.width?.size || "100%"}`) / (children.length * 100)) * 100}%`, + type: "pct", + }, + children: cell.options.children, + }); + }, + ), + }), + ], + }); + }, image: async (block, exporter) => { const blob = await exporter.resolveFile(block.props.url); const { width, height } = await getImageDimensions(blob); diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts index 813b826eb4..d9d3adeb2b 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts @@ -6,6 +6,8 @@ import { describe, expect, it } from "vitest"; import xmlFormat from "xml-formatter"; import { docxDefaultSchemaMappings } from "./defaultSchema/index.js"; import { DOCXExporter } from "./docxExporter.js"; +import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; +import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; const getZIPEntryContent = (entries: Entry[], fileName: string) => { const entry = entries.find((entry) => { @@ -109,6 +111,90 @@ describe("exporter", () => { ).toMatchFileSnapshot("__snapshots__/withCustomOptions/core.xml"); }, ); + + it( + "should export a document with a multi-column block", + { timeout: 10000 }, + async () => { + const schema = BlockNoteSchema.create({ + blockSpecs: { + ...defaultBlockSpecs, + pageBreak: PageBreak, + column: ColumnBlock, + columnList: ColumnListBlock, + }, + }); + const exporter = new DOCXExporter(schema, docxDefaultSchemaMappings); + const doc = await exporter.toDocxJsDocument( + partialBlocksToBlocksForTesting(schema, [ + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", + }, + ], + }, + { + type: "column", + props: { + width: 1.4, + }, + children: [ + { + type: "heading", + content: "So is this heading!", + }, + ], + }, + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, + ]), + ); + + const blob = await Packer.toBlob(doc); + const zip = new ZipReader(new BlobReader(blob)); + const entries = await zip.getEntries(); + + await expect( + prettify(await getZIPEntryContent(entries, "word/document.xml")), + ).toMatchFileSnapshot("__snapshots__/withMultiColumn/document.xml"); + await expect( + prettify(await getZIPEntryContent(entries, "word/styles.xml")), + ).toMatchFileSnapshot("__snapshots__/withMultiColumn/styles.xml"); + }, + ); }); function prettify(sourceXml: string) { diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.ts b/packages/xl-docx-exporter/src/docx/docxExporter.ts index a9f7f07d56..9e2ddfd6b6 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.ts @@ -114,22 +114,33 @@ export class DOCXExporter< for (const b of blocks) { let children = await this.transformBlocks(b.children, nestingLevel + 1); - children = children.map((c, _i) => { - // NOTE: nested tables not supported (we can't insert the new Tab before a table) - if ( - c instanceof Paragraph && - !(c as any).properties.numberingReferences.length - ) { - c.addRunToFront( - new TextRun({ - children: [new Tab()], - }), - ); - } - return c; - }); - const self = await this.mapBlock(b as any, nestingLevel, 0 /*unused*/); // TODO: any - if (Array.isArray(self)) { + + if (!["columnList", "column"].includes(b.type)) { + children = children.map((c, _i) => { + // NOTE: nested tables not supported (we can't insert the new Tab before a table) + if ( + c instanceof Paragraph && + !(c as any).properties.numberingReferences.length + ) { + c.addRunToFront( + new TextRun({ + children: [new Tab()], + }), + ); + } + return c; + }); + } + + const self = await this.mapBlock( + b as any, + nestingLevel, + 0 /*unused*/, + children, + ); // TODO: any + if (["columnList", "column"].includes(b.type)) { + ret.push(self as Table); + } else if (Array.isArray(self)) { ret.push(...self, ...children); } else { ret.push(self, ...children); @@ -281,13 +292,6 @@ export class DOCXExporter< ], }); - // fix https://github.com/dolanmiu/docx/pull/2800/files - doc.Document.Relationships.createRelationship( - doc.Document.Relationships.RelationshipCount + 1, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable", - "fontTable.xml", - ); - return doc; } } diff --git a/packages/xl-odt-exporter/package.json b/packages/xl-odt-exporter/package.json index 5a9b18f979..5f8ff78551 100644 --- a/packages/xl-odt-exporter/package.json +++ b/packages/xl-odt-exporter/package.json @@ -58,6 +58,7 @@ }, "dependencies": { "@blocknote/core": "0.33.0", + "@blocknote/xl-multi-column": "0.33.0", "@zip.js/zip.js": "^2.7.57", "buffer": "^6.0.3", "image-meta": "^0.2.1" diff --git a/packages/xl-odt-exporter/src/odt/__snapshots__/withMultiColumn/content.xml b/packages/xl-odt-exporter/src/odt/__snapshots__/withMultiColumn/content.xml new file mode 100644 index 0000000000..fd41c45fa7 --- /dev/null +++ b/packages/xl-odt-exporter/src/odt/__snapshots__/withMultiColumn/content.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This paragraph is in a column! + + + + + So is this heading! + + + + + You can have multiple blocks in a column too + + + + + Block 1 + + + + + + + Block 2 + + + + + + + Block 3 + + + + + + + + + \ No newline at end of file diff --git a/packages/xl-odt-exporter/src/odt/__snapshots__/withMultiColumn/styles.xml b/packages/xl-odt-exporter/src/odt/__snapshots__/withMultiColumn/styles.xml new file mode 100644 index 0000000000..9a53dac929 --- /dev/null +++ b/packages/xl-odt-exporter/src/odt/__snapshots__/withMultiColumn/styles.xml @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx b/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx index fab2516523..0c2e512793 100644 --- a/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx +++ b/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx @@ -1,4 +1,5 @@ import { + BlockFromConfig, BlockMapping, DefaultBlockSchema, DefaultProps, @@ -8,6 +9,7 @@ import { TableCell, } from "@blocknote/core"; import { ODTExporter } from "../odtExporter.js"; +import { multiColumnSchema } from "@blocknote/xl-multi-column"; export const getTabs = (nestingLevel: number) => { return Array.from({ length: nestingLevel }, () => ); @@ -168,7 +170,9 @@ const wrapWithLists = ( }; export const odtBlockMappingForDefaultSchema: BlockMapping< - DefaultBlockSchema & typeof pageBreakSchema.blockSchema, + DefaultBlockSchema & + typeof pageBreakSchema.blockSchema & + typeof multiColumnSchema.blockSchema, any, any, React.ReactNode, @@ -309,6 +313,69 @@ export const odtBlockMappingForDefaultSchema: BlockMapping< return ; }, + column: (_block, exporter, _nestingLevel, _numberedListIndex, children) => { + const ex = exporter as ODTExporter; + const style = ex.registerStyle((name) => ( + + + + )); + + return ( + {children} + ); + }, + columnList: ( + block, + exporter, + _nestingLevel, + _numberedListIndex, + children, + ) => { + const blockWithChildren = block as BlockFromConfig< + { + type: "columnList"; + content: "none"; + propSchema: Record; + }, + any, + any + >; + const ex = exporter as ODTExporter; + const style = ex.registerStyle((name) => ( + + + + )); + + return ( + + {(blockWithChildren.children || []).map((column, index) => { + const style = ex.registerStyle((name) => ( + + + + )); + + return ; + })} + {children} + + ); + }, + image: async (block, exporter) => { const odtExporter = exporter as ODTExporter; diff --git a/packages/xl-odt-exporter/src/odt/odtExporter.test.ts b/packages/xl-odt-exporter/src/odt/odtExporter.test.ts index d048a01a6b..c5e97e7718 100644 --- a/packages/xl-odt-exporter/src/odt/odtExporter.test.ts +++ b/packages/xl-odt-exporter/src/odt/odtExporter.test.ts @@ -5,6 +5,8 @@ import { beforeAll, describe, expect, it } from "vitest"; import xmlFormat from "xml-formatter"; import { odtDefaultSchemaMappings } from "./defaultSchema/index.js"; import { ODTExporter } from "./odtExporter.js"; +import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; +import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; beforeAll(async () => { // @ts-ignore @@ -51,6 +53,84 @@ describe("exporter", () => { }); }, ); + + it( + "should export a document with a multi-column block", + { timeout: 10000 }, + async () => { + const schema = BlockNoteSchema.create({ + blockSpecs: { + ...defaultBlockSpecs, + pageBreak: PageBreak, + column: ColumnBlock, + columnList: ColumnListBlock, + }, + }); + const exporter = new ODTExporter(schema, odtDefaultSchemaMappings); + const odt = await exporter.toODTDocument( + partialBlocksToBlocksForTesting(schema, [ + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", + }, + ], + }, + { + type: "column", + props: { + width: 1.4, + }, + children: [ + { + type: "heading", + content: "So is this heading!", + }, + ], + }, + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, + ]), + ); + + await testODTDocumentAgainstSnapshot(odt, { + styles: "__snapshots__/withMultiColumn/styles.xml", + content: "__snapshots__/withMultiColumn/content.xml", + }); + }, + ); }); async function testODTDocumentAgainstSnapshot( diff --git a/packages/xl-odt-exporter/src/odt/odtExporter.tsx b/packages/xl-odt-exporter/src/odt/odtExporter.tsx index eb3cf08369..ba52c22f25 100644 --- a/packages/xl-odt-exporter/src/odt/odtExporter.tsx +++ b/packages/xl-odt-exporter/src/odt/odtExporter.tsx @@ -126,20 +126,32 @@ export class ODTExporter< numberedListIndex = 0; } - const children = await this.transformBlocks( - block.children, - nestingLevel + 1, - ); - - const content = await this.mapBlock( - block as any, - nestingLevel, - numberedListIndex, - ); - - ret.push(content); - if (children.length > 0) { - ret.push(...children); + if (["columnList", "column"].includes(block.type)) { + const children = await this.transformBlocks(block.children, 0); + const content = await this.mapBlock( + block as any, + 0, + numberedListIndex, + children, + ); + + ret.push(content); + } else { + const children = await this.transformBlocks( + block.children, + nestingLevel + 1, + ); + const content = await this.mapBlock( + block as any, + nestingLevel, + numberedListIndex, + children, + ); + + ret.push(content); + if (children.length > 0) { + ret.push(...children); + } } } diff --git a/packages/xl-pdf-exporter/package.json b/packages/xl-pdf-exporter/package.json index a1f43732a0..1412b0e600 100644 --- a/packages/xl-pdf-exporter/package.json +++ b/packages/xl-pdf-exporter/package.json @@ -58,6 +58,7 @@ "dependencies": { "@blocknote/core": "0.33.0", "@blocknote/react": "0.33.0", + "@blocknote/xl-multi-column": "0.33.0", "@react-pdf/renderer": "^4.3.0", "buffer": "^6.0.3", "docx": "^9.0.2" diff --git a/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx b/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx index 14c309af12..a9b3f7ef67 100644 --- a/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx +++ b/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx @@ -137,7 +137,8 @@ @@ -159,7 +160,8 @@ diff --git a/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx b/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx index cad7caa99b..89d1d756ea 100644 --- a/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx +++ b/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx @@ -145,7 +145,8 @@ @@ -167,7 +168,8 @@ diff --git a/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithMultiColumn.jsx b/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithMultiColumn.jsx new file mode 100644 index 0000000000..9a535d0d5f --- /dev/null +++ b/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithMultiColumn.jsx @@ -0,0 +1,155 @@ + + + + + + + + + This paragraph is in a column! + + + + + + + + + + + So is this heading! + + + + + + + + + + + You can have multiple blocks in a column too + + + + + + + + + + Block 1 + + + + + + + + + + + Block 2 + + + + + + + + + + + Block 3 + + + + + + + + + \ No newline at end of file diff --git a/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx b/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx index 4ea71c0d53..693f77783b 100644 --- a/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx +++ b/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx @@ -5,6 +5,7 @@ import { pageBreakSchema, StyledText, } from "@blocknote/core"; +import { multiColumnSchema } from "@blocknote/xl-multi-column"; import { Image, Link, Path, Svg, Text, View } from "@react-pdf/renderer"; import { BULLET_MARKER, @@ -19,7 +20,9 @@ const PIXELS_PER_POINT = 0.75; const FONT_SIZE = 16; export const pdfBlockMappingForDefaultSchema: BlockMapping< - DefaultBlockSchema & typeof pageBreakSchema.blockSchema, + DefaultBlockSchema & + typeof pageBreakSchema.blockSchema & + typeof multiColumnSchema.blockSchema, any, any, React.ReactElement, @@ -88,6 +91,7 @@ export const pdfBlockMappingForDefaultSchema: BlockMapping< key={"heading" + block.id} style={{ fontSize: levelFontSizeEM * FONT_SIZE * PIXELS_PER_POINT, + lineHeight: 1.25, fontWeight: 700, }} > @@ -146,6 +150,28 @@ export const pdfBlockMappingForDefaultSchema: BlockMapping< pageBreak: () => { return ; }, + column: (block, _exporter, _nestingLevel, _numberedListIndex, children) => { + return {children}; + }, + columnList: ( + _block, + _exporter, + _nestingLevel, + _numberedListIndex, + children, + ) => { + return ( + + {children} + + ); + }, audio: (block, exporter) => { return ( diff --git a/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx b/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx index 51095f90ac..584961f750 100644 --- a/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx +++ b/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx @@ -8,12 +8,14 @@ import { defaultStyleSpecs, PageBreak, } from "@blocknote/core"; +import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; import { Text } from "@react-pdf/renderer"; import { testDocument } from "@shared/testDocument.js"; import reactElementToJSXString from "react-element-to-jsx-string"; import { describe, expect, it } from "vitest"; import { pdfDefaultSchemaMappings } from "./defaultSchema/index.js"; import { PDFExporter } from "./pdfExporter.js"; +import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; // import * as ReactPDF from "@react-pdf/renderer"; // expect.extend({ toMatchImageSnapshot }); // import { toMatchImageSnapshot } from "jest-image-snapshot"; @@ -28,6 +30,8 @@ describe("exporter", () => { blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak, + column: ColumnBlock, + columnList: ColumnListBlock, extraBlock: createBlockSpec( { content: "none", @@ -158,7 +162,12 @@ describe("exporter", () => { it("should export a document", async () => { const exporter = new PDFExporter( BlockNoteSchema.create({ - blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak }, + blockSpecs: { + ...defaultBlockSpecs, + pageBreak: PageBreak, + column: ColumnBlock, + columnList: ColumnListBlock, + }, }), pdfDefaultSchemaMappings, ); @@ -191,7 +200,12 @@ describe("exporter", () => { it("should export a document with header and footer", async () => { const exporter = new PDFExporter( BlockNoteSchema.create({ - blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak }, + blockSpecs: { + ...defaultBlockSpecs, + pageBreak: PageBreak, + column: ColumnBlock, + columnList: ColumnListBlock, + }, }), pdfDefaultSchemaMappings, ); @@ -210,4 +224,77 @@ describe("exporter", () => { // `${__dirname}/exampleWithHeaderAndFooter.pdf` // ); }); + it("should export a document with a multi-column block", async () => { + const schema = BlockNoteSchema.create({ + blockSpecs: { + ...defaultBlockSpecs, + pageBreak: PageBreak, + column: ColumnBlock, + columnList: ColumnListBlock, + }, + }); + const exporter = new PDFExporter(schema, pdfDefaultSchemaMappings); + const transformed = await exporter.toReactPDFDocument( + partialBlocksToBlocksForTesting(schema, [ + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", + }, + ], + }, + { + type: "column", + props: { + width: 1.4, + }, + children: [ + { + type: "heading", + content: "So is this heading!", + }, + ], + }, + { + type: "column", + props: { + width: 0.8, + }, + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, + ]), + ); + const str = reactElementToJSXString(transformed); + + await expect(str).toMatchFileSnapshot( + "__snapshots__/exampleWithMultiColumn.jsx", + ); + }); }); diff --git a/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx b/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx index c7cbe843f3..fb399f5ccf 100644 --- a/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx +++ b/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx @@ -146,9 +146,10 @@ export class PDFExporter< b as any, nestingLevel, numberedListIndex, + children, ); // TODO: any - if (b.type === "pageBreak") { + if (["pageBreak", "columnList", "column"].includes(b.type)) { ret.push(self); continue; } diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index e136417a60..27f52c431a 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -964,6 +964,7 @@ ], "dependencies": { "@blocknote/xl-pdf-exporter": "latest", + "@blocknote/xl-multi-column": "latest", "@react-pdf/renderer": "^4.3.0" } as any, "pro": true @@ -987,6 +988,7 @@ ], "dependencies": { "@blocknote/xl-docx-exporter": "latest", + "@blocknote/xl-multi-column": "latest", "docx": "^9.0.2" } as any, "pro": true @@ -1009,7 +1011,8 @@ "" ], "dependencies": { - "@blocknote/xl-odt-exporter": "latest" + "@blocknote/xl-odt-exporter": "latest", + "@blocknote/xl-multi-column": "latest" } as any, "pro": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9557da304..24ab649d97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1995,6 +1995,9 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn + '@blocknote/xl-multi-column': + specifier: latest + version: link:../../../packages/xl-multi-column '@blocknote/xl-pdf-exporter': specifier: latest version: link:../../../packages/xl-pdf-exporter @@ -2041,6 +2044,9 @@ importers: '@blocknote/xl-docx-exporter': specifier: latest version: link:../../../packages/xl-docx-exporter + '@blocknote/xl-multi-column': + specifier: latest + version: link:../../../packages/xl-multi-column docx: specifier: ^9.0.2 version: 9.3.0 @@ -2081,6 +2087,9 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn + '@blocknote/xl-multi-column': + specifier: latest + version: link:../../../packages/xl-multi-column '@blocknote/xl-odt-exporter': specifier: latest version: link:../../../packages/xl-odt-exporter @@ -3992,6 +4001,9 @@ importers: '@blocknote/core': specifier: 0.33.0 version: link:../core + '@blocknote/xl-multi-column': + specifier: 0.33.0 + version: link:../xl-multi-column buffer: specifier: ^6.0.3 version: 6.0.3 @@ -4169,6 +4181,9 @@ importers: '@blocknote/core': specifier: 0.33.0 version: link:../core + '@blocknote/xl-multi-column': + specifier: 0.33.0 + version: link:../xl-multi-column '@zip.js/zip.js': specifier: ^2.7.57 version: 2.7.57 @@ -4221,6 +4236,9 @@ importers: '@blocknote/react': specifier: 0.33.0 version: link:../react + '@blocknote/xl-multi-column': + specifier: 0.33.0 + version: link:../xl-multi-column '@react-pdf/renderer': specifier: ^4.3.0 version: 4.3.0(react@18.3.1)