@@ -7,6 +7,8 @@ import mysql from 'mysql2/promise'
77import pg from 'pg'
88import { spawn } from 'child_process'
99import { autoUpdater } from 'electron-updater'
10+ import MarkdownIt from 'markdown-it'
11+ import puppeteer from 'puppeteer'
1012
1113let mainWindow : BrowserWindow | null = null
1214
@@ -32,6 +34,11 @@ function createWindow(): void {
3234 mainWindow ?. show ( )
3335 } )
3436
37+ mainWindow . webContents . on ( 'did-fail-load' , ( _event , errorCode , errorDescription , validatedURL ) => {
38+ console . error ( `[Main Window] Failed to load URL: ${ validatedURL } ` )
39+ console . error ( `Error Code: ${ errorCode } (${ errorDescription } )` )
40+ } )
41+
3542 mainWindow . webContents . setWindowOpenHandler ( ( details ) => {
3643 shell . openExternal ( details . url )
3744 return { action : 'deny' }
@@ -138,6 +145,275 @@ ipcMain.handle("export-pdf", async (_, html, fileName) => {
138145 win . close ( )
139146} )
140147
148+ // Helper for PDF generation
149+ async function generatePdfBuffer ( markdownContent : string ) {
150+ const md = new MarkdownIt ( {
151+ html : true ,
152+ linkify : true ,
153+ typographer : true
154+ } )
155+
156+ const htmlContent = md . render ( markdownContent )
157+ const fullHtml = `
158+ <!DOCTYPE html>
159+ <html>
160+ <head>
161+ <meta charset="UTF-8">
162+ <style>
163+ @page {
164+ size: A4;
165+ margin: 20mm;
166+ }
167+ body {
168+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
169+ line-height: 1.5;
170+ color: #111827;
171+ background-color: #ffffff;
172+ margin: 0;
173+ padding: 0;
174+ }
175+ .container {
176+ width: 100%;
177+ }
178+ h1 {
179+ font-size: 2.5rem;
180+ font-weight: 800;
181+ color: #111827;
182+ margin-top: 0;
183+ margin-bottom: 24px;
184+ border-bottom: 2px solid #E5E7EB;
185+ padding-bottom: 12px;
186+ }
187+ h2 {
188+ font-size: 1.875rem;
189+ font-weight: 700;
190+ color: #1F2937;
191+ margin-top: 48px;
192+ margin-bottom: 16px;
193+ background: #F3F4F6;
194+ padding: 12px 16px;
195+ border-radius: 8px;
196+ }
197+ h3 {
198+ font-size: 1.5rem;
199+ font-weight: 600;
200+ color: #374151;
201+ margin-top: 32px;
202+ margin-bottom: 12px;
203+ border-left: 4px solid #3B82F6;
204+ padding-left: 16px;
205+ }
206+ h4 {
207+ font-size: 1.125rem;
208+ font-weight: 600;
209+ color: #4B5563;
210+ margin-top: 24px;
211+ margin-bottom: 8px;
212+ text-transform: uppercase;
213+ letter-spacing: 0.025em;
214+ }
215+ h5 {
216+ font-size: 1rem;
217+ font-weight: 600;
218+ color: #6B7280;
219+ margin-top: 16px;
220+ margin-bottom: 8px;
221+ }
222+ p {
223+ margin: 12px 0;
224+ }
225+ .method {
226+ display: inline-block;
227+ padding: 2px 8px;
228+ border-radius: 4px;
229+ font-family: monospace;
230+ font-weight: 700;
231+ font-size: 0.875rem;
232+ margin-right: 8px;
233+ color: white;
234+ }
235+ .method-GET { background: #10B981; }
236+ .method-POST { background: #3B82F6; }
237+ .method-PUT { background: #F59E0B; }
238+ .method-DELETE { background: #EF4444; }
239+ .method-PATCH { background: #8B5CF6; }
240+
241+ /* Table of Contents Styling */
242+ .toc-link {
243+ color: #2563EB;
244+ text-decoration: none;
245+ }
246+ .toc-list {
247+ list-style: none;
248+ padding-left: 0;
249+ }
250+ .toc-item {
251+ margin-bottom: 8px;
252+ }
253+
254+ /* Page Break support */
255+ .page-break {
256+ page-break-after: always;
257+ }
258+
259+ /* Custom Header IDs for TOC anchoring */
260+ h2[id], h3[id] {
261+ scroll-margin-top: 20px;
262+ }
263+
264+ .status-code {
265+ display: inline-block;
266+ padding: 2px 6px;
267+ border-radius: 4px;
268+ font-weight: 700;
269+ font-size: 0.8125rem;
270+ margin-left: 8px;
271+ }
272+ .status-2xx { background: #DCFCE7; color: #166534; }
273+ .status-3xx { background: #FEF3C7; color: #92400E; }
274+ .status-4xx { background: #FEE2E2; color: #991B1B; }
275+ .status-5xx { background: #FEF2F2; color: #991B1B; }
276+
277+ .endpoint-header {
278+ display: flex;
279+ align-items: center;
280+ gap: 12px;
281+ margin-bottom: 24px;
282+ }
283+
284+ pre {
285+ background-color: #0F172A;
286+ color: #CBD5E1;
287+ padding: 18px;
288+ border-radius: 12px;
289+ font-family: 'ui-monospace', SFMono-Regular, Menlo, Monaco, Consolas, monospace;
290+ font-size: 0.875rem;
291+ line-height: 1.6;
292+ overflow-x: auto;
293+ margin: 16px 0;
294+ border: 1px solid #1E293B;
295+ box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05);
296+ }
297+ code {
298+ font-family: 'ui-monospace', SFMono-Regular, Menlo, Monaco, Consolas, monospace;
299+ background-color: #F1F5F9;
300+ color: #334155;
301+ padding: 0.2rem 0.4rem;
302+ border-radius: 4px;
303+ font-size: 0.8125rem;
304+ }
305+ pre code {
306+ background-color: transparent;
307+ color: inherit;
308+ padding: 0;
309+ }
310+ table {
311+ width: 100%;
312+ border-collapse: collapse;
313+ margin: 24px 0;
314+ font-size: 0.875rem;
315+ }
316+ table th, table td {
317+ padding: 12px 16px;
318+ border: 1px solid #E2E8F0;
319+ text-align: left;
320+ vertical-align: top;
321+ }
322+ table th {
323+ background-color: #F8FAFC;
324+ font-weight: 700;
325+ color: #475569;
326+ text-transform: uppercase;
327+ font-size: 0.75rem;
328+ letter-spacing: 0.05em;
329+ }
330+ blockquote {
331+ padding: 12px 24px;
332+ color: #64748B;
333+ border-left: 6px solid #E2E8F0;
334+ background: #F8FAFC;
335+ margin: 24px 0;
336+ font-style: italic;
337+ border-radius: 0 8px 8px 0;
338+ }
339+ .hr {
340+ border: none;
341+ border-top: 2px solid #F1F5F9;
342+ margin: 60px 0;
343+ }
344+ img {
345+ max-width: 100%;
346+ border-radius: 8px;
347+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
348+ }
349+ </style>
350+ </head>
351+ <body>
352+ <div class="container">
353+ ${ htmlContent }
354+ </div>
355+ </body>
356+ </html>
357+ `
358+
359+ const browser = await puppeteer . launch ( {
360+ headless : true ,
361+ args : [ '--no-sandbox' , '--disable-setuid-sandbox' ]
362+ } )
363+ const page = await browser . newPage ( )
364+ await page . setContent ( fullHtml , { waitUntil : 'networkidle0' } )
365+
366+ const buffer = await page . pdf ( {
367+ format : 'A4' ,
368+ margin : {
369+ top : '25mm' ,
370+ right : '25mm' ,
371+ bottom : '25mm' ,
372+ left : '25mm'
373+ } ,
374+ displayHeaderFooter : true ,
375+ headerTemplate : '<span></span>' ,
376+ footerTemplate : `
377+ <div style="font-size: 10px; color: #aaa; width: 100%; text-align: center; font-family: 'Segoe UI', Arial, sans-serif;">
378+ Page <span class="pageNumber"></span> of <span class="totalPages"></span>
379+ </div>` ,
380+ printBackground : true
381+ } )
382+
383+ await browser . close ( )
384+ return buffer
385+ }
386+
387+ ipcMain . handle ( 'preview-doc-pdf' , async ( _event , markdownContent : string ) => {
388+ try {
389+ const buffer = await generatePdfBuffer ( markdownContent )
390+ return { success : true , data : buffer }
391+ } catch ( error : any ) {
392+ console . error ( 'Error during PDF preview generation:' , error )
393+ return { success : false , error : error . message }
394+ }
395+ } )
396+
397+ ipcMain . handle ( 'generate-doc-pdf' , async ( _event , markdownContent : string , fileName : string ) => {
398+ try {
399+ const { filePath } = await dialog . showSaveDialog ( {
400+ defaultPath : fileName ,
401+ filters : [ { name : 'PDF Files' , extensions : [ 'pdf' ] } ]
402+ } )
403+
404+ if ( ! filePath ) return { success : false , error : 'Cancelled' }
405+
406+ const buffer = await generatePdfBuffer ( markdownContent )
407+ fs . writeFileSync ( filePath , buffer )
408+
409+ return { success : true }
410+ } catch ( error : any ) {
411+
412+ console . error ( 'Error during PDF conversion:' , error )
413+ return { success : false , error : error . message }
414+ }
415+ } )
416+
141417// ─── HTTP Request Handler (Postman-like API testing) ─────────────
142418ipcMain . handle ( 'send-http-request' , async ( _event , opts : {
143419 url : string
0 commit comments