Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * PDF Export module for Atmosphere Docs.
3 *
4 * Uses html2pdf.js to convert the editor content into a downloadable PDF.
5 * Always exports in light mode regardless of current theme.
6 */
7import type { PdfExportOptions } from './types.js';
8
9/**
10 * Build a self-contained HTML string suitable for PDF rendering.
11 * Extracted as a pure function for testability.
12 */
13export function buildPdfHtml(editorHtml: string, title: string): string {
14 return `<!DOCTYPE html>
15<html lang="en">
16<head>
17<meta charset="UTF-8">
18<style>
19 body {
20 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21 max-width: 100%;
22 margin: 0;
23 padding: 0 0.5in;
24 line-height: 1.6;
25 color: #1a1815;
26 background: #fff;
27 }
28 h1, h2, h3 { margin-top: 1.5em; margin-bottom: 0.5em; }
29 blockquote { border-left: 3px solid #ccc; margin-left: 0; padding-left: 1em; color: #555; }
30 pre { background: #f5f5f5; padding: 1em; border-radius: 4px; overflow-x: auto; }
31 code { background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
32 pre code { background: none; padding: 0; }
33 table { border-collapse: collapse; width: 100%; }
34 th, td { border: 1px solid #ddd; padding: 0.5em; text-align: left; }
35 th { background: #f5f5f5; }
36 hr { border: none; border-top: 1px solid #ddd; margin: 2em 0; }
37 img { max-width: 100%; }
38 ul[data-type="taskList"] { list-style: none; padding-left: 0; }
39 ul[data-type="taskList"] li { display: flex; align-items: flex-start; gap: 0.5em; }
40</style>
41</head>
42<body>
43${editorHtml}
44</body>
45</html>`;
46}
47
48/**
49 * Derive a safe filename from a document title.
50 */
51export function pdfFilename(title: string | null | undefined): string {
52 const clean = (title || '').trim() || 'Untitled Document';
53 return clean.replace(/[^a-zA-Z0-9_\- ]/g, '').replace(/\s+/g, '_');
54}
55
56/**
57 * Generate and download a PDF from editor content.
58 * This is the DOM-coupled entry point — not unit-testable.
59 */
60export async function exportPdf({ editorHtml, title }: PdfExportOptions): Promise<void> {
61 const html2pdf = (await import('html2pdf.js')).default;
62
63 const container = document.createElement('div');
64 container.innerHTML = buildPdfHtml(editorHtml, title);
65 container.style.position = 'fixed';
66 container.style.left = '-9999px';
67 container.style.top = '0';
68 container.style.width = '8.5in';
69 container.style.background = '#fff';
70 container.style.color = '#1a1815';
71 document.body.appendChild(container);
72
73 try {
74 await html2pdf()
75 .set({
76 margin: [0.5, 0.5, 0.5, 0.5],
77 filename: `${pdfFilename(title)}.pdf`,
78 image: { type: 'jpeg', quality: 0.95 },
79 html2canvas: {
80 scale: 2,
81 useCORS: true,
82 backgroundColor: '#ffffff',
83 },
84 jsPDF: {
85 unit: 'in',
86 format: 'letter',
87 orientation: 'portrait',
88 },
89 })
90 .from(container)
91 .save();
92 } finally {
93 document.body.removeChild(container);
94 }
95}