UNPKG

@blocknote/xl-docx-exporter

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

320 lines (313 loc) 8.5 kB
import { BlockMapping, COLORS_DEFAULT, DefaultBlockSchema, DefaultProps, pageBreakSchema, StyledText, UnreachableCaseError, } from "@blocknote/core"; import { getImageDimensions } from "@shared/util/imageUtil.js"; import { CheckBox, Table as DocxTable, ExternalHyperlink, ImageRun, IParagraphOptions, PageBreak, 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<DefaultProps>, colors: typeof COLORS_DEFAULT, ): IParagraphOptions { return { shading: props.backgroundColor === "default" || !props.backgroundColor ? undefined : { type: ShadingType.SOLID, color: colors[ props.backgroundColor as keyof typeof colors ].background.slice(1), }, run: props.textColor === "default" || !props.textColor ? undefined : { color: colors[props.textColor as keyof typeof colors].text.slice(1), }, alignment: !props.textAlignment || props.textAlignment === "left" ? undefined : props.textAlignment === "center" ? "center" : props.textAlignment === "right" ? "right" : props.textAlignment === "justify" ? "distribute" : (() => { throw new UnreachableCaseError(props.textAlignment); })(), }; } export const docxBlockMappingForDefaultSchema: BlockMapping< DefaultBlockSchema & typeof pageBreakSchema.blockSchema & typeof multiColumnSchema.blockSchema, any, any, | Promise<Paragraph[] | Paragraph | DocxTable> | Paragraph[] | Paragraph | DocxTable, ParagraphChild > = { paragraph: (block, exporter) => { return new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: exporter.transformInlineContent(block.content), style: "Normal", run: { font: "Inter", }, }); }, toggleListItem: (block, exporter) => { return new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: [ new TextRun({ children: ["> "], }), ...exporter.transformInlineContent(block.content), ], }); }, numberedListItem: (block, exporter, nestingLevel) => { return new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: exporter.transformInlineContent(block.content), numbering: { reference: "blocknote-numbered-list", level: nestingLevel, }, }); }, bulletListItem: (block, exporter, nestingLevel) => { return new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: exporter.transformInlineContent(block.content), numbering: { reference: "blocknote-bullet-list", level: nestingLevel, }, }); }, checkListItem: (block, exporter) => { return new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: [ new CheckBox({ checked: block.props.checked }), new TextRun({ children: [" "], }), ...exporter.transformInlineContent(block.content), ], }); }, heading: (block, exporter) => { return new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: exporter.transformInlineContent(block.content), heading: `Heading${block.props.level}`, }); }, quote: (block, exporter) => { return new Paragraph({ shading: { color: "#7D797A", }, border: { left: { color: "#7D797A", space: 100, style: "single", size: 8, }, }, ...blockPropsToStyles(block.props, exporter.options.colors), children: exporter.transformInlineContent(block.content), }); }, audio: (block, exporter) => { return [ file(block.props, "Open audio", exporter), ...caption(block.props, exporter), ]; }, video: (block, exporter) => { return [ file(block.props, "Open video", exporter), ...caption(block.props, exporter), ]; }, file: (block, exporter) => { return [ file(block.props, "Open file", exporter), ...caption(block.props, exporter), ]; }, codeBlock: (block) => { const textContent = (block.content as StyledText<any>[])[0]?.text || ""; return new Paragraph({ style: "Codeblock", shading: { type: ShadingType.SOLID, fill: "161616", color: "161616", }, children: [ ...textContent.split("\n").map((line, index) => { return new TextRun({ text: line, break: index > 0 ? 1 : 0, }); }), ], }); }, pageBreak: () => { return new Paragraph({ 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); return [ new Paragraph({ ...blockPropsToStyles(block.props, exporter.options.colors), children: [ new ImageRun({ data: await blob.arrayBuffer(), // it would be nicer to set the actual data type here, but then we'd need to use a mime type / image type // detector. atm passing gif does not seem to be causing issues as the "type" is mainly used by docxjs internally // (i.e.: to make sure it's not svg) type: "gif", altText: block.props.caption ? { description: block.props.caption, name: block.props.caption, title: block.props.caption, } : undefined, transformation: { width: block.props.previewWidth || width, height: ((block.props.previewWidth || width) / width) * height, }, }), ], }), ...caption(block.props, exporter), ]; }, table: (block, exporter) => { return Table(block.content, exporter); }, }; function file( props: Partial<DefaultProps & { name: string; url: string }>, defaultText: string, exporter: any, ) { return new Paragraph({ ...blockPropsToStyles(props, exporter.options.colors), children: [ new ExternalHyperlink({ children: [ new TextRun({ text: props.name || defaultText, style: "Hyperlink", }), ], link: props.url!, }), ], }); } function caption( props: Partial<DefaultProps & { caption: string }>, exporter: any, ) { if (!props.caption) { return []; } return [ new Paragraph({ ...blockPropsToStyles(props, exporter.options.colors), children: [ new TextRun({ text: props.caption, }), ], style: "Caption", }), ]; }