@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.
- package/LICENSE +661 -0
- package/dist/GeistMono-Regular-D4rKXxwr.js +5 -0
- package/dist/GeistMono-Regular-D4rKXxwr.js.map +1 -0
- package/dist/Inter_18pt-Regular-byxnNS-8.js +5 -0
- package/dist/Inter_18pt-Regular-byxnNS-8.js.map +1 -0
- package/dist/blocknote-xl-odt-exporter.js +1646 -0
- package/dist/blocknote-xl-odt-exporter.js.map +1 -0
- package/dist/blocknote-xl-odt-exporter.umd.cjs +1080 -0
- package/dist/blocknote-xl-odt-exporter.umd.cjs.map +1 -0
- package/dist/webpack-stats.json +1 -0
- package/package.json +82 -0
- package/src/index.ts +1 -0
- package/src/odt/__snapshots__/basic/content.xml +448 -0
- package/src/odt/__snapshots__/basic/styles.xml +599 -0
- package/src/odt/__snapshots__/withCustomOptions/content.xml +462 -0
- package/src/odt/__snapshots__/withCustomOptions/styles.xml +599 -0
- package/src/odt/defaultSchema/blocks.tsx +460 -0
- package/src/odt/defaultSchema/index.ts +9 -0
- package/src/odt/defaultSchema/inlineContent.tsx +30 -0
- package/src/odt/defaultSchema/styles.ts +59 -0
- package/src/odt/index.ts +2 -0
- package/src/odt/odtExporter.test.ts +80 -0
- package/src/odt/odtExporter.tsx +360 -0
- package/src/odt/template/META-INF/manifest.xml +18 -0
- package/src/odt/template/Pictures/100000000000014C0000014CDD284996.jpg +0 -0
- package/src/odt/template/README.md +3 -0
- package/src/odt/template/Thumbnails/thumbnail.png +0 -0
- package/src/odt/template/content.xml +430 -0
- package/src/odt/template/manifest.rdf +6 -0
- package/src/odt/template/meta.xml +19 -0
- package/src/odt/template/mimetype +1 -0
- package/src/odt/template/settings.xml +173 -0
- package/src/odt/template/styles.xml +1078 -0
- package/src/odt/template/template blocknote.odt +0 -0
- package/src/odt/util/jsx.d.ts +55 -0
- package/src/vite-env.d.ts +11 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/odt/defaultSchema/blocks.d.ts +4 -0
- package/types/src/odt/defaultSchema/index.d.ts +529 -0
- package/types/src/odt/defaultSchema/inlineContent.d.ts +3 -0
- package/types/src/odt/defaultSchema/styles.d.ts +2 -0
- package/types/src/odt/index.d.ts +2 -0
- package/types/src/odt/odtExporter.d.ts +28 -0
- 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>
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|