@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,6 @@
1
+ //#region src/import/titles.ts
2
+ const MAX_TITLE = 200;
3
+ const UNTITLED = "Untitled";
4
+ const titleOf = (text, filename) => (text.trim() || filename.replace(/\.[a-z0-9]+$/i, "").replace(/[_-]+/g, " ").trim() || UNTITLED).slice(0, MAX_TITLE);
5
+ //#endregion
6
+ export { titleOf };
@@ -0,0 +1,14 @@
1
+ import { SaveImage } from "./import/images.js";
2
+ import { Content } from "@pollium/vyva-core/content";
3
+ //#region src/import.d.ts
4
+ export declare const MAX_IMPORT_BYTES: number;
5
+ export interface ImportOptions {
6
+ saveImage: SaveImage;
7
+ maxBytes?: number;
8
+ }
9
+ export interface ImportedContent {
10
+ title: string;
11
+ content: Content;
12
+ }
13
+ export declare const importFile: (data: Uint8Array, filename: string, { saveImage, maxBytes }: ImportOptions) => Promise<ImportedContent>;
14
+ //#endregion
package/dist/import.js ADDED
@@ -0,0 +1,66 @@
1
+ import { ConvertError } from "./errors.js";
2
+ import { importDocx, importMarkdown, importPdf, importPlainText } from "./import/documents.js";
3
+ import { importPptx } from "./import/pptx.js";
4
+ import { unzipSync } from "fflate";
5
+ //#region src/import.ts
6
+ const MAX_IMPORT_BYTES = 26214400;
7
+ const ZIP_SIGNATURE = 67324752;
8
+ const PDF_SIGNATURE = "%PDF-";
9
+ const TEXT_SAMPLE_BYTES = 65536;
10
+ const BOM = /^/;
11
+ const zipParts = (buffer) => {
12
+ if (buffer.length < 4 || buffer.readUInt32LE(0) !== ZIP_SIGNATURE) return null;
13
+ try {
14
+ const names = /* @__PURE__ */ new Set();
15
+ unzipSync(buffer, { filter: (file) => {
16
+ names.add(file.name);
17
+ return false;
18
+ } });
19
+ return names;
20
+ } catch {
21
+ return null;
22
+ }
23
+ };
24
+ const isUtf8Text = (buffer) => {
25
+ const sample = buffer.subarray(0, TEXT_SAMPLE_BYTES);
26
+ if (sample.includes(0)) return false;
27
+ try {
28
+ new TextDecoder("utf-8", { fatal: true }).decode(sample);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+ const detect = (buffer, filename) => {
35
+ const parts = zipParts(buffer);
36
+ if (parts !== null) {
37
+ if (parts.has("word/document.xml")) return "docx";
38
+ return parts.has("ppt/presentation.xml") ? "pptx" : null;
39
+ }
40
+ if (buffer.subarray(0, 5).toString("latin1") === PDF_SIGNATURE) return "pdf";
41
+ if (!isUtf8Text(buffer)) return null;
42
+ const extension = filename.toLowerCase().split(".").at(-1) ?? "";
43
+ if (extension === "md" || extension === "markdown") return "markdown";
44
+ if (extension === "txt") return "text";
45
+ return null;
46
+ };
47
+ const convert = async (detected, buffer, filename, saveImage) => {
48
+ if (detected === "docx") return importDocx(buffer, filename, saveImage);
49
+ if (detected === "pptx") return importPptx(buffer, filename, saveImage);
50
+ if (detected === "pdf") return importPdf(buffer, filename);
51
+ const text = buffer.toString("utf8").replace(BOM, "");
52
+ return detected === "markdown" ? importMarkdown(text, filename) : importPlainText(text, filename);
53
+ };
54
+ const isEmpty = (content) => content.kind === "document" ? content.blocks.length === 0 : content.slides.length === 0;
55
+ const importFile = async (data, filename, { saveImage, maxBytes = MAX_IMPORT_BYTES }) => {
56
+ if (data.length > maxBytes) throw new ConvertError("too-large", `The file is larger than ${maxBytes} bytes`);
57
+ const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
58
+ const name = filename.replace(/[\\/]/g, "_").slice(-200);
59
+ const detected = detect(buffer, name);
60
+ if (detected === null) throw new ConvertError("unsupported", `${name} is not a file Vyva can import`);
61
+ const imported = await convert(detected, buffer, name, saveImage);
62
+ if (isEmpty(imported.content)) throw new ConvertError("empty", `${name} has nothing to import`);
63
+ return imported;
64
+ };
65
+ //#endregion
66
+ export { MAX_IMPORT_BYTES, importFile };
@@ -0,0 +1,4 @@
1
+ import { FontFamily, TextStyle } from "@pollium/vyva-core/content";
2
+ //#region src/text-measure.d.ts
3
+ export declare const textHeight: (html: string, style: TextStyle, font: FontFamily, width: number) => number;
4
+ //#endregion
@@ -0,0 +1,98 @@
1
+ import { FONT_WIDTHS, MEASURED_CHARS } from "./font-widths.js";
2
+ import { CODE_FONT } from "@pollium/vyva-core/code";
3
+ //#region src/text-measure.ts
4
+ const EM = 1e3;
5
+ const BLOCK_GAP_EMS = .35;
6
+ const LIST_INDENT_EMS = 1.15;
7
+ const WIDTH_SAFETY = .97;
8
+ const BLOCK_END = /<\/(?:p|li|h[1-6])>/i;
9
+ const TAG = /<(\/?)([a-z0-9]+)[^>]*>|([^<]+)/gi;
10
+ const BOLD_TAGS = /* @__PURE__ */ new Set([
11
+ "strong",
12
+ "b",
13
+ "h1",
14
+ "h2",
15
+ "h3",
16
+ "h4",
17
+ "h5",
18
+ "h6"
19
+ ]);
20
+ const ENTITIES = {
21
+ amp: "&",
22
+ lt: "<",
23
+ gt: ">",
24
+ quot: "\"",
25
+ apos: "'",
26
+ nbsp: " ",
27
+ "#39": "'"
28
+ };
29
+ const INDEX = new Map([...MEASURED_CHARS].map((char, index) => [char, index]));
30
+ const FALLBACK = INDEX.get("n") ?? 0;
31
+ const decoded = (text) => text.replace(/&(#?\w+);/g, (entity, name) => ENTITIES[name] ?? (name.startsWith("#") ? String.fromCodePoint(Number(name.slice(1))) : entity));
32
+ const glyphsOf = (html, font, isBold) => {
33
+ const glyphs = [];
34
+ let bold = 0;
35
+ let code = 0;
36
+ for (const [, closing, tag, text] of html.matchAll(TAG)) {
37
+ if (text !== void 0) {
38
+ for (const char of decoded(text).replace(/\s+/g, " ")) glyphs.push({
39
+ char,
40
+ font: code > 0 ? CODE_FONT : font,
41
+ isBold: isBold || bold > 0
42
+ });
43
+ continue;
44
+ }
45
+ const step = closing === "/" ? -1 : 1;
46
+ const name = tag.toLowerCase();
47
+ if (BOLD_TAGS.has(name)) bold = Math.max(0, bold + step);
48
+ else if (name === "code") code = Math.max(0, code + step);
49
+ }
50
+ return glyphs;
51
+ };
52
+ const blocksOf = (html, font, isBold) => html.split(BLOCK_END).flatMap((chunk) => {
53
+ const glyphs = glyphsOf(chunk, font, isBold);
54
+ while (glyphs[0]?.char === " ") glyphs.shift();
55
+ while (glyphs.at(-1)?.char === " ") glyphs.pop();
56
+ return glyphs.length === 0 ? [] : [{
57
+ glyphs,
58
+ isItem: /<li\b/i.test(chunk)
59
+ }];
60
+ });
61
+ const widthOf = (glyph, fontSize) => {
62
+ return FONT_WIDTHS[glyph.font][glyph.isBold ? "bold" : "regular"][INDEX.get(glyph.char) ?? FALLBACK] / EM * fontSize;
63
+ };
64
+ const linesOf = (glyphs, fontSize, width) => {
65
+ let lines = 1;
66
+ let line = 0;
67
+ let word = 0;
68
+ let space = 0;
69
+ const place = () => {
70
+ if (word > width) lines = Infinity;
71
+ if (line > 0 && line + space + word > width) {
72
+ lines += 1;
73
+ line = 0;
74
+ }
75
+ line += (line === 0 ? 0 : space) + word;
76
+ word = 0;
77
+ };
78
+ for (const glyph of glyphs) {
79
+ if (glyph.char !== " ") {
80
+ word += widthOf(glyph, fontSize);
81
+ continue;
82
+ }
83
+ place();
84
+ space = widthOf(glyph, fontSize);
85
+ }
86
+ place();
87
+ return lines;
88
+ };
89
+ const textHeight = (html, style, font, width) => {
90
+ const blocks = blocksOf(html, font, style.isBold);
91
+ return blocks.reduce((height, block, index) => {
92
+ const indent = block.isItem ? LIST_INDENT_EMS * style.fontSize : 0;
93
+ const gap = !block.isItem && index === blocks.length - 1 ? 0 : BLOCK_GAP_EMS * style.fontSize;
94
+ return height + linesOf(block.glyphs, style.fontSize, (width - indent) * WIDTH_SAFETY) * style.fontSize * style.lineHeight + gap;
95
+ }, 0);
96
+ };
97
+ //#endregion
98
+ export { textHeight };
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@pollium/vyva-convert",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Server-side tools for Vyva content: clean what clients send, import Word, PowerPoint, PDF and Markdown files, and export to PDF, Word, PowerPoint and HTML.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Pollium/Vyva.git",
10
+ "directory": "packages/convert"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ "./*": {
17
+ "types": "./dist/*.d.ts",
18
+ "default": "./dist/*.js"
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@fontsource-variable/inter": "^5.3.0",
26
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
27
+ "@fontsource-variable/lora": "^5.3.0",
28
+ "@fontsource-variable/merriweather": "^5.3.0",
29
+ "@fontsource-variable/montserrat": "^5.3.0",
30
+ "@fontsource-variable/open-sans": "^5.3.0",
31
+ "@fontsource-variable/playfair-display": "^5.3.0",
32
+ "@fontsource-variable/roboto": "^5.3.0",
33
+ "docx": "^9.9.0",
34
+ "fflate": "^0.8.3",
35
+ "htmlparser2": "^10.1.0",
36
+ "mammoth": "^1.13.0",
37
+ "pptxgenjs": "^4.0.1",
38
+ "sanitize-html": "^2.18.0",
39
+ "unpdf": "^1.8.1",
40
+ "@pollium/vyva-core": "0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.6.4",
44
+ "@types/sanitize-html": "^2.16.2"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "typecheck": "tsc -p tsconfig.json",
49
+ "lint": "oxlint --deny-warnings",
50
+ "test": "vitest run"
51
+ }
6
52
  }