import { FileSystemItem } from '@/lib/types/database'
import { Editor } from '@tiptap/react'
export interface DownloadOptions {
format: 'current' | 'pdf'
fileName: string
content: string
fileExtension?: string
}
/**
* Downloads a file in its current format
*/
export function downloadCurrentFormat(file: FileSystemItem): void {
if (!file.content) {
console.error('No content to download')
return
}
const blob = new Blob([file.content], {
type: getContentType(file.file_extension || 'txt')
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = file.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
/**
* Downloads a file as PDF using TipTap editor instance
* This provides the best formatting since it uses the actual rendered HTML
*/
export async function downloadAsPDFFromEditor(
editor: Editor,
fileName: string
): Promise<void> {
try {
// Get the HTML content from TipTap editor
const htmlContent = editor.getHTML()
// Create a complete HTML document with proper styling
const fullHtml = createPrintableHTML(htmlContent, fileName)
// Use the browser's print API with PDF generation
await generatePDFFromHTML(fullHtml, fileName)
} catch (error) {
console.error('Error generating PDF from editor:', error)
throw new Error('Failed to generate PDF from editor')
}
}
/**
* Downloads a file as PDF (fallback method)
* This is used when TipTap editor instance is not available
*/
export async function downloadAsPDF(file: FileSystemItem): Promise<void> {
if (!file.content) {
console.error('No content to download')
return
}
try {
// Create a temporary editor instance to convert markdown to HTML
const { Editor } = await import('@tiptap/core')
const { default: StarterKit } = await import('@tiptap/starter-kit')
const editor = new Editor({
extensions: [StarterKit],
content: file.content,
})
const htmlContent = editor.getHTML()
const fileName = file.name.replace(/\.[^/.]+$/, '')
// Create a complete HTML document
const fullHtml = createPrintableHTML(htmlContent, fileName)
// Generate PDF
await generatePDFFromHTML(fullHtml, fileName + '.pdf')
} catch (error) {
console.error('Error generating PDF:', error)
// Fallback to plain text download
downloadCurrentFormat(file)
}
}
/**
* Create a complete HTML document optimized for PDF generation
*/
function createPrintableHTML(content: string, title: string): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(title)}</title>
<style>
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 2cm;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
font-size: 12pt;
line-height: 1.6;
color: #333;
background: white;
max-width: none;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
page-break-after: avoid;
}
h1 {
font-size: 24pt;
border-bottom: 2px solid #333;
padding-bottom: 0.3em;
margin-bottom: 1em;
}
h2 {
font-size: 20pt;
border-bottom: 1px solid #ddd;
padding-bottom: 0.2em;
}
h3 { font-size: 16pt; }
h4 { font-size: 14pt; }
h5 { font-size: 12pt; }
h6 { font-size: 11pt; }
p {
margin-bottom: 1em;
orphans: 3;
widows: 3;
}
/* Lists */
ul, ol {
margin: 0.5em 0 1em 1.5em;
}
li {
margin-bottom: 0.3em;
page-break-inside: avoid;
}
/* Task lists */
li[data-type="taskItem"] {
list-style: none;
margin-left: -1.5em;
}
li[data-type="taskItem"]:before {
content: "☐ ";
margin-right: 0.5em;
}
li[data-type="taskItem"][data-checked="true"]:before {
content: "☑ ";
}
/* Code */
code {
background: #f5f5f5;
padding: 0.1em 0.3em;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9em;
}
pre {
background: #f8f8f8;
border: 1px solid #ddd;
border-radius: 4px;
padding: 1em;
margin: 1em 0;
overflow-x: auto;
page-break-inside: avoid;
}
pre code {
background: none;
padding: 0;
border-radius: 0;
}
/* Blockquotes */
blockquote {
border-left: 4px solid #ddd;
padding-left: 1em;
margin: 1em 0;
color: #666;
font-style: italic;
page-break-inside: avoid;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
page-break-inside: avoid;
}
th, td {
border: 1px solid #ddd;
padding: 0.5em;
text-align: left;
}
th {
background: #f5f5f5;
font-weight: 600;
}
tbody tr:nth-child(even) {
background: #f9f9f9;
}
/* Links */
a {
color: #0066cc;
text-decoration: none;
}
a:after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #666;
}
/* Images */
img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
page-break-inside: avoid;
}
/* Horizontal rules */
hr {
border: none;
border-top: 2px solid #ddd;
margin: 2em 0;
page-break-after: avoid;
}
/* Page breaks */
.page-break {
page-break-before: always;
break-before: page;
}
/* Utility classes */
.no-break {
page-break-inside: avoid;
}
/* Print-specific */
@media print {
body {
font-size: 11pt;
}
a:after {
font-size: 9pt;
}
.no-print {
display: none !important;
}
/* Ensure proper page margins in print */
@page :first {
margin-top: 3cm;
}
}
</style>
</head>
<body>
<div class="document-content">
${content}
</div>
</body>
</html>`
}
/**
* Generate PDF from HTML using the browser's print API
*/
async function generatePDFFromHTML(html: string, fileName: string): Promise<void> {
// Create a new window for printing
const printWindow = window.open('', '_blank', 'width=800,height=600')
if (!printWindow) {
throw new Error('Could not open print window. Please allow popups for this site.')
}
// Write HTML to the new window
printWindow.document.write(html)
printWindow.document.close()
// Wait for the content to load
await new Promise<void>((resolve) => {
printWindow.onload = () => {
// Small delay to ensure everything is rendered
setTimeout(() => {
resolve()
}, 100)
}
})
// Set the document title for the PDF
printWindow.document.title = fileName
// Trigger print dialog
printWindow.print()
// Clean up after a delay
setTimeout(() => {
printWindow.close()
}, 1000)
}
/**
* Alternative PDF generation using Puppeteer (for server-side)
* This would be called via an API endpoint
*/
export async function generatePDFServerSide(
content: string,
fileName: string
): Promise<Blob> {
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: createPrintableHTML(content, fileName),
fileName
})
})
if (!response.ok) {
throw new Error('Failed to generate PDF on server')
}
return response.blob()
}
/**
* Escape HTML special characters
*/
function escapeHtml(text: string): string {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
/**
* Get the appropriate MIME type for a file extension
*/
function getContentType(extension: string): string {
const ext = extension.toLowerCase()
const mimeTypes: Record<string, string> = {
'txt': 'text/plain',
'md': 'text/markdown',
'markdown': 'text/markdown',
'html': 'text/html',
'htm': 'text/html',
'json': 'application/json',
'js': 'text/javascript',
'ts': 'text/typescript',
'css': 'text/css',
'xml': 'text/xml',
'csv': 'text/csv'
}
return mimeTypes[ext] || 'text/plain'
}
/**
* Get a user-friendly format name for display
*/
export function getFormatDisplayName(extension?: string): string {
if (!extension) return 'Text File'
const ext = extension.toLowerCase()
const displayNames: Record<string, string> = {
'txt': 'Text File',
'md': 'Markdown',
'markdown': 'Markdown',
'html': 'HTML',
'htm': 'HTML',
'json': 'JSON',
'js': 'JavaScript',
'ts': 'TypeScript',
'css': 'CSS',
'xml': 'XML',
'csv': 'CSV'
}
return displayNames[ext] || ext.toUpperCase()
}
/**
* Get a description of supported PDF features
*/
export function getPDFFeatures(): string[] {
return [
'✓ Perfect formatting using TipTap\'s rendered HTML',
'✓ Proper page breaks and print styling',
'✓ All markdown elements: headings, lists, tables, quotes',
'✓ Syntax-highlighted code blocks',
'✓ Clickable links with URLs shown in print',
'✓ Task lists with checkboxes',
'✓ Professional typography and spacing',
'✓ Responsive images and media',
'✓ Print-optimized styling',
'✓ Browser-native PDF generation (no external dependencies)'
]
}