@blocknote/xl-odt-exporter 0.25.0

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.
Files changed (44) hide show
  1. package/LICENSE +661 -0
  2. package/dist/GeistMono-Regular-D4rKXxwr.js +5 -0
  3. package/dist/GeistMono-Regular-D4rKXxwr.js.map +1 -0
  4. package/dist/Inter_18pt-Regular-byxnNS-8.js +5 -0
  5. package/dist/Inter_18pt-Regular-byxnNS-8.js.map +1 -0
  6. package/dist/blocknote-xl-odt-exporter.js +1646 -0
  7. package/dist/blocknote-xl-odt-exporter.js.map +1 -0
  8. package/dist/blocknote-xl-odt-exporter.umd.cjs +1080 -0
  9. package/dist/blocknote-xl-odt-exporter.umd.cjs.map +1 -0
  10. package/dist/webpack-stats.json +1 -0
  11. package/package.json +82 -0
  12. package/src/index.ts +1 -0
  13. package/src/odt/__snapshots__/basic/content.xml +448 -0
  14. package/src/odt/__snapshots__/basic/styles.xml +599 -0
  15. package/src/odt/__snapshots__/withCustomOptions/content.xml +462 -0
  16. package/src/odt/__snapshots__/withCustomOptions/styles.xml +599 -0
  17. package/src/odt/defaultSchema/blocks.tsx +460 -0
  18. package/src/odt/defaultSchema/index.ts +9 -0
  19. package/src/odt/defaultSchema/inlineContent.tsx +30 -0
  20. package/src/odt/defaultSchema/styles.ts +59 -0
  21. package/src/odt/index.ts +2 -0
  22. package/src/odt/odtExporter.test.ts +80 -0
  23. package/src/odt/odtExporter.tsx +360 -0
  24. package/src/odt/template/META-INF/manifest.xml +18 -0
  25. package/src/odt/template/Pictures/100000000000014C0000014CDD284996.jpg +0 -0
  26. package/src/odt/template/README.md +3 -0
  27. package/src/odt/template/Thumbnails/thumbnail.png +0 -0
  28. package/src/odt/template/content.xml +430 -0
  29. package/src/odt/template/manifest.rdf +6 -0
  30. package/src/odt/template/meta.xml +19 -0
  31. package/src/odt/template/mimetype +1 -0
  32. package/src/odt/template/settings.xml +173 -0
  33. package/src/odt/template/styles.xml +1078 -0
  34. package/src/odt/template/template blocknote.odt +0 -0
  35. package/src/odt/util/jsx.d.ts +55 -0
  36. package/src/vite-env.d.ts +11 -0
  37. package/types/src/index.d.ts +1 -0
  38. package/types/src/odt/defaultSchema/blocks.d.ts +4 -0
  39. package/types/src/odt/defaultSchema/index.d.ts +529 -0
  40. package/types/src/odt/defaultSchema/inlineContent.d.ts +3 -0
  41. package/types/src/odt/defaultSchema/styles.d.ts +2 -0
  42. package/types/src/odt/index.d.ts +2 -0
  43. package/types/src/odt/odtExporter.d.ts +28 -0
  44. package/types/src/odt/odtExporter.test.d.ts +1 -0
