@pollium/vyva-convert 0.0.0-stage → 0.1.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/README.md +88 -2
- package/dist/attachment.d.ts +3 -0
- package/dist/attachment.js +8 -0
- package/dist/clean-content.d.ts +6 -0
- package/dist/clean-content.js +58 -0
- package/dist/clean-html.d.ts +3 -0
- package/dist/clean-html.js +161 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +11 -0
- package/dist/export/bundle.d.ts +34 -0
- package/dist/export/bundle.js +41 -0
- package/dist/export/docx.d.ts +5 -0
- package/dist/export/docx.js +382 -0
- package/dist/export/fonts.d.ts +16 -0
- package/dist/export/fonts.js +37 -0
- package/dist/export/html-blocks.d.ts +58 -0
- package/dist/export/html-blocks.js +170 -0
- package/dist/export/html.d.ts +8 -0
- package/dist/export/html.js +31 -0
- package/dist/export/image-size.d.ts +8 -0
- package/dist/export/image-size.js +83 -0
- package/dist/export/pdf.d.ts +28 -0
- package/dist/export/pdf.js +84 -0
- package/dist/export/pptx.d.ts +5 -0
- package/dist/export/pptx.js +310 -0
- package/dist/export.d.ts +19 -0
- package/dist/export.js +39 -0
- package/dist/font-widths.d.ts +8 -0
- package/dist/font-widths.js +3430 -0
- package/dist/gotenberg.d.ts +8 -0
- package/dist/gotenberg.js +39 -0
- package/dist/import/documents.d.ts +12 -0
- package/dist/import/documents.js +44 -0
- package/dist/import/html-to-blocks.d.ts +6 -0
- package/dist/import/html-to-blocks.js +58 -0
- package/dist/import/images.d.ts +10 -0
- package/dist/import/images.js +23 -0
- package/dist/import/pptx-package.d.ts +22 -0
- package/dist/import/pptx-package.js +61 -0
- package/dist/import/pptx-placeholders.d.ts +11 -0
- package/dist/import/pptx-placeholders.js +44 -0
- package/dist/import/pptx-slide.d.ts +19 -0
- package/dist/import/pptx-slide.js +324 -0
- package/dist/import/pptx-theme.d.ts +18 -0
- package/dist/import/pptx-theme.js +161 -0
- package/dist/import/pptx-xml.d.ts +23 -0
- package/dist/import/pptx-xml.js +27 -0
- package/dist/import/pptx.d.ts +9 -0
- package/dist/import/pptx.js +112 -0
- package/dist/import/titles.d.ts +3 -0
- package/dist/import/titles.js +6 -0
- package/dist/import.d.ts +14 -0
- package/dist/import.js +66 -0
- package/dist/text-measure.d.ts +4 -0
- package/dist/text-measure.js +98 -0
- package/package.json +49 -3
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { fitWithin } from "./image-size.js";
|
|
2
|
+
import { blocksOfHtml } from "./html-blocks.js";
|
|
3
|
+
import { CODE_FONT } from "@pollium/vyva-core/code";
|
|
4
|
+
import { AlignmentType, BorderStyle, Document, ExternalHyperlink, Footer, Header, HeadingLevel, ImageRun, LevelFormat, Packer, PageBreak, PageNumber, Paragraph, ShadingType, Table, TableCell, TableRow, TextRun, WidthType } from "docx";
|
|
5
|
+
//#region src/export/docx.ts
|
|
6
|
+
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
7
|
+
const TWIPS_PER_MM = 56.7;
|
|
8
|
+
const A4_WIDTH_MM = 210;
|
|
9
|
+
const PX_PER_MM = 3.78;
|
|
10
|
+
const CODE_FILL = "F3F4F6";
|
|
11
|
+
const MUTED = "6B7280";
|
|
12
|
+
const LINE = "D5D9E0";
|
|
13
|
+
const DOCX_IMAGE_TYPES = {
|
|
14
|
+
png: "png",
|
|
15
|
+
jpg: "jpg",
|
|
16
|
+
gif: "gif"
|
|
17
|
+
};
|
|
18
|
+
const hex = (color) => color.replace("#", "").toUpperCase();
|
|
19
|
+
const lighten = (color, amount) => {
|
|
20
|
+
const value = hex(color);
|
|
21
|
+
const channel = (offset) => Math.round(parseInt(value.slice(offset, offset + 2), 16) * amount + 255 * (1 - amount)).toString(16).padStart(2, "0");
|
|
22
|
+
return `${channel(0)}${channel(2)}${channel(4)}`.toUpperCase();
|
|
23
|
+
};
|
|
24
|
+
const ALIGNMENTS = {
|
|
25
|
+
left: AlignmentType.LEFT,
|
|
26
|
+
center: AlignmentType.CENTER,
|
|
27
|
+
right: AlignmentType.RIGHT,
|
|
28
|
+
justify: AlignmentType.JUSTIFIED
|
|
29
|
+
};
|
|
30
|
+
const HEADINGS = [
|
|
31
|
+
HeadingLevel.HEADING_1,
|
|
32
|
+
HeadingLevel.HEADING_2,
|
|
33
|
+
HeadingLevel.HEADING_3,
|
|
34
|
+
HeadingLevel.HEADING_4
|
|
35
|
+
];
|
|
36
|
+
const levels = (bullet) => [
|
|
37
|
+
0,
|
|
38
|
+
1,
|
|
39
|
+
2
|
|
40
|
+
].map((level) => ({
|
|
41
|
+
level,
|
|
42
|
+
format: bullet ? LevelFormat.BULLET : [
|
|
43
|
+
LevelFormat.DECIMAL,
|
|
44
|
+
LevelFormat.LOWER_LETTER,
|
|
45
|
+
LevelFormat.LOWER_ROMAN
|
|
46
|
+
][level],
|
|
47
|
+
text: bullet ? [
|
|
48
|
+
"•",
|
|
49
|
+
"◦",
|
|
50
|
+
"▪"
|
|
51
|
+
][level] : `%${level + 1}.`,
|
|
52
|
+
alignment: AlignmentType.LEFT,
|
|
53
|
+
style: { paragraph: { indent: {
|
|
54
|
+
left: 360 * (level + 1),
|
|
55
|
+
hanging: 260
|
|
56
|
+
} } }
|
|
57
|
+
}));
|
|
58
|
+
var DocxComposer = class {
|
|
59
|
+
#bundle;
|
|
60
|
+
#content;
|
|
61
|
+
#listInstance = 0;
|
|
62
|
+
constructor(bundle, content) {
|
|
63
|
+
this.#bundle = bundle;
|
|
64
|
+
this.#content = content;
|
|
65
|
+
}
|
|
66
|
+
get accent() {
|
|
67
|
+
return hex(this.#content.design.accentColor);
|
|
68
|
+
}
|
|
69
|
+
get maxImageWidth() {
|
|
70
|
+
const { margins } = this.#content.design;
|
|
71
|
+
return Math.round((A4_WIDTH_MM - margins.left - margins.right) * PX_PER_MM);
|
|
72
|
+
}
|
|
73
|
+
runs(runs, extra = {}) {
|
|
74
|
+
return runs.map((run) => {
|
|
75
|
+
if (run.isBreak === true) return new TextRun({
|
|
76
|
+
text: "",
|
|
77
|
+
break: 1
|
|
78
|
+
});
|
|
79
|
+
const text = new TextRun({
|
|
80
|
+
text: run.text,
|
|
81
|
+
bold: run.bold || extra.bold,
|
|
82
|
+
italics: run.italic,
|
|
83
|
+
strike: run.strike,
|
|
84
|
+
...run.underline || run.link !== null ? { underline: {} } : {},
|
|
85
|
+
...run.color !== null ? { color: hex(run.color) } : run.link !== null ? { color: this.accent } : extra.color === void 0 ? {} : { color: extra.color },
|
|
86
|
+
...run.code ? { font: CODE_FONT } : {},
|
|
87
|
+
...run.sizePx !== null ? { size: Math.round(run.sizePx * .75 * 2) } : extra.size === void 0 ? {} : { size: extra.size },
|
|
88
|
+
...run.highlight !== null ? { shading: {
|
|
89
|
+
type: ShadingType.CLEAR,
|
|
90
|
+
fill: hex(run.highlight),
|
|
91
|
+
color: "auto"
|
|
92
|
+
} } : run.code ? { shading: {
|
|
93
|
+
type: ShadingType.CLEAR,
|
|
94
|
+
fill: CODE_FILL,
|
|
95
|
+
color: "auto"
|
|
96
|
+
} } : {}
|
|
97
|
+
});
|
|
98
|
+
return run.link === null || !/^(https?:|mailto:)/i.test(run.link) ? text : new ExternalHyperlink({
|
|
99
|
+
link: run.link,
|
|
100
|
+
children: [text]
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
image(image, block) {
|
|
105
|
+
const type = image === void 0 ? void 0 : DOCX_IMAGE_TYPES[image.extension];
|
|
106
|
+
if (image === void 0 || type === void 0) return null;
|
|
107
|
+
const maxWidth = block.widthPx !== null ? Math.min(this.maxImageWidth, Math.round(block.widthPx)) : block.widthPercent === null ? this.maxImageWidth : Math.round(this.maxImageWidth * Math.min(100, block.widthPercent) / 100);
|
|
108
|
+
const size = fitWithin(image.size, maxWidth, 900);
|
|
109
|
+
const alt = block.alt;
|
|
110
|
+
return new Paragraph({
|
|
111
|
+
alignment: block.align === "left" ? AlignmentType.LEFT : block.align === "right" ? AlignmentType.RIGHT : AlignmentType.CENTER,
|
|
112
|
+
spacing: {
|
|
113
|
+
before: 120,
|
|
114
|
+
after: 120
|
|
115
|
+
},
|
|
116
|
+
children: [new ImageRun({
|
|
117
|
+
type,
|
|
118
|
+
data: image.data,
|
|
119
|
+
transformation: size,
|
|
120
|
+
altText: {
|
|
121
|
+
name: alt || "Image",
|
|
122
|
+
description: alt,
|
|
123
|
+
title: alt
|
|
124
|
+
}
|
|
125
|
+
})]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
block(block) {
|
|
129
|
+
if (block.kind === "heading") return [new Paragraph({
|
|
130
|
+
heading: HEADINGS[block.level - 1],
|
|
131
|
+
...block.align === null ? {} : { alignment: ALIGNMENTS[block.align] },
|
|
132
|
+
children: this.runs(block.runs)
|
|
133
|
+
})];
|
|
134
|
+
if (block.kind === "paragraph") return [new Paragraph({
|
|
135
|
+
spacing: { after: 120 },
|
|
136
|
+
...block.align === null ? {} : { alignment: ALIGNMENTS[block.align] },
|
|
137
|
+
...block.quote ? {
|
|
138
|
+
indent: { left: 360 },
|
|
139
|
+
border: { left: {
|
|
140
|
+
style: BorderStyle.SINGLE,
|
|
141
|
+
size: 18,
|
|
142
|
+
color: this.accent,
|
|
143
|
+
space: 8
|
|
144
|
+
} }
|
|
145
|
+
} : {},
|
|
146
|
+
children: this.runs(block.runs, block.quote ? { color: MUTED } : {})
|
|
147
|
+
})];
|
|
148
|
+
if (block.kind === "list") {
|
|
149
|
+
this.#listInstance += 1;
|
|
150
|
+
const instance = this.#listInstance;
|
|
151
|
+
return block.items.map((item) => item.checked === null ? new Paragraph({
|
|
152
|
+
numbering: {
|
|
153
|
+
reference: block.ordered ? "numbers" : "bullets",
|
|
154
|
+
level: Math.min(item.level, 2),
|
|
155
|
+
instance
|
|
156
|
+
},
|
|
157
|
+
children: this.runs(item.runs)
|
|
158
|
+
}) : new Paragraph({
|
|
159
|
+
indent: { left: 360 * (item.level + 1) },
|
|
160
|
+
children: [new TextRun({
|
|
161
|
+
text: item.checked ? "☑ " : "☐ ",
|
|
162
|
+
color: this.accent
|
|
163
|
+
}), ...this.runs(item.runs)]
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
if (block.kind === "table") {
|
|
167
|
+
const columns = Math.max(1, ...block.rows.map((row) => row.length));
|
|
168
|
+
return [new Table({
|
|
169
|
+
width: {
|
|
170
|
+
size: 100,
|
|
171
|
+
type: WidthType.PERCENTAGE
|
|
172
|
+
},
|
|
173
|
+
rows: block.rows.map((row) => new TableRow({
|
|
174
|
+
...row.every((cell) => cell.header) ? { tableHeader: true } : {},
|
|
175
|
+
children: Array.from({ length: columns }, (_, index) => {
|
|
176
|
+
const cell = row[index] ?? {
|
|
177
|
+
runs: [],
|
|
178
|
+
header: false
|
|
179
|
+
};
|
|
180
|
+
return new TableCell({
|
|
181
|
+
children: [new Paragraph({ children: this.runs(cell.runs, cell.header ? { bold: true } : {}) })],
|
|
182
|
+
...cell.header ? { shading: {
|
|
183
|
+
type: ShadingType.CLEAR,
|
|
184
|
+
fill: lighten(this.#content.design.accentColor, .12),
|
|
185
|
+
color: "auto"
|
|
186
|
+
} } : {},
|
|
187
|
+
margins: {
|
|
188
|
+
top: 60,
|
|
189
|
+
bottom: 60,
|
|
190
|
+
left: 100,
|
|
191
|
+
right: 100
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
})
|
|
195
|
+
}))
|
|
196
|
+
}), new Paragraph({ children: [] })];
|
|
197
|
+
}
|
|
198
|
+
if (block.kind === "code") return block.text.split("\n").map((line, index, lines) => new Paragraph({
|
|
199
|
+
shading: {
|
|
200
|
+
type: ShadingType.CLEAR,
|
|
201
|
+
fill: CODE_FILL,
|
|
202
|
+
color: "auto"
|
|
203
|
+
},
|
|
204
|
+
spacing: {
|
|
205
|
+
before: index === 0 ? 120 : 0,
|
|
206
|
+
after: index === lines.length - 1 ? 160 : 0
|
|
207
|
+
},
|
|
208
|
+
children: [new TextRun({
|
|
209
|
+
text: line === "" ? " " : line,
|
|
210
|
+
font: CODE_FONT,
|
|
211
|
+
size: 18
|
|
212
|
+
})]
|
|
213
|
+
}));
|
|
214
|
+
if (block.kind === "image") {
|
|
215
|
+
const paragraph = this.image(this.#bundle.images.get(block.fileId), block);
|
|
216
|
+
return paragraph === null ? [] : [paragraph];
|
|
217
|
+
}
|
|
218
|
+
if (block.kind === "rule") return [new Paragraph({
|
|
219
|
+
border: { bottom: {
|
|
220
|
+
style: BorderStyle.SINGLE,
|
|
221
|
+
size: 6,
|
|
222
|
+
color: LINE,
|
|
223
|
+
space: 4
|
|
224
|
+
} },
|
|
225
|
+
children: []
|
|
226
|
+
})];
|
|
227
|
+
return [new Paragraph({ children: [new PageBreak()] })];
|
|
228
|
+
}
|
|
229
|
+
body() {
|
|
230
|
+
return this.#content.blocks.flatMap((block) => blocksOfHtml(block.html)).flatMap((block) => this.block(block));
|
|
231
|
+
}
|
|
232
|
+
header() {
|
|
233
|
+
const { design } = this.#content;
|
|
234
|
+
const logo = design.headerLogoFileId === null ? void 0 : this.#bundle.images.get(design.headerLogoFileId);
|
|
235
|
+
const type = logo === void 0 ? void 0 : DOCX_IMAGE_TYPES[logo.extension];
|
|
236
|
+
const children = [];
|
|
237
|
+
if (logo !== void 0 && type !== void 0) children.push(new ImageRun({
|
|
238
|
+
type,
|
|
239
|
+
data: logo.data,
|
|
240
|
+
transformation: fitWithin(logo.size, 160, 40),
|
|
241
|
+
altText: {
|
|
242
|
+
name: "Logo",
|
|
243
|
+
description: "Logo",
|
|
244
|
+
title: "Logo"
|
|
245
|
+
}
|
|
246
|
+
}));
|
|
247
|
+
if (design.headerText.trim() !== "") children.push(new TextRun({
|
|
248
|
+
text: `${children.length > 0 ? " " : ""}${design.headerText}`,
|
|
249
|
+
color: MUTED,
|
|
250
|
+
size: 16
|
|
251
|
+
}));
|
|
252
|
+
return new Header({ children: children.length === 0 ? [] : [new Paragraph({
|
|
253
|
+
border: { bottom: {
|
|
254
|
+
style: BorderStyle.SINGLE,
|
|
255
|
+
size: 4,
|
|
256
|
+
color: LINE,
|
|
257
|
+
space: 4
|
|
258
|
+
} },
|
|
259
|
+
children
|
|
260
|
+
})] });
|
|
261
|
+
}
|
|
262
|
+
footer() {
|
|
263
|
+
const { design } = this.#content;
|
|
264
|
+
const children = [];
|
|
265
|
+
if (design.footerText.trim() !== "") children.push(new TextRun({
|
|
266
|
+
text: design.showPageNumbers ? `${design.footerText} · ` : design.footerText,
|
|
267
|
+
color: MUTED,
|
|
268
|
+
size: 16
|
|
269
|
+
}));
|
|
270
|
+
if (design.showPageNumbers) children.push(new TextRun({
|
|
271
|
+
children: [
|
|
272
|
+
PageNumber.CURRENT,
|
|
273
|
+
" / ",
|
|
274
|
+
PageNumber.TOTAL_PAGES
|
|
275
|
+
],
|
|
276
|
+
color: MUTED,
|
|
277
|
+
size: 16
|
|
278
|
+
}));
|
|
279
|
+
return new Footer({ children: children.length === 0 ? [] : [new Paragraph({
|
|
280
|
+
alignment: AlignmentType.RIGHT,
|
|
281
|
+
children
|
|
282
|
+
})] });
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const buildDocx = async (content, bundle) => {
|
|
286
|
+
const composer = new DocxComposer(bundle, content);
|
|
287
|
+
const { design } = content;
|
|
288
|
+
const size = Math.round(design.baseSize * 2);
|
|
289
|
+
const ink = hex(design.textColor);
|
|
290
|
+
const accent = composer.accent;
|
|
291
|
+
const margin = (mm) => Math.round(mm * TWIPS_PER_MM);
|
|
292
|
+
const document = new Document({
|
|
293
|
+
title: bundle.title,
|
|
294
|
+
styles: { default: {
|
|
295
|
+
document: {
|
|
296
|
+
run: {
|
|
297
|
+
font: design.font,
|
|
298
|
+
size,
|
|
299
|
+
color: ink
|
|
300
|
+
},
|
|
301
|
+
paragraph: { spacing: { line: 300 } }
|
|
302
|
+
},
|
|
303
|
+
heading1: {
|
|
304
|
+
run: {
|
|
305
|
+
font: design.font,
|
|
306
|
+
size: size * 2,
|
|
307
|
+
bold: true,
|
|
308
|
+
color: accent
|
|
309
|
+
},
|
|
310
|
+
paragraph: { spacing: {
|
|
311
|
+
before: 240,
|
|
312
|
+
after: 120
|
|
313
|
+
} }
|
|
314
|
+
},
|
|
315
|
+
heading2: {
|
|
316
|
+
run: {
|
|
317
|
+
font: design.font,
|
|
318
|
+
size: Math.round(size * 1.5),
|
|
319
|
+
bold: true,
|
|
320
|
+
color: accent
|
|
321
|
+
},
|
|
322
|
+
paragraph: { spacing: {
|
|
323
|
+
before: 280,
|
|
324
|
+
after: 100
|
|
325
|
+
} }
|
|
326
|
+
},
|
|
327
|
+
heading3: {
|
|
328
|
+
run: {
|
|
329
|
+
font: design.font,
|
|
330
|
+
size: Math.round(size * 1.2),
|
|
331
|
+
bold: true,
|
|
332
|
+
color: accent
|
|
333
|
+
},
|
|
334
|
+
paragraph: { spacing: {
|
|
335
|
+
before: 220,
|
|
336
|
+
after: 80
|
|
337
|
+
} }
|
|
338
|
+
},
|
|
339
|
+
heading4: {
|
|
340
|
+
run: {
|
|
341
|
+
font: design.font,
|
|
342
|
+
size: Math.round(size * 1.05),
|
|
343
|
+
bold: true,
|
|
344
|
+
color: accent
|
|
345
|
+
},
|
|
346
|
+
paragraph: { spacing: {
|
|
347
|
+
before: 200,
|
|
348
|
+
after: 60
|
|
349
|
+
} }
|
|
350
|
+
},
|
|
351
|
+
hyperlink: { run: {
|
|
352
|
+
color: accent,
|
|
353
|
+
underline: {}
|
|
354
|
+
} }
|
|
355
|
+
} },
|
|
356
|
+
numbering: { config: [{
|
|
357
|
+
reference: "bullets",
|
|
358
|
+
levels: levels(true)
|
|
359
|
+
}, {
|
|
360
|
+
reference: "numbers",
|
|
361
|
+
levels: levels(false)
|
|
362
|
+
}] },
|
|
363
|
+
sections: [{
|
|
364
|
+
properties: { page: { margin: {
|
|
365
|
+
top: margin(design.margins.top),
|
|
366
|
+
bottom: margin(design.margins.bottom),
|
|
367
|
+
left: margin(design.margins.left),
|
|
368
|
+
right: margin(design.margins.right)
|
|
369
|
+
} } },
|
|
370
|
+
headers: { default: composer.header() },
|
|
371
|
+
footers: { default: composer.footer() },
|
|
372
|
+
children: composer.body()
|
|
373
|
+
}]
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
data: await Packer.toBuffer(document),
|
|
377
|
+
contentType: DOCX_MIME,
|
|
378
|
+
extension: "docx"
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
export { buildDocx };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FontFamily } from "@pollium/vyva-core/content";
|
|
2
|
+
//#region src/export/fonts.d.ts
|
|
3
|
+
declare const SUBSETS: readonly ["latin", "latin-ext"];
|
|
4
|
+
declare const STYLES: readonly ["normal", "italic"];
|
|
5
|
+
type Subset = typeof SUBSETS[number];
|
|
6
|
+
type Style = typeof STYLES[number];
|
|
7
|
+
export interface FontFile {
|
|
8
|
+
family: FontFamily;
|
|
9
|
+
name: string;
|
|
10
|
+
style: Style;
|
|
11
|
+
subset: Subset;
|
|
12
|
+
data: Buffer;
|
|
13
|
+
}
|
|
14
|
+
export declare const fontFilesOf: (families: Iterable<FontFamily>) => Promise<FontFile[]>;
|
|
15
|
+
export declare const fontFaces: (files: FontFile[], urlOf: (file: FontFile) => string) => string;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
//#region src/export/fonts.ts
|
|
4
|
+
const PACKAGES = {
|
|
5
|
+
Inter: "inter",
|
|
6
|
+
Roboto: "roboto",
|
|
7
|
+
"Open Sans": "open-sans",
|
|
8
|
+
Montserrat: "montserrat",
|
|
9
|
+
Lora: "lora",
|
|
10
|
+
Merriweather: "merriweather",
|
|
11
|
+
"Playfair Display": "playfair-display",
|
|
12
|
+
"JetBrains Mono": "jetbrains-mono"
|
|
13
|
+
};
|
|
14
|
+
const SUBSETS = ["latin", "latin-ext"];
|
|
15
|
+
const STYLES = ["normal", "italic"];
|
|
16
|
+
const RANGES = {
|
|
17
|
+
latin: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
|
18
|
+
"latin-ext": "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF"
|
|
19
|
+
};
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const filesOf = (family) => {
|
|
22
|
+
const slug = PACKAGES[family];
|
|
23
|
+
return Promise.all(SUBSETS.flatMap((subset) => STYLES.map(async (style) => {
|
|
24
|
+
const name = `${slug}-${subset}-wght-${style}.woff2`;
|
|
25
|
+
return {
|
|
26
|
+
family,
|
|
27
|
+
name,
|
|
28
|
+
style,
|
|
29
|
+
subset,
|
|
30
|
+
data: await readFile(require.resolve(`@fontsource-variable/${slug}/files/${name}`))
|
|
31
|
+
};
|
|
32
|
+
})));
|
|
33
|
+
};
|
|
34
|
+
const fontFilesOf = async (families) => (await Promise.all([...new Set(families)].map(filesOf))).flat();
|
|
35
|
+
const fontFaces = (files, urlOf) => files.map((file) => `@font-face{font-family:'${file.family}';font-style:${file.style};font-weight:100 900;font-display:block;src:url('${urlOf(file)}') format('woff2');unicode-range:${RANGES[file.subset]}}`).join("\n");
|
|
36
|
+
//#endregion
|
|
37
|
+
export { fontFaces, fontFilesOf };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region src/export/html-blocks.d.ts
|
|
2
|
+
export interface Run {
|
|
3
|
+
text: string;
|
|
4
|
+
bold: boolean;
|
|
5
|
+
italic: boolean;
|
|
6
|
+
underline: boolean;
|
|
7
|
+
strike: boolean;
|
|
8
|
+
code: boolean;
|
|
9
|
+
color: string | null;
|
|
10
|
+
highlight: string | null;
|
|
11
|
+
sizePx: number | null;
|
|
12
|
+
link: string | null;
|
|
13
|
+
isBreak?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type Align = 'left' | 'center' | 'right' | 'justify';
|
|
16
|
+
export type Block = {
|
|
17
|
+
kind: 'paragraph';
|
|
18
|
+
runs: Run[];
|
|
19
|
+
align: Align | null;
|
|
20
|
+
quote: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'heading';
|
|
23
|
+
level: 1 | 2 | 3 | 4;
|
|
24
|
+
runs: Run[];
|
|
25
|
+
align: Align | null;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'list';
|
|
28
|
+
ordered: boolean;
|
|
29
|
+
items: Array<{
|
|
30
|
+
runs: Run[];
|
|
31
|
+
checked: boolean | null;
|
|
32
|
+
level: number;
|
|
33
|
+
}>;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'table';
|
|
36
|
+
rows: Array<Array<{
|
|
37
|
+
runs: Run[];
|
|
38
|
+
header: boolean;
|
|
39
|
+
}>>;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'code';
|
|
42
|
+
language: string | null;
|
|
43
|
+
text: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'image';
|
|
46
|
+
fileId: string;
|
|
47
|
+
alt: string;
|
|
48
|
+
widthPercent: number | null;
|
|
49
|
+
widthPx: number | null;
|
|
50
|
+
align: 'left' | 'center' | 'right';
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'rule';
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'pageBreak';
|
|
55
|
+
};
|
|
56
|
+
export declare const blocksOfHtml: (html: string) => Block[];
|
|
57
|
+
export declare const plainOfRuns: (runs: Run[]) => string;
|
|
58
|
+
//#endregion
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { DomUtils, parseDocument } from "htmlparser2";
|
|
2
|
+
//#region src/export/html-blocks.ts
|
|
3
|
+
const BASE = {
|
|
4
|
+
text: "",
|
|
5
|
+
bold: false,
|
|
6
|
+
italic: false,
|
|
7
|
+
underline: false,
|
|
8
|
+
strike: false,
|
|
9
|
+
code: false,
|
|
10
|
+
color: null,
|
|
11
|
+
highlight: null,
|
|
12
|
+
sizePx: null,
|
|
13
|
+
link: null
|
|
14
|
+
};
|
|
15
|
+
const HEADING_LEVELS = /* @__PURE__ */ new Map([
|
|
16
|
+
["h1", 1],
|
|
17
|
+
["h2", 2],
|
|
18
|
+
["h3", 3],
|
|
19
|
+
["h4", 4]
|
|
20
|
+
]);
|
|
21
|
+
const isElement = (node) => DomUtils.isTag(node);
|
|
22
|
+
const styleOf = (element) => Object.fromEntries((element.attribs.style ?? "").split(";").map((part) => part.split(":").map((value) => value.trim())).filter((pair) => pair.length >= 2 && pair[0] !== "").map(([key, ...value]) => [key.toLowerCase(), value.join(":")]));
|
|
23
|
+
const COLOR = /^#[0-9a-f]{6}$/i;
|
|
24
|
+
const toHex = (value) => {
|
|
25
|
+
if (value === void 0) return null;
|
|
26
|
+
if (COLOR.test(value)) return value.toLowerCase();
|
|
27
|
+
if (/^#[0-9a-f]{3}$/i.test(value)) return `#${value.slice(1).split("").map((char) => char + char).join("")}`.toLowerCase();
|
|
28
|
+
const match = /^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})/i.exec(value);
|
|
29
|
+
return match === null ? null : `#${match.slice(1, 4).map((part) => Math.min(255, Number(part)).toString(16).padStart(2, "0")).join("")}`;
|
|
30
|
+
};
|
|
31
|
+
const sizeToPx = (value) => {
|
|
32
|
+
if (value === void 0) return null;
|
|
33
|
+
const match = /^(\d+(?:\.\d+)?)(px|pt|em|rem)$/.exec(value);
|
|
34
|
+
if (match === null) return null;
|
|
35
|
+
const amount = Number(match[1]);
|
|
36
|
+
return match[2] === "pt" ? amount * 96 / 72 : match[2] === "px" ? amount : amount * 16;
|
|
37
|
+
};
|
|
38
|
+
const alignOf = (element) => {
|
|
39
|
+
const align = styleOf(element)["text-align"];
|
|
40
|
+
return align === "left" || align === "center" || align === "right" || align === "justify" ? align : null;
|
|
41
|
+
};
|
|
42
|
+
const runsOf = (nodes, inherited = BASE) => nodes.flatMap((node) => {
|
|
43
|
+
if (node.type === "text") return node.data === "" ? [] : [{
|
|
44
|
+
...inherited,
|
|
45
|
+
text: node.data.replace(/\s+/g, " ")
|
|
46
|
+
}];
|
|
47
|
+
if (!isElement(node)) return [];
|
|
48
|
+
const style = styleOf(node);
|
|
49
|
+
const next = {
|
|
50
|
+
...inherited,
|
|
51
|
+
bold: inherited.bold || node.name === "strong" || node.name === "b" || style["font-weight"] === "700" || style["font-weight"] === "bold",
|
|
52
|
+
italic: inherited.italic || node.name === "em" || node.name === "i",
|
|
53
|
+
underline: inherited.underline || node.name === "u",
|
|
54
|
+
strike: inherited.strike || node.name === "s",
|
|
55
|
+
code: inherited.code || node.name === "code",
|
|
56
|
+
color: toHex(style.color) ?? inherited.color,
|
|
57
|
+
highlight: node.name === "mark" ? toHex(style["background-color"]) ?? "#fff3a3" : toHex(style["background-color"]) ?? inherited.highlight,
|
|
58
|
+
sizePx: sizeToPx(style["font-size"]) ?? inherited.sizePx,
|
|
59
|
+
link: node.name === "a" && typeof node.attribs.href === "string" ? node.attribs.href : inherited.link
|
|
60
|
+
};
|
|
61
|
+
if (node.name === "br") return [{
|
|
62
|
+
...inherited,
|
|
63
|
+
text: "",
|
|
64
|
+
isBreak: true
|
|
65
|
+
}];
|
|
66
|
+
if (node.name === "img" || node.name === "input" || node.name === "label") return [];
|
|
67
|
+
return runsOf(node.children, next);
|
|
68
|
+
});
|
|
69
|
+
const textOf = (nodes) => nodes.map((node) => node.type === "text" ? node.data : isElement(node) ? textOf(node.children) : "").join("");
|
|
70
|
+
const trimRuns = (runs) => {
|
|
71
|
+
const copy = runs.map((run) => ({ ...run }));
|
|
72
|
+
if (copy.length > 0) copy[0].text = copy[0].text.replace(/^\s+/, "");
|
|
73
|
+
if (copy.length > 0) copy[copy.length - 1].text = copy[copy.length - 1].text.replace(/\s+$/, "");
|
|
74
|
+
return copy.filter((run) => run.text !== "" || run.isBreak === true);
|
|
75
|
+
};
|
|
76
|
+
const imageOf = (element) => {
|
|
77
|
+
const fileId = element.attribs["data-file-id"];
|
|
78
|
+
if (fileId === void 0) return null;
|
|
79
|
+
const width = styleOf(element).width ?? "";
|
|
80
|
+
const percent = /^(\d+(?:\.\d+)?)%$/.exec(width);
|
|
81
|
+
const pixels = /^(\d+(?:\.\d+)?)px$/.exec(width);
|
|
82
|
+
const align = element.attribs["data-align"];
|
|
83
|
+
return {
|
|
84
|
+
kind: "image",
|
|
85
|
+
fileId,
|
|
86
|
+
alt: element.attribs.alt ?? "",
|
|
87
|
+
widthPercent: percent === null ? null : Number(percent[1]),
|
|
88
|
+
widthPx: pixels === null ? null : Number(pixels[1]),
|
|
89
|
+
align: align === "left" || align === "right" ? align : "center"
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const listItems = (list, level) => list.children.filter(isElement).filter((child) => child.name === "li").flatMap((item) => {
|
|
93
|
+
const nested = item.children.filter(isElement).filter((child) => child.name === "ul" || child.name === "ol");
|
|
94
|
+
const own = item.children.filter((child) => !(isElement(child) && (child.name === "ul" || child.name === "ol")));
|
|
95
|
+
const checked = item.attribs["data-type"] === "taskItem" ? item.attribs["data-checked"] === "true" : null;
|
|
96
|
+
return [{
|
|
97
|
+
runs: trimRuns(runsOf(own)),
|
|
98
|
+
checked,
|
|
99
|
+
level
|
|
100
|
+
}, ...nested.flatMap((child) => listItems(child, level + 1))];
|
|
101
|
+
});
|
|
102
|
+
const tableRows = (table) => {
|
|
103
|
+
const rows = [];
|
|
104
|
+
const visit = (node) => {
|
|
105
|
+
for (const child of node.children.filter(isElement)) if (child.name === "tr") rows.push(child.children.filter(isElement).filter((cell) => cell.name === "td" || cell.name === "th").map((cell) => ({
|
|
106
|
+
runs: trimRuns(runsOf(cell.children)),
|
|
107
|
+
header: cell.name === "th"
|
|
108
|
+
})));
|
|
109
|
+
else if ([
|
|
110
|
+
"thead",
|
|
111
|
+
"tbody",
|
|
112
|
+
"tfoot"
|
|
113
|
+
].includes(child.name)) visit(child);
|
|
114
|
+
};
|
|
115
|
+
visit(table);
|
|
116
|
+
return rows;
|
|
117
|
+
};
|
|
118
|
+
const blockOf = (element) => {
|
|
119
|
+
const { name } = element;
|
|
120
|
+
const level = HEADING_LEVELS.get(name);
|
|
121
|
+
if (level !== void 0) return [{
|
|
122
|
+
kind: "heading",
|
|
123
|
+
level,
|
|
124
|
+
runs: trimRuns(runsOf(element.children)),
|
|
125
|
+
align: alignOf(element)
|
|
126
|
+
}];
|
|
127
|
+
if (name === "p") {
|
|
128
|
+
const images = element.children.filter(isElement).filter((child) => child.name === "img").flatMap((child) => imageOf(child) ?? []);
|
|
129
|
+
const runs = trimRuns(runsOf(element.children));
|
|
130
|
+
return [...runs.length === 0 && images.length > 0 ? [] : [{
|
|
131
|
+
kind: "paragraph",
|
|
132
|
+
runs,
|
|
133
|
+
align: alignOf(element),
|
|
134
|
+
quote: false
|
|
135
|
+
}], ...images];
|
|
136
|
+
}
|
|
137
|
+
if (name === "blockquote") return element.children.filter(isElement).flatMap(blockOf).map((block) => block.kind === "paragraph" ? {
|
|
138
|
+
...block,
|
|
139
|
+
quote: true
|
|
140
|
+
} : block);
|
|
141
|
+
if (name === "ul" || name === "ol") return [{
|
|
142
|
+
kind: "list",
|
|
143
|
+
ordered: name === "ol",
|
|
144
|
+
items: listItems(element, 0)
|
|
145
|
+
}];
|
|
146
|
+
if (name === "table") return [{
|
|
147
|
+
kind: "table",
|
|
148
|
+
rows: tableRows(element)
|
|
149
|
+
}];
|
|
150
|
+
if (name === "pre") {
|
|
151
|
+
const code = element.children.filter(isElement).find((child) => child.name === "code");
|
|
152
|
+
return [{
|
|
153
|
+
kind: "code",
|
|
154
|
+
language: /language-([a-z0-9+#-]+)/.exec(code?.attribs.class ?? element.attribs.class ?? "")?.[1] ?? null,
|
|
155
|
+
text: textOf(element.children).replace(/\n$/, "")
|
|
156
|
+
}];
|
|
157
|
+
}
|
|
158
|
+
if (name === "img") {
|
|
159
|
+
const image = imageOf(element);
|
|
160
|
+
return image === null ? [] : [image];
|
|
161
|
+
}
|
|
162
|
+
if (name === "hr") return [{ kind: "rule" }];
|
|
163
|
+
if (name === "div" && element.attribs["data-type"] === "page-break") return [{ kind: "pageBreak" }];
|
|
164
|
+
if (name === "div") return element.children.filter(isElement).flatMap(blockOf);
|
|
165
|
+
return [];
|
|
166
|
+
};
|
|
167
|
+
const blocksOfHtml = (html) => parseDocument(html).children.filter(isElement).flatMap(blockOf);
|
|
168
|
+
const plainOfRuns = (runs) => runs.map((run) => run.isBreak === true ? "\n" : run.text).join("");
|
|
169
|
+
//#endregion
|
|
170
|
+
export { blocksOfHtml, plainOfRuns };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ExportBundle, ExportedFile } from "./bundle.js";
|
|
2
|
+
import { Content } from "@pollium/vyva-core/content";
|
|
3
|
+
import { FileUrl } from "@pollium/vyva-core/render";
|
|
4
|
+
//#region src/export/html.d.ts
|
|
5
|
+
export declare const bodyOf: (content: Content, fileUrl: FileUrl) => string;
|
|
6
|
+
export declare const pageHtml: ({ title, language }: ExportBundle, css: string, body: string) => string;
|
|
7
|
+
export declare const buildHtml: (content: Content, bundle: ExportBundle) => Promise<ExportedFile>;
|
|
8
|
+
//#endregion
|