mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
Adds File > Export as PDF... menu item (Cmd+Shift+E) and an overflow menu entry that renders the active markdown preview through a sandboxed Electron BrowserWindow and writes it to disk via printToPDF. - New main-side IPC handler (src/main/ipc/export.ts) and html-to-pdf helper that loads a CSP-locked HTML document in a sandboxed, context- isolated window with javascript enabled only for image-ready polling. - Renderer helpers clone the rendered markdown subtree, inline all computed styles through a curated allowlist, and ship the resulting HTML fragment over IPC. - Ref-counted listener registration so split-pane layouts install exactly one IPC subscription and survive panel churn.
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
|
import { writeFile } from 'node:fs/promises'
|
|
import { ExportTimeoutError, htmlToPdf } from '../lib/html-to-pdf'
|
|
|
|
export type ExportHtmlToPdfArgs = {
|
|
html: string
|
|
title: string
|
|
}
|
|
|
|
export type ExportHtmlToPdfResult =
|
|
| { success: true; filePath: string }
|
|
| { success: false; cancelled?: boolean; error?: string }
|
|
|
|
export function registerExportHandlers(): void {
|
|
ipcMain.removeHandler('export:html-to-pdf')
|
|
ipcMain.handle(
|
|
'export:html-to-pdf',
|
|
async (event, args: ExportHtmlToPdfArgs): Promise<ExportHtmlToPdfResult> => {
|
|
const { html, title } = args
|
|
if (!html.trim()) {
|
|
return { success: false, error: 'No content to export' }
|
|
}
|
|
|
|
try {
|
|
const pdfBuffer = await htmlToPdf(html)
|
|
|
|
// Why: sanitize to keep the suggested filename legal on every platform.
|
|
// Windows forbids /\:*?"<>| in filenames; truncate to keep the OS save
|
|
// dialog stable when titles are pathologically long.
|
|
const sanitizedTitle = title.replace(/[/\\:*?"<>|]/g, '_').slice(0, 100) || 'export'
|
|
const defaultFilename = `${sanitizedTitle}.pdf`
|
|
|
|
const parent = BrowserWindow.fromWebContents(event.sender) ?? undefined
|
|
const dialogOptions = {
|
|
defaultPath: defaultFilename,
|
|
filters: [{ name: 'PDF', extensions: ['pdf'] }]
|
|
}
|
|
const { canceled, filePath } = parent
|
|
? await dialog.showSaveDialog(parent, dialogOptions)
|
|
: await dialog.showSaveDialog(dialogOptions)
|
|
|
|
if (canceled || !filePath) {
|
|
return { success: false, cancelled: true }
|
|
}
|
|
|
|
await writeFile(filePath, pdfBuffer)
|
|
return { success: true, filePath }
|
|
} catch (error) {
|
|
if (error instanceof ExportTimeoutError) {
|
|
return { success: false, error: 'Export timed out' }
|
|
}
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to export PDF'
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|