@@ -0,0 +1,360 @@
1
+ import {
2
+ Block,
3
+ BlockNoteSchema,
4
+ BlockSchema,
5
+ COLORS_DEFAULT,
6
+ Exporter,
7
+ ExporterOptions,
8
+ InlineContentSchema,
9
+ StyleSchema,
10
+ StyledText,
11
+ } from "@blocknote/core";
12
+ import { loadFileBuffer } from "@shared/util/fileUtil.js";
13
+ import { getImageDimensions } from "@shared/util/imageUtil.js";
14
+ import { BlobReader, BlobWriter, TextReader, ZipWriter } from "@zip.js/zip.js";
15
+ import { renderToString } from "react-dom/server";
16
+ import stylesXml from "./template/styles.xml?raw";
17
+
18
+ export class ODTExporter<
19
+ B extends BlockSchema,
20
+ S extends StyleSchema,
21
+ I extends InlineContentSchema
22
+ > extends Exporter<
23
+ B,
24
+ I,
25
+ S,
26
+ React.ReactNode,
27
+ React.ReactNode,
28
+ Record<string, string>,
29
+ React.ReactNode
30
+ > {
31
+ // "Styles" to be added to the AutomaticStyles section of the ODT file
32
+ // Keyed by the style name
33
+ private automaticStyles: Map<string, React.ReactNode> = new Map();
34
+
35
+ // "Pictures" to be added to the Pictures folder in the ODT file
36
+ // Keyed by the original image URL
37
+ private pictures = new Map<
38
+ string,
39
+ {
40
+ file: Blob;
41
+ fileName: string;
42
+ height: number;
43
+ width: number;
44
+ }
45
+ >();
46
+
47
+ private styleCounter = 0;
48
+
49
+ public readonly options: ExporterOptions;
50
+
51
+ constructor(
52
+ protected readonly schema: BlockNoteSchema<B, I, S>,
53
+ mappings: Exporter<
54
+ NoInfer<B>,
55
+ NoInfer<I>,
56
+ NoInfer<S>,
57
+ React.ReactNode,
58
+ React.ReactNode,
59
+ Record<string, string>,
60
+ React.ReactNode
61
+ >["mappings"],
62
+ options?: Partial<ExporterOptions>
63
+ ) {
64
+ const defaults = {
65
+ colors: COLORS_DEFAULT,
66
+ } satisfies Partial<ExporterOptions>;
67
+
68
+ super(schema, mappings, { ...defaults, ...options });
69
+ this.options = { ...defaults, ...options };
70
+ }
71
+
72
+ protected async loadFonts() {
73
+ const interFont = await loadFileBuffer(
74
+ await import("@shared/assets/fonts/inter/Inter_18pt-Regular.ttf")
75
+ );
76
+ const geistMonoFont = await loadFileBuffer(
77
+ await import("@shared/assets/fonts/GeistMono-Regular.ttf")
78
+ );
79
+
80
+ return [
81
+ {
82
+ name: "Inter 18pt",
83
+ fileName: "Inter_18pt-Regular.ttf",
84
+ data: new Blob([interFont], { type: "font/ttf" }),
85
+ },
86
+ {
87
+ name: "Geist Mono",
88
+ fileName: "GeistMono-Regular.ttf",
89
+ data: new Blob([geistMonoFont], { type: "font/ttf" }),
90
+ },
91
+ ];
92
+ }
93
+
94
+ public transformStyledText(styledText: StyledText<S>): React.ReactNode {
95
+ const stylesArray = this.mapStyles(styledText.styles);
96
+ const styles = Object.assign({}, ...stylesArray);
97
+
98
+ if (Object.keys(styles).length === 0) {
99
+ return styledText.text;
100
+ }
101
+
102
+ const styleName = `BN_T${++this.styleCounter}`;
103
+
104
+ // Store the complete style element
105
+ this.automaticStyles.set(
106
+ styleName,
107
+ <style:style style:name={styleName} style:family="text">
108
+ <style:text-properties {...styles} />
109
+ </style:style>
110
+ );
111
+
112
+ return <text:span text:style-name={styleName}>{styledText.text}</text:span>;
113
+ }
114
+
115
+ public async transformBlocks(
116
+ blocks: Block<B, I, S>[],
117
+ nestingLevel = 0
118
+ ): Promise<React.ReactNode[]> {
119
+ const ret: React.ReactNode[] = [];
120
+ let numberedListIndex = 0;
121
+
122
+ for (const block of blocks) {
123
+ if (block.type === "numberedListItem") {
124
+ numberedListIndex++;
125
+ } else {
126
+ numberedListIndex = 0;
127
+ }
128
+
129
+ const children = await this.transformBlocks(
130
+ block.children,
131
+ nestingLevel + 1
132
+ );
133
+
134
+ const content = await this.mapBlock(
135
+ block as any,
136
+ nestingLevel,
137
+ numberedListIndex
138
+ );
139
+
140
+ ret.push(content);
141
+ if (children.length > 0) {
142
+ ret.push(...children);
143
+ }
144
+ }
145
+
146
+ return ret;
147
+ }
148
+
149
+ public async toODTDocument(
150
+ blocks: Block<B, I, S>[],
151
+ options?: {
152
+ header?: string | XMLDocument;
153
+ footer?: string | XMLDocument;
154
+ }
155
+ ): Promise<Blob> {
156
+ const xmlOptionToString = (xmlDocument: string | XMLDocument) => {
157
+ const xmlNamespacesRegEx =
158
+ /<([a-zA-Z0-9:]+)\s+?(?:xml)ns(?::[a-zA-Z0-9]+)?=".*"(.*)>/g;
159
+ let stringifiedDoc = "";
160
+
161
+ if (typeof xmlDocument === "string") {
162
+ stringifiedDoc = xmlDocument;
163
+ } else {
164
+ const serializer = new XMLSerializer();
165
+
166
+ stringifiedDoc = serializer.serializeToString(xmlDocument);
167
+ }
168
+
169
+ // Detect and remove XML namespaces (already defined in the root element)
170
+ return stringifiedDoc.replace(xmlNamespacesRegEx, "<$1$2>");
171
+ };
172
+ const blockContent = await this.transformBlocks(blocks);
173
+ const styles = Array.from(this.automaticStyles.values());
174
+ const pictures = Array.from(this.pictures.values());
175
+ const fonts = await this.loadFonts();
176
+ const header = xmlOptionToString(options?.header || "");
177
+ const footer = xmlOptionToString(options?.footer || "");
178
+
179
+ const content = (
180
+ <office:document-content
181
+ xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
182
+ xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
183
+ xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
184
+ xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
185
+ xmlns:xlink="http://www.w3.org/1999/xlink"
186
+ xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
187
+ xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
188
+ xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
189
+ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"
190
+ office:version="1.3">
191
+ <office:font-face-decls>
192
+ {fonts.map((font) => {
193
+ return (
194
+ <style:font-face
195
+ style:name={font.name}
196
+ svg:font-family={font.name}
197
+ style:font-pitch="variable">
198
+ <svg:font-face-src>
199
+ <svg:font-face-uri
200
+ xlink:href={`Fonts/${font.fileName}`}
201
+ xlink:type="simple"
202
+ loext:font-style="normal"
203
+ loext:font-weight="normal">
204
+ <svg:font-face-format svg:string="truetype" />
205
+ </svg:font-face-uri>
206
+ </svg:font-face-src>
207
+ </style:font-face>
208
+ );
209
+ })}
210
+ </office:font-face-decls>
211
+ <office:automatic-styles>{styles}</office:automatic-styles>
212
+ {(header || footer) && (
213
+ <office:master-styles>
214
+ <style:master-page
215
+ style:name="Standard"
216
+ style:page-layout-name="Mpm1"
217
+ draw:style-name="Mdp1">
218
+ {header && (
219
+ <style:header
220
+ dangerouslySetInnerHTML={{
221
+ __html: header,
222
+ }}></style:header>
223
+ )}
224
+ {footer && (
225
+ <style:footer
226
+ dangerouslySetInnerHTML={{
227
+ __html: footer,
228
+ }}></style:footer>
229
+ )}
230
+ </style:master-page>
231
+ </office:master-styles>
232
+ )}
233
+ <office:body>
234
+ <office:text>{blockContent}</office:text>
235
+ </office:body>
236
+ </office:document-content>
237
+ );
238
+
239
+ const manifestNode = (
240
+ <manifest:manifest
241
+ xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
242
+ manifest:version="1.3">
243
+ <manifest:file-entry
244
+ manifest:media-type="application/vnd.oasis.opendocument.text"
245
+ manifest:full-path="/"
246
+ />
247
+ <manifest:file-entry
248
+ manifest:media-type="text/xml"
249
+ manifest:full-path="content.xml"
250
+ />
251
+ <manifest:file-entry
252
+ manifest:media-type="text/xml"
253
+ manifest:full-path="styles.xml"
254
+ />
255
+ {pictures.map((picture) => {
256
+ return (
257
+ <manifest:file-entry
258
+ manifest:media-type={picture.file.type}
259
+ manifest:full-path={`Pictures/${picture.fileName}`}
260
+ />
261
+ );
262
+ })}
263
+ {fonts.map((font) => {
264
+ return (
265
+ <manifest:file-entry
266
+ manifest:media-type="application/x-font-ttf"
267
+ manifest:full-path={`Fonts/${font.fileName}`}
268
+ />
269
+ );
270
+ })}
271
+ </manifest:manifest>
272
+ );
273
+ const zipWriter = new ZipWriter(
274
+ new BlobWriter("application/vnd.oasis.opendocument.text")
275
+ );
276
+
277
+ // Add mimetype first, uncompressed
278
+ zipWriter.add(
279
+ "mimetype",
280
+ new TextReader("application/vnd.oasis.opendocument.text"),
281
+ {
282
+ compressionMethod: 0,
283
+ level: 0,
284
+ dataDescriptor: false,
285
+ extendedTimestamp: false,
286
+ }
287
+ );
288
+
289
+ const contentXml = renderToString(content);
290
+ const manifestXml = renderToString(manifestNode);
291
+
292
+ zipWriter.add("content.xml", new TextReader(contentXml));
293
+ zipWriter.add("styles.xml", new TextReader(stylesXml));
294
+ zipWriter.add("META-INF/manifest.xml", new TextReader(manifestXml));
295
+ fonts.forEach((font) => {
296
+ zipWriter.add(`Fonts/${font.fileName}`, new BlobReader(font.data));
297
+ });
298
+ pictures.forEach((picture) => {
299
+ zipWriter.add(
300
+ `Pictures/${picture.fileName}`,
301
+ new BlobReader(picture.file)
302
+ );
303
+ });
304
+
305
+ return zipWriter.close();
306
+ }
307
+
308
+ public registerStyle(style: (name: string) => React.ReactNode): string {
309
+ const styleName = `BN_S${++this.styleCounter}`;
310
+ this.automaticStyles.set(styleName, style(styleName));
311
+ return styleName;
312
+ }
313
+
314
+ public async registerPicture(url: string): Promise<{
315
+ path: string;
316
+ mimeType: string;
317
+ height: number;
318
+ width: number;
319
+ }> {
320
+ const mimeTypeFileExtensionMap = {
321
+ "image/apng": "apng",
322
+ "image/avif": "avif",
323
+ "image/bmp": "bmp",
324
+ "image/gif": "gif",
325
+ "image/vnd.microsoft.icon": "ico",
326
+ "image/jpeg": "jpg",
327
+ "image/png": "png",
328
+ "image/svg+xml": "svg",
329
+ "image/tiff": "tiff",
330
+ "image/webp": "webp",
331
+ };
332
+ if (this.pictures.has(url)) {
333
+ const picture = this.pictures.get(url)!;
334
+
335
+ return {
336
+ path: `Pictures/${picture.fileName}`,
337
+ mimeType: picture.file.type,
338
+ height: picture.height,
339
+ width: picture.width,
340
+ };
341
+ }
342
+
343
+ const blob = await this.resolveFile(url);
344
+ const fileExtension =
345
+ mimeTypeFileExtensionMap[
346
+ blob.type as keyof typeof mimeTypeFileExtensionMap
347
+ ] || "png";
348
+ const fileName = `picture-${this.pictures.size}.${fileExtension}`;
349
+ const { width, height } = await getImageDimensions(blob);
350
+
351
+ this.pictures.set(url, {
352
+ file: blob,
353
+ fileName: fileName,
354
+ height,
355
+ width,
356
+ });
357
+
358
+ return { path: `Pictures/${fileName}`, mimeType: blob.type, height, width };
359
+ }
360
+ }
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
3
+ manifest:version="1.3"
4
+ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0">
5
+ <manifest:file-entry manifest:full-path="/" manifest:version="1.3"
6
+ manifest:media-type="application/vnd.oasis.opendocument.text" />
7
+ <manifest:file-entry manifest:full-path="Configurations2/"
8
+ manifest:media-type="application/vnd.sun.xml.ui.configuration" />
9
+ <manifest:file-entry manifest:full-path="manifest.rdf" manifest:media-type="application/rdf+xml" />
10
+ <manifest:file-entry manifest:full-path="settings.xml" manifest:media-type="text/xml" />
11
+ <manifest:file-entry manifest:full-path="Pictures/100000000000014C0000014CDD284996.jpg"
12
+ manifest:media-type="image/jpeg" />
13
+ <manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml" />
14
+ <manifest:file-entry manifest:full-path="Thumbnails/thumbnail.png"
15
+ manifest:media-type="image/png" />
16
+ <manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml" />
17
+ <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml" />
18
+ </manifest:manifest>
@@ -0,0 +1,3 @@
1
+ - `template blocknote.odt` is the demo docx export (https://www.blocknotejs.org/examples/interoperability/converting-blocks-to-docx), opened by libreoffice on mac and saved as odt
2
+ - the extracted files have been formatted (using vs code xml formatter) so we can see diffs across commits
3
+ - the `styles.xml` file is the only file actually used by the odt exporter