export interface PrintExportPayload { fileName: string; title: string; html: string; previewFont: string; previewFontSize: number; previewFontFaceCss?: string; } export const PRINT_EXPORT_QUERY_PARAM = 'printJob'; const PRINT_DOCUMENT_CSP = [ "default-src 'none'", "style-src 'unsafe-inline'", "img-src data: blob: https: http:", "font-src data:", "object-src 'none'", "base-uri 'none'", ].join('; '); export const getNoteTitleFromContent = (content: string) => { const firstLine = content .split('\n') .map((line) => line.trim()) .find((line) => line.length > 0); return (firstLine || 'Untitled').replace(/^#+\s*/, '').trim(); }; export const sanitizeFileName = (name: string) => name .replace(/[\\/:*?"<>|]/g, '-') .replace(/\s+/g, ' ') .trim() || 'note'; const escapeHtml = (value: string) => value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const escapeFontFamily = (value: string) => value.replace(/["\\]/g, '\\$&'); interface PrintFontAsset { fileName: string; fontStyle: 'normal' | 'italic'; fontWeight: string; } const PRINT_FONT_ASSETS: Record = { Merriweather: [ { fileName: 'Merriweather-VariableFont_opsz,wdth,wght.ttf', fontStyle: 'normal', fontWeight: '300 900', }, { fileName: 'Merriweather-Italic-VariableFont_opsz,wdth,wght.ttf', fontStyle: 'italic', fontWeight: '300 900', }, ], 'Crimson Pro': [ { fileName: 'CrimsonPro-VariableFont_wght.ttf', fontStyle: 'normal', fontWeight: '200 900', }, { fileName: 'CrimsonPro-Italic-VariableFont_wght.ttf', fontStyle: 'italic', fontWeight: '200 900', }, ], 'Roboto Serif': [ { fileName: 'RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf', fontStyle: 'normal', fontWeight: '100 900', }, { fileName: 'RobotoSerif-Italic-VariableFont_GRAD,opsz,wdth,wght.ttf', fontStyle: 'italic', fontWeight: '100 900', }, ], Average: [ { fileName: 'Average-Regular.ttf', fontStyle: 'normal', fontWeight: '400', }, ], }; const fontDataUrlCache = new Map>(); const blobToDataUrl = (blob: Blob) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.onerror = () => reject(reader.error ?? new Error('Failed to read font file.')); reader.readAsDataURL(blob); }); const getBundledFontUrl = (fileName: string) => new URL(`./fonts/${fileName}`, window.location.href).toString(); const loadBundledFontDataUrl = async (fileName: string) => { const cached = fontDataUrlCache.get(fileName); if (cached) { return cached; } const pending = (async () => { const response = await fetch(getBundledFontUrl(fileName)); if (!response.ok) { throw new Error(`Failed to load bundled font ${fileName}: ${response.status}`); } return blobToDataUrl(await response.blob()); })(); fontDataUrlCache.set(fileName, pending); return pending; }; export const loadPrintFontFaceCss = async (fontFamily: string) => { const fontAssets = PRINT_FONT_ASSETS[fontFamily]; if (!fontAssets) { return ''; } const rules = await Promise.all( fontAssets.map(async ({ fileName, fontStyle, fontWeight }) => { try { const dataUrl = await loadBundledFontDataUrl(fileName); return `@font-face { font-family: "${escapeFontFamily(fontFamily)}"; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: swap; src: url("${dataUrl}") format("truetype"); }`; } catch (error) { console.error(`Failed to embed preview font "${fontFamily}" from ${fileName}:`, error); return ''; } }) ); return rules.filter(Boolean).join('\n'); }; export const buildPrintDocument = (payload: PrintExportPayload) => { const fontFamily = `"${escapeFontFamily(payload.previewFont)}", Georgia, serif`; return ` ${escapeHtml(payload.fileName)} `; };