- tom-generator/export/zip.ts: extract private helpers to zip-helpers.ts (544→342 LOC) - tom-generator/export/docx.ts: extract private helpers to docx-helpers.ts (525→378 LOC) - tom-generator/export/pdf.ts: extract private helpers to pdf-helpers.ts (517→446 LOC) - tom-generator/demo-data/index.ts: extract DEMO_RISK_PROFILES + DEMO_EVIDENCE_DOCUMENTS to demo-data-part2.ts (518→360 LOC) - einwilligungen/generator/privacy-policy-sections.ts: extract sections 5-7 to part2 (559→313 LOC) - einwilligungen/export/pdf.ts: extract HTML/CSS helpers to pdf-helpers.ts (505→296 LOC) - vendor-compliance/context.tsx: extract API action hooks to context-actions.tsx (509→286 LOC) All originals re-export from sibling files — zero consumer import changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
219 lines
5.1 KiB
TypeScript
219 lines
5.1 KiB
TypeScript
// =============================================================================
|
|
// Privacy Policy PDF Export - Helper Functions
|
|
// Private helpers extracted from pdf.ts to stay under 500 LOC hard cap
|
|
// =============================================================================
|
|
|
|
import type { PDFExportOptions, PDFSection } from './pdf'
|
|
|
|
// =============================================================================
|
|
// HELPER FUNCTIONS
|
|
// =============================================================================
|
|
|
|
export function generateHTMLFromContent(
|
|
content: PDFSection[],
|
|
options: PDFExportOptions
|
|
): string {
|
|
const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in'
|
|
const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in'
|
|
|
|
let html = `
|
|
<!DOCTYPE html>
|
|
<html lang="${options.language}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
|
|
<style>
|
|
@page {
|
|
size: ${pageWidth} ${pageHeight};
|
|
margin: 20mm;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Calibri, Arial, sans-serif;
|
|
font-size: ${options.fontSize}pt;
|
|
line-height: 1.6;
|
|
color: #1e293b;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 24pt;
|
|
color: ${options.primaryColor};
|
|
border-bottom: 2px solid ${options.primaryColor};
|
|
padding-bottom: 10px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 16pt;
|
|
color: ${options.primaryColor};
|
|
margin-top: 24px;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 13pt;
|
|
color: #334155;
|
|
margin-top: 18px;
|
|
}
|
|
|
|
p {
|
|
margin: 12px 0;
|
|
text-align: justify;
|
|
}
|
|
|
|
.title {
|
|
font-size: 28pt;
|
|
text-align: center;
|
|
color: ${options.primaryColor};
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
font-style: italic;
|
|
color: #64748b;
|
|
}
|
|
|
|
.center {
|
|
text-align: center;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 16px 0;
|
|
font-size: 10pt;
|
|
}
|
|
|
|
th, td {
|
|
border: 1px solid #e2e8f0;
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
}
|
|
|
|
th {
|
|
background-color: ${options.primaryColor};
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
ul {
|
|
margin: 12px 0;
|
|
padding-left: 24px;
|
|
}
|
|
|
|
li {
|
|
margin: 6px 0;
|
|
}
|
|
|
|
.pagebreak {
|
|
page-break-after: always;
|
|
}
|
|
|
|
.footer {
|
|
margin-top: 40px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #e2e8f0;
|
|
font-size: 9pt;
|
|
color: #94a3b8;
|
|
text-align: center;
|
|
}
|
|
|
|
@media print {
|
|
body {
|
|
padding: 0;
|
|
}
|
|
.pagebreak {
|
|
page-break-after: always;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
`
|
|
|
|
for (const section of content) {
|
|
switch (section.type) {
|
|
case 'title':
|
|
html += `<div class="title" style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</div>\n`
|
|
break
|
|
|
|
case 'heading':
|
|
html += `<h1 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h1>\n`
|
|
break
|
|
|
|
case 'subheading':
|
|
html += `<h3 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h3>\n`
|
|
break
|
|
|
|
case 'paragraph': {
|
|
const alignClass = section.style?.align === 'center' ? ' class="center"' : ''
|
|
html += `<p${alignClass} style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</p>\n`
|
|
break
|
|
}
|
|
|
|
case 'list':
|
|
html += '<ul>\n'
|
|
for (const item of section.items || []) {
|
|
html += ` <li>${escapeHtml(item)}</li>\n`
|
|
}
|
|
html += '</ul>\n'
|
|
break
|
|
|
|
case 'table':
|
|
if (section.table) {
|
|
html += '<table>\n<thead><tr>\n'
|
|
for (const header of section.table.headers) {
|
|
html += ` <th>${escapeHtml(header)}</th>\n`
|
|
}
|
|
html += '</tr></thead>\n<tbody>\n'
|
|
for (const row of section.table.rows) {
|
|
html += '<tr>\n'
|
|
for (const cell of row) {
|
|
html += ` <td>${escapeHtml(cell)}</td>\n`
|
|
}
|
|
html += '</tr>\n'
|
|
}
|
|
html += '</tbody></table>\n'
|
|
}
|
|
break
|
|
|
|
case 'pagebreak':
|
|
html += '<div class="pagebreak"></div>\n'
|
|
break
|
|
}
|
|
}
|
|
|
|
html += '</body></html>'
|
|
return html
|
|
}
|
|
|
|
export function getStyleString(style?: PDFSection['style']): string {
|
|
if (!style) return ''
|
|
|
|
const parts: string[] = []
|
|
if (style.color) parts.push(`color: ${style.color}`)
|
|
if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`)
|
|
if (style.bold) parts.push('font-weight: bold')
|
|
if (style.italic) parts.push('font-style: italic')
|
|
if (style.align) parts.push(`text-align: ${style.align}`)
|
|
|
|
return parts.join('; ')
|
|
}
|
|
|
|
export function escapeHtml(text: string): string {
|
|
return text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|