@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,460 @@
1
+ import {
2
+ BlockMapping,
3
+ DefaultBlockSchema,
4
+ DefaultProps,
5
+ mapTableCell,
6
+ pageBreakSchema,
7
+ StyledText,
8
+ TableCell,
9
+ } from "@blocknote/core";
10
+ import { ODTExporter } from "../odtExporter.js";
11
+
12
+ export const getTabs = (nestingLevel: number) => {
13
+ return Array.from({ length: nestingLevel }, () => <text:tab />);
14
+ };
15
+
16
+ const createParagraphStyle = (
17
+ exporter: ODTExporter<any, any, any>,
18
+ props: Partial<DefaultProps>,
19
+ parentStyleName = "Standard",
20
+ styleAttributes: Record<string, string> = {}
21
+ ) => {
22
+ const paragraphStyles: Record<string, string> = {};
23
+ const textStyles: Record<string, string> = {};
24
+
25
+ if (props.textAlignment && props.textAlignment !== "left") {
26
+ const alignmentMap = {
27
+ left: "start",
28
+ center: "center",
29
+ right: "end",
30
+ justify: "justify",
31
+ };
32
+ paragraphStyles["fo:text-align"] =
33
+ alignmentMap[props.textAlignment as keyof typeof alignmentMap];
34
+ }
35
+
36
+ const backgroundColor =
37
+ props.backgroundColor && props.backgroundColor !== "default"
38
+ ? exporter.options.colors[
39
+ props.backgroundColor as keyof typeof exporter.options.colors
40
+ ].background
41
+ : undefined;
42
+
43
+ if (backgroundColor) {
44
+ paragraphStyles["fo:background-color"] = backgroundColor;
45
+ }
46
+
47
+ if (props.textColor && props.textColor !== "default") {
48
+ const color =
49
+ exporter.options.colors[
50
+ props.textColor as keyof typeof exporter.options.colors
51
+ ].text;
52
+ textStyles["fo:color"] = color;
53
+ }
54
+
55
+ if (
56
+ !backgroundColor &&
57
+ !Object.keys(styleAttributes).length &&
58
+ !Object.keys(paragraphStyles).length &&
59
+ !Object.keys(textStyles).length
60
+ ) {
61
+ return parentStyleName || "Standard";
62
+ }
63
+
64
+ return exporter.registerStyle((name) => (
65
+ <style:style
66
+ style:family="paragraph"
67
+ style:name={name}
68
+ style:parent-style-name={parentStyleName}
69
+ {...styleAttributes}>
70
+ {backgroundColor && (
71
+ <loext:graphic-properties
72
+ draw:fill="solid"
73
+ draw:fill-color={backgroundColor}
74
+ />
75
+ )}
76
+ {Object.keys(paragraphStyles).length > 0 && (
77
+ <style:paragraph-properties {...paragraphStyles} />
78
+ )}
79
+ {Object.keys(textStyles).length > 0 && (
80
+ <style:text-properties {...textStyles}></style:text-properties>
81
+ )}
82
+ </style:style>
83
+ ));
84
+ };
85
+
86
+ const createTableCellStyle = (
87
+ exporter: ODTExporter<any, any, any>
88
+ ): ((cell: TableCell<any, any>) => string) => {
89
+ // To not create a new style for each cell within a table, we cache the styles based on unique cell properties
90
+ const cellStyleCache = new Map<string, string>();
91
+
92
+ return (cell: TableCell<any, any>) => {
93
+ const key = `${cell.props.backgroundColor}-${cell.props.textColor}-${cell.props.textAlignment}`;
94
+
95
+ if (cellStyleCache.has(key)) {
96
+ return cellStyleCache.get(key)!;
97
+ }
98
+
99
+ const styleName = exporter.registerStyle((name) => (
100
+ <style:style style:family="table-cell" style:name={name}>
101
+ <style:table-cell-properties
102
+ fo:border="0.5pt solid #000000"
103
+ style:writing-mode="lr-tb"
104
+ fo:padding-top="0in"
105
+ fo:padding-left="0.075in"
106
+ fo:padding-bottom="0in"
107
+ fo:padding-right="0.075in"
108
+ fo:background-color={
109
+ cell.props.backgroundColor !== "default" &&
110
+ cell.props.backgroundColor
111
+ ? exporter.options.colors[
112
+ cell.props
113
+ .backgroundColor as keyof typeof exporter.options.colors
114
+ ].background
115
+ : undefined
116
+ }
117
+ // TODO This is not applying because the children set their own colors
118
+ fo:color={
119
+ cell.props.textColor !== "default" && cell.props.textColor
120
+ ? exporter.options.colors[
121
+ cell.props.textColor as keyof typeof exporter.options.colors
122
+ ].text
123
+ : undefined
124
+ }
125
+ />
126
+ </style:style>
127
+ ));
128
+
129
+ cellStyleCache.set(key, styleName);
130
+
131
+ return styleName;
132
+ };
133
+ };
134
+ const createTableStyle = (
135
+ exporter: ODTExporter<any, any, any>,
136
+ options: { width: number }
137
+ ) => {
138
+ const tableStyleName = exporter.registerStyle((name) => (
139
+ <style:style style:family="table" style:name={name}>
140
+ <style:table-properties
141
+ table:align="left"
142
+ style:writing-mode="lr-tb"
143
+ style:width={`${options.width}pt`}
144
+ />
145
+ </style:style>
146
+ ));
147
+
148
+ return tableStyleName;
149
+ };
150
+
151
+ const wrapWithLists = (
152
+ content: React.ReactNode,
153
+ level: number
154
+ ): React.ReactNode => {
155
+ if (level <= 0) {
156
+ return content;
157
+ }
158
+ return (
159
+ <text:list>
160
+ <text:list-item>{wrapWithLists(content, level - 1)}</text:list-item>
161
+ </text:list>
162
+ );
163
+ };
164
+
165
+ export const odtBlockMappingForDefaultSchema: BlockMapping<
166
+ DefaultBlockSchema & typeof pageBreakSchema.blockSchema,
167
+ any,
168
+ any,
169
+ React.ReactNode,
170
+ React.ReactNode
171
+ > = {
172
+ paragraph: (block, exporter, nestingLevel) => {
173
+ const styleName = createParagraphStyle(
174
+ exporter as ODTExporter<any, any, any>,
175
+ block.props
176
+ );
177
+
178
+ return (
179
+ <text:p text:style-name={styleName}>
180
+ {getTabs(nestingLevel)}
181
+ {exporter.transformInlineContent(block.content)}
182
+ </text:p>
183
+ );
184
+ },
185
+
186
+ heading: (block, exporter, nestingLevel) => {
187
+ const customStyleName = createParagraphStyle(
188
+ exporter as ODTExporter<any, any, any>,
189
+ block.props,
190
+ "Heading_20_" + block.props.level
191
+ );
192
+ const styleName = customStyleName;
193
+
194
+ return (
195
+ <text:h
196
+ text:outline-level={`${block.props.level}`}
197
+ text:style-name={styleName}>
198
+ {getTabs(nestingLevel)}
199
+ {exporter.transformInlineContent(block.content)}
200
+ </text:h>
201
+ );
202
+ },
203
+
204
+ /**
205
+ * Note: we wrap each list item in it's own list element.
206
+ * This is not the cleanest solution, it would be nicer to recognize subsequent
207
+ * list items and wrap them in the same list element.
208
+ *
209
+ * However, Word DocX -> ODT export actually does the same thing, so
210
+ * for now it seems reasonable.
211
+ *
212
+ * (LibreOffice does nicely wrap the list items in the same list element)
213
+ */
214
+ bulletListItem: (block, exporter, nestingLevel) => {
215
+ const styleName = createParagraphStyle(
216
+ exporter as ODTExporter<any, any, any>,
217
+ block.props,
218
+ "Standard",
219
+ { "style:list-style-name": "WWNum1" }
220
+ );
221
+ return (
222
+ <text:list text:style-name="WWNum1">
223
+ <text:list-item>
224
+ {wrapWithLists(
225
+ <text:p text:style-name={styleName}>
226
+ {exporter.transformInlineContent(block.content)}
227
+ </text:p>,
228
+ nestingLevel
229
+ )}
230
+ </text:list-item>
231
+ </text:list>
232
+ );
233
+ },
234
+
235
+ numberedListItem: (block, exporter, nestingLevel, numberedListIndex) => {
236
+ const styleName = createParagraphStyle(
237
+ exporter as ODTExporter<any, any, any>,
238
+ block.props
239
+ );
240
+ // continue numbering from the previous list item if this is not the first item
241
+ const continueNumbering = (numberedListIndex || 0) > 1 ? "true" : "false";
242
+
243
+ return (
244
+ <text:list
245
+ text:style-name="No_20_List"
246
+ text:continue-numbering={continueNumbering}>
247
+ <text:list-item
248
+ {...(continueNumbering === "false" && {
249
+ "text:start-value": block.props.start,
250
+ })}>
251
+ {wrapWithLists(
252
+ <text:p text:style-name={styleName}>
253
+ {exporter.transformInlineContent(block.content)}
254
+ </text:p>,
255
+ nestingLevel
256
+ )}
257
+ </text:list-item>
258
+ </text:list>
259
+ );
260
+ },
261
+
262
+ checkListItem: (block, exporter) => (
263
+ <text:p text:style-name="Standard">
264
+ {block.props.checked ? "☒ " : "☐ "}
265
+ {exporter.transformInlineContent(block.content)}
266
+ </text:p>
267
+ ),
268
+
269
+ pageBreak: async () => {
270
+ return <text:p text:style-name="PageBreak" />;
271
+ },
272
+
273
+ image: async (block, exporter) => {
274
+ const odtExporter = exporter as ODTExporter<any, any, any>;
275
+
276
+ const { path, mimeType, ...originalDimensions } =
277
+ await odtExporter.registerPicture(block.props.url);
278
+ const styleName = createParagraphStyle(
279
+ exporter as ODTExporter<any, any, any>,
280
+ block.props
281
+ );
282
+ const width = block.props.previewWidth;
283
+ const height =
284
+ (originalDimensions.height / originalDimensions.width) * width;
285
+ const captionHeight = 20;
286
+ const imageFrame = (
287
+ <text:p text:style-name={block.props.caption ? "Caption" : styleName}>
288
+ <draw:frame
289
+ draw:style-name="Frame"
290
+ style:rel-height="scale"
291
+ svg:width={`${width}px`}
292
+ svg:height={`${height}px`}
293
+ style:rel-width={block.props.caption ? "100%" : `${width}px`}
294
+ {...(!block.props.caption && {
295
+ "text:anchor-type": "as-char",
296
+ })}>
297
+ <draw:image
298
+ xlink:type="simple"
299
+ xlink:show="embed"
300
+ xlink:actuate="onLoad"
301
+ xlink:href={path}
302
+ draw:mime-type={mimeType}
303
+ />
304
+ </draw:frame>
305
+ <text:line-break />
306
+ <text:span text:style-name="Caption">{block.props.caption}</text:span>
307
+ </text:p>
308
+ );
309
+
310
+ if (block.props.caption) {
311
+ return (
312
+ <text:p text:style-name={styleName}>
313
+ <draw:frame
314
+ draw:style-name="Frame"
315
+ style:rel-height="scale"
316
+ style:rel-width={`${width}px`}
317
+ svg:width={`${width}px`}
318
+ svg:height={`${height + captionHeight}px`}
319
+ text:anchor-type="as-char">
320
+ <draw:text-box>{imageFrame}</draw:text-box>
321
+ </draw:frame>
322
+ </text:p>
323
+ );
324
+ }
325
+
326
+ return imageFrame;
327
+ },
328
+
329
+ table: (block, exporter) => {
330
+ const DEFAULT_COLUMN_WIDTH_PX = 120;
331
+ const tableWidthPX =
332
+ block.content.columnWidths.reduce(
333
+ (totalWidth, colWidth) =>
334
+ (totalWidth || 0) + (colWidth || DEFAULT_COLUMN_WIDTH_PX),
335
+ 0
336
+ ) || 0;
337
+ const tableWidthPT = tableWidthPX * 0.75;
338
+ const ex = exporter as ODTExporter<any, any, any>;
339
+ const getCellStyleName = createTableCellStyle(ex);
340
+ const tableStyleName = createTableStyle(ex, { width: tableWidthPT });
341
+
342
+ return (
343
+ <table:table table:name={block.id} table:style-name={tableStyleName}>
344
+ {block.content.rows[0]?.cells.map((_, i) => {
345
+ const colWidthPX =
346
+ block.content.columnWidths[i] || DEFAULT_COLUMN_WIDTH_PX;
347
+ const colWidthPT = colWidthPX * 0.75;
348
+ const style = ex.registerStyle((name) => (
349
+ <style:style style:name={name} style:family="table-column">
350
+ <style:table-column-properties
351
+ style:column-width={`${colWidthPT}pt`}
352
+ />
353
+ </style:style>
354
+ ));
355
+ return <table:table-column table:style-name={style} key={i} />;
356
+ })}
357
+ {block.content.rows.map((row, rowIndex) => (
358
+ <table:table-row key={rowIndex}>
359
+ {row.cells.map((c, colIndex) => {
360
+ const cell = mapTableCell(c);
361
+ return (
362
+ <table:table-cell
363
+ key={`${rowIndex}-${colIndex}`}
364
+ table:style-name={getCellStyleName(cell)}
365
+ office:value-type="string"
366
+ style:text-align-source="fix"
367
+ style:paragraph-properties-text-align={
368
+ cell.props.textAlignment
369
+ }>
370
+ <text:p text:style-name="Standard">
371
+ {exporter.transformInlineContent(cell.content)}
372
+ </text:p>
373
+ </table:table-cell>
374
+ );
375
+ })}
376
+ </table:table-row>
377
+ ))}
378
+ </table:table>
379
+ );
380
+ },
381
+
382
+ codeBlock: (block) => {
383
+ const textContent = (block.content as StyledText<any>[])[0]?.text || "";
384
+
385
+ return (
386
+ <text:p text:style-name="Codeblock">
387
+ {...textContent.split("\n").map((line, index) => {
388
+ return (
389
+ <>
390
+ {index !== 0 && <text:line-break />}
391
+ {line}
392
+ </>
393
+ );
394
+ })}
395
+ </text:p>
396
+ );
397
+ },
398
+
399
+ file: async (block) => {
400
+ return (
401
+ <>
402
+ <text:p style:style-name="Standard">
403
+ {block.props.url ? (
404
+ <text:a
405
+ xlink:type="simple"
406
+ text:style-name="Internet_20_link"
407
+ office:target-frame-name="_top"
408
+ xlink:show="replace"
409
+ xlink:href={block.props.url}>
410
+ <text:span text:style-name="Internet_20_link">
411
+ Open file
412
+ </text:span>
413
+ </text:a>
414
+ ) : (
415
+ "Open file"
416
+ )}
417
+ </text:p>
418
+ {block.props.caption && (
419
+ <text:p text:style-name="Caption">{block.props.caption}</text:p>
420
+ )}
421
+ </>
422
+ );
423
+ },
424
+
425
+ video: (block) => (
426
+ <>
427
+ <text:p style:style-name="Standard">
428
+ <text:a
429
+ xlink:type="simple"
430
+ text:style-name="Internet_20_link"
431
+ office:target-frame-name="_top"
432
+ xlink:show="replace"
433
+ xlink:href={block.props.url}>
434
+ <text:span text:style-name="Internet_20_link">Open video</text:span>
435
+ </text:a>
436
+ </text:p>
437
+ {block.props.caption && (
438
+ <text:p text:style-name="Caption">{block.props.caption}</text:p>
439
+ )}
440
+ </>
441
+ ),
442
+
443
+ audio: (block) => (
444
+ <>
445
+ <text:p style:style-name="Standard">
446
+ <text:a
447
+ xlink:type="simple"
448
+ text:style-name="Internet_20_link"
449
+ office:target-frame-name="_top"
450
+ xlink:show="replace"
451
+ xlink:href={block.props.url}>
452
+ <text:span text:style-name="Internet_20_link">Open audio</text:span>
453
+ </text:a>
454
+ </text:p>
455
+ {block.props.caption && (
456
+ <text:p text:style-name="Caption">{block.props.caption}</text:p>
457
+ )}
458
+ </>
459
+ ),
460
+ };
@@ -0,0 +1,9 @@
1
+ import { odtBlockMappingForDefaultSchema } from "./blocks.js";
2
+ import { odtInlineContentMappingForDefaultSchema } from "./inlineContent.js";
3
+ import { odtStyleMappingForDefaultSchema } from "./styles.js";
4
+
5
+ export const odtDefaultSchemaMappings = {
6
+ blockMapping: odtBlockMappingForDefaultSchema,
7
+ inlineContentMapping: odtInlineContentMappingForDefaultSchema,
8
+ styleMapping: odtStyleMappingForDefaultSchema,
9
+ };
@@ -0,0 +1,30 @@
1
+ import {
2
+ DefaultInlineContentSchema,
3
+ InlineContentMapping,
4
+ } from "@blocknote/core";
5
+
6
+ export const odtInlineContentMappingForDefaultSchema: InlineContentMapping<
7
+ DefaultInlineContentSchema,
8
+ any,
9
+ JSX.Element,
10
+ JSX.Element
11
+ > = {
12
+ link: (ic, exporter) => {
13
+ const content = ic.content.map((c) => exporter.transformStyledText(c));
14
+
15
+ return (
16
+ <text:a
17
+ xlink:type="simple"
18
+ text:style-name="Internet_20_link"
19
+ office:target-frame-name="_top"
20
+ xlink:show="replace"
21
+ xlink:href={ic.href}>
22
+ {content}
23
+ </text:a>
24
+ );
25
+ },
26
+
27
+ text: (ic, exporter) => {
28
+ return exporter.transformStyledText(ic);
29
+ },
30
+ };
@@ -0,0 +1,59 @@
1
+ import { DefaultStyleSchema, StyleMapping } from "@blocknote/core";
2
+ export const odtStyleMappingForDefaultSchema: StyleMapping<
3
+ DefaultStyleSchema,
4
+ Record<string, string>
5
+ > = {
6
+ bold: (val): Record<string, string> => {
7
+ if (!val) {
8
+ return {};
9
+ }
10
+ return { "fo:font-weight": "bold" };
11
+ },
12
+
13
+ italic: (val): Record<string, string> => {
14
+ if (!val) {
15
+ return {};
16
+ }
17
+ return { "fo:font-style": "italic" };
18
+ },
19
+
20
+ underline: (val): Record<string, string> => {
21
+ if (!val) {
22
+ return {};
23
+ }
24
+ return { "style:text-underline-style": "solid" };
25
+ },
26
+
27
+ strike: (val): Record<string, string> => {
28
+ if (!val) {
29
+ return {};
30
+ }
31
+ return { "style:text-line-through-style": "solid" };
32
+ },
33
+
34
+ textColor: (val, exporter): Record<string, string> => {
35
+ if (!val) {
36
+ return {};
37
+ }
38
+ const color =
39
+ exporter.options.colors[val as keyof typeof exporter.options.colors].text;
40
+ return { "fo:color": color };
41
+ },
42
+
43
+ backgroundColor: (val, exporter): Record<string, string> => {
44
+ if (!val) {
45
+ return {};
46
+ }
47
+ const color =
48
+ exporter.options.colors[val as keyof typeof exporter.options.colors]
49
+ .background;
50
+ return { "fo:background-color": color };
51
+ },
52
+
53
+ code: (val): Record<string, string> => {
54
+ if (!val) {
55
+ return {};
56
+ }
57
+ return { "style:font-name": "Courier New" };
58
+ },
59
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./defaultSchema/index.js";
2
+ export * from "./odtExporter.js";
@@ -0,0 +1,80 @@
1
+ import { BlockNoteSchema, defaultBlockSpecs, PageBreak } from "@blocknote/core";
2
+ import { testDocument } from "@shared/testDocument.js";
3
+ import { BlobReader, TextWriter, ZipReader } from "@zip.js/zip.js";
4
+ import { beforeAll, describe, expect, it } from "vitest";
5
+ import xmlFormat from "xml-formatter";
6
+ import { odtDefaultSchemaMappings } from "./defaultSchema/index.js";
7
+ import { ODTExporter } from "./odtExporter.js";
8
+
9
+ beforeAll(async () => {
10
+ // @ts-ignore
11
+ globalThis.Blob = (await import("node:buffer")).Blob;
12
+ });
13
+
14
+ describe("exporter", () => {
15
+ it("should export a document", { timeout: 10000 }, async () => {
16
+ const exporter = new ODTExporter(
17
+ BlockNoteSchema.create({
18
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
19
+ }),
20
+ odtDefaultSchemaMappings
21
+ );
22
+ const odt = await exporter.toODTDocument(testDocument);
23
+ await testODTDocumentAgainstSnapshot(odt, {
24
+ styles: "__snapshots__/basic/styles.xml",
25
+ content: "__snapshots__/basic/content.xml",
26
+ });
27
+ });
28
+
29
+ it(
30
+ "should export a document with custom document options",
31
+ { timeout: 10000 },
32
+ async () => {
33
+ const exporter = new ODTExporter(
34
+ BlockNoteSchema.create({
35
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
36
+ }),
37
+ odtDefaultSchemaMappings
38
+ );
39
+
40
+ const odt = await exporter.toODTDocument(testDocument, {
41
+ footer: "<text:p>FOOTER</text:p>",
42
+ header: new DOMParser().parseFromString(
43
+ `<text:p xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">HEADER</text:p>`,
44
+ "text/xml"
45
+ ),
46
+ });
47
+
48
+ await testODTDocumentAgainstSnapshot(odt, {
49
+ styles: "__snapshots__/withCustomOptions/styles.xml",
50
+ content: "__snapshots__/withCustomOptions/content.xml",
51
+ });
52
+ }
53
+ );
54
+ });
55
+
56
+ async function testODTDocumentAgainstSnapshot(
57
+ odt: globalThis.Blob,
58
+ snapshots: {
59
+ styles: string;
60
+ content: string;
61
+ }
62
+ ) {
63
+ const zipReader = new ZipReader(new BlobReader(odt));
64
+ const entries = await zipReader.getEntries();
65
+ const stylesXMLWriter = new TextWriter();
66
+ const contentXMLWriter = new TextWriter();
67
+ const stylesXML = entries.find((entry) => entry.filename === "styles.xml");
68
+ const contentXML = entries.find((entry) => {
69
+ return entry.filename === "content.xml";
70
+ });
71
+
72
+ expect(stylesXML).toBeDefined();
73
+ expect(contentXML).toBeDefined();
74
+ expect(
75
+ xmlFormat(await stylesXML!.getData!(stylesXMLWriter))
76
+ ).toMatchFileSnapshot(snapshots.styles);
77
+ expect(
78
+ xmlFormat(await contentXML!.getData!(contentXMLWriter))
79
+ ).toMatchFileSnapshot(snapshots.content);
80
+ }