@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.
Files changed (56) hide show
  1. package/README.md +88 -2
  2. package/dist/attachment.d.ts +3 -0
  3. package/dist/attachment.js +8 -0
  4. package/dist/clean-content.d.ts +6 -0
  5. package/dist/clean-content.js +58 -0
  6. package/dist/clean-html.d.ts +3 -0
  7. package/dist/clean-html.js +161 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +11 -0
  10. package/dist/export/bundle.d.ts +34 -0
  11. package/dist/export/bundle.js +41 -0
  12. package/dist/export/docx.d.ts +5 -0
  13. package/dist/export/docx.js +382 -0
  14. package/dist/export/fonts.d.ts +16 -0
  15. package/dist/export/fonts.js +37 -0
  16. package/dist/export/html-blocks.d.ts +58 -0
  17. package/dist/export/html-blocks.js +170 -0
  18. package/dist/export/html.d.ts +8 -0
  19. package/dist/export/html.js +31 -0
  20. package/dist/export/image-size.d.ts +8 -0
  21. package/dist/export/image-size.js +83 -0
  22. package/dist/export/pdf.d.ts +28 -0
  23. package/dist/export/pdf.js +84 -0
  24. package/dist/export/pptx.d.ts +5 -0
  25. package/dist/export/pptx.js +310 -0
  26. package/dist/export.d.ts +19 -0
  27. package/dist/export.js +39 -0
  28. package/dist/font-widths.d.ts +8 -0
  29. package/dist/font-widths.js +3430 -0
  30. package/dist/gotenberg.d.ts +8 -0
  31. package/dist/gotenberg.js +39 -0
  32. package/dist/import/documents.d.ts +12 -0
  33. package/dist/import/documents.js +44 -0
  34. package/dist/import/html-to-blocks.d.ts +6 -0
  35. package/dist/import/html-to-blocks.js +58 -0
  36. package/dist/import/images.d.ts +10 -0
  37. package/dist/import/images.js +23 -0
  38. package/dist/import/pptx-package.d.ts +22 -0
  39. package/dist/import/pptx-package.js +61 -0
  40. package/dist/import/pptx-placeholders.d.ts +11 -0
  41. package/dist/import/pptx-placeholders.js +44 -0
  42. package/dist/import/pptx-slide.d.ts +19 -0
  43. package/dist/import/pptx-slide.js +324 -0
  44. package/dist/import/pptx-theme.d.ts +18 -0
  45. package/dist/import/pptx-theme.js +161 -0
  46. package/dist/import/pptx-xml.d.ts +23 -0
  47. package/dist/import/pptx-xml.js +27 -0
  48. package/dist/import/pptx.d.ts +9 -0
  49. package/dist/import/pptx.js +112 -0
  50. package/dist/import/titles.d.ts +3 -0
  51. package/dist/import/titles.js +6 -0
  52. package/dist/import.d.ts +14 -0
  53. package/dist/import.js +66 -0
  54. package/dist/text-measure.d.ts +4 -0
  55. package/dist/text-measure.js +98 -0
  56. 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