@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,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
|
+
};
|
package/src/odt/index.ts
ADDED
|
@@ -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
|
+
}
|