11import * as helper from './helper'
2-
32import * as COLOR from '../colorize'
4- const path = require ( 'path' )
5- import puppeteer from 'puppeteer'
3+ import * as path from 'path'
4+ import puppeteer , { Browser , Page } from 'puppeteer'
5+
6+ // Default PDF generation settings
7+ const DEFAULT_TIMEOUT_MS = 60000 // 60 seconds
8+ const DEFAULT_MARGIN_TOP = 80
9+ const DEFAULT_MARGIN_BOTTOM = 80
10+ const DEFAULT_MARGIN_LEFT = 30
11+ const DEFAULT_MARGIN_RIGHT = 30
12+ const DEFAULT_SCALE = 1
13+ const DEFAULT_FORMAT = 'a4'
14+ const DEFAULT_PRINT_BACKGROUND = true
15+ const DEFAULT_DISPLAY_HEADER_FOOTER = false
16+ const DEFAULT_LANDSCAPE = false
17+ const DEFAULT_OMIT_BACKGROUND = false
618
19+ /**
20+ * Displays help information about PDF export options and settings.
21+ * Shows both LiaScript-specific settings and Puppeteer PDF options.
22+ */
723export function help ( ) {
824 console . log ( '' )
925 console . log ( COLOR . heading ( 'PDF settings:' ) , '\n' )
@@ -27,7 +43,7 @@ export function help() {
2743 COLOR . command (
2844 null ,
2945 '--pdf-timeout' ,
30- ' Set an additional time horizon to wait until finished.'
46+ ` Set an additional time horizon to wait until finished (default ${ DEFAULT_TIMEOUT_MS } ms)`
3147 )
3248 COLOR . command (
3349 null ,
@@ -125,18 +141,17 @@ export function help() {
125141 )
126142}
127143
128- export async function exporter ( argument : {
144+ /**
145+ * Configuration options for PDF export.
146+ */
147+ interface PdfExportArguments {
129148 input : string
130- readme : string
131149 output : string
132- format : string
133- path : string
134- key ?: string
135150
136- // special cases for PDF
151+ // PDF-specific settings
137152 'pdf-preview' ?: boolean
138153 'pdf-scale' ?: number
139- 'pdf-displayHeaderFooter' ?: string
154+ 'pdf-displayHeaderFooter' ?: boolean
140155 'pdf-headerTemplate' ?: string
141156 'pdf-footerTemplate' ?: string
142157 'pdf-printBackground' ?: boolean
@@ -151,10 +166,32 @@ export async function exporter(argument: {
151166 'pdf-preferCSSPageSize' ?: boolean
152167 'pdf-omitBackground' ?: boolean
153168 'pdf-timeout' ?: number
169+ 'pdf-pageRanges' ?: string
154170
155171 'pdf-stylesheet' ?: string
156172 'pdf-theme' ?: string
157- } ) {
173+ }
174+
175+ /**
176+ * Exports a LiaScript course to PDF format using Puppeteer.
177+ *
178+ * This function launches a headless Chrome browser, loads the LiaScript content,
179+ * applies any custom styling or themes, and generates a PDF file.
180+ *
181+ * @param argument - Configuration options for the PDF export
182+ * @throws {Error } If browser launch fails, page navigation fails, or PDF generation fails
183+ *
184+ * @example
185+ * ```typescript
186+ * await exporter({
187+ * input: './course.md',
188+ * output: './output',
189+ * 'pdf-format': 'a4',
190+ * 'pdf-printBackground': true
191+ * })
192+ * ```
193+ */
194+ export async function exporter ( argument : PdfExportArguments ) {
158195 const dirname = helper . dirname ( )
159196
160197 let url = `file://${ dirname } /assets/pdf/index.html?`
@@ -165,132 +202,179 @@ export async function exporter(argument: {
165202 url += 'file://' + path . resolve ( argument . input )
166203 }
167204
168- const browser = await puppeteer . launch ( {
169- pipe : true ,
170- args : [
171- '--no-sandbox' ,
172- '--disable-web-security' ,
173- '--disable-features=IsolateOrigins' ,
174- '--disable-site-isolation-trials' ,
175- '--unhandled-rejections=strict' ,
176- '--disable-features=BlockInsecurePrivateNetworkRequests' ,
177- '--allow-file-access-from-files' ,
178- '--enable-local-file-accesses' ,
179- '--enable-features=ExperimentalJavaScript' ,
180- ] ,
181- headless : argument [ 'pdf-preview' ] ? false : true ,
182- // Try to use the system Chrome if available
183- executablePath : process . env . PUPPETEER_EXECUTABLE_PATH || undefined ,
184- // Add this to use the installed browser if no executable path is provided
185- channel : process . env . PUPPETEER_EXECUTABLE_PATH ? undefined : 'chrome' ,
186- } )
187- const page = await browser . newPage ( )
205+ let browser : Browser | null = null
206+ let page : Page | null = null
188207
189- console . warn (
190- 'depending on the size of the course, this can take a while, please be patient...'
191- )
208+ try {
209+ // Configure browser launch options
210+ const launchOptions : any = {
211+ pipe : true ,
212+ args : [
213+ '--no-sandbox' ,
214+ '--disable-web-security' ,
215+ '--disable-features=IsolateOrigins' ,
216+ '--disable-site-isolation-trials' ,
217+ '--unhandled-rejections=strict' ,
218+ '--disable-features=BlockInsecurePrivateNetworkRequests' ,
219+ '--allow-file-access-from-files' ,
220+ '--enable-local-file-accesses' ,
221+ '--enable-features=ExperimentalJavaScript' ,
222+ ] ,
223+ headless : ! argument [ 'pdf-preview' ] ,
224+ }
192225
193- // this handle the alert - boxes, so that these are not blocking
194- page . on ( 'dialog' , async ( dialog ) => {
195- console . log ( dialog . type ( ) )
196- console . log ( dialog . message ( ) )
197- await dialog . accept ( )
198- } )
226+ // Use custom executable path if provided, otherwise use Chrome channel
227+ if ( process . env . PUPPETEER_EXECUTABLE_PATH ) {
228+ launchOptions . executablePath = process . env . PUPPETEER_EXECUTABLE_PATH
229+ } else {
230+ launchOptions . channel = 'chrome'
231+ }
199232
200- try {
233+ try {
234+ browser = await puppeteer . launch ( launchOptions )
235+ } catch ( launchError ) {
236+ throw new Error (
237+ `Failed to launch browser. Make sure Chrome is installed. ${ launchError } `
238+ )
239+ }
240+ page = await browser . newPage ( )
241+
242+ console . log (
243+ 'Loading course content... This may take a while for large courses.'
244+ )
245+
246+ // Handle alert dialogs automatically to prevent blocking
247+ page . on ( 'dialog' , async ( dialog ) => {
248+ console . log ( `[Dialog ${ dialog . type ( ) } ]: ${ dialog . message ( ) } ` )
249+ await dialog . accept ( )
250+ } )
251+
252+ // Wait for page to load completely
253+ // Using 'networkidle2' ensures all network requests are complete
254+ // Timeout set to 0 (unlimited) to handle large courses
201255 await page . goto ( url , {
202256 waitUntil : 'networkidle2' ,
203- // remove timeout
204- timeout : 0 ,
257+ timeout : DEFAULT_TIMEOUT_MS ,
205258 } )
206- } catch ( e ) {
207- console . warn ( 'pdf generation failed:' , e )
208- }
209259
210- if ( argument [ 'pdf-stylesheet' ] ) {
211- const href = path . resolve ( dirname + '/../' , argument [ 'pdf-stylesheet' ] )
260+ if ( argument [ 'pdf-stylesheet' ] ) {
261+ const href = path . resolve ( dirname + '/../' , argument [ 'pdf-stylesheet' ] )
212262
213- await page . evaluate ( async ( href ) => {
214- const link = document . createElement ( 'link' )
215- link . rel = 'stylesheet'
216- link . href = href
263+ try {
264+ await page . evaluate ( async ( href ) => {
265+ const link = document . createElement ( 'link' )
266+ link . rel = 'stylesheet'
267+ link . href = href
217268
218- const promise = new Promise ( ( resolve , reject ) => {
219- link . onload = resolve
220- link . onerror = reject
221- } )
222- document . head . appendChild ( link )
223- await promise
224- } , href )
225- }
269+ const promise = new Promise ( ( resolve , reject ) => {
270+ link . onload = resolve
271+ link . onerror = reject
272+ } )
273+ document . head . appendChild ( link )
274+ await promise
275+ } , href )
276+ } catch ( e ) {
277+ throw new Error (
278+ `Failed to load custom stylesheet from '${ argument [ 'pdf-stylesheet' ] } ': ${ e } `
279+ )
280+ }
281+ }
226282
227- if ( argument [ 'pdf-theme' ] ) {
228- await page . evaluate ( async ( theme ) => {
229- document . documentElement . classList . remove ( 'lia-theme-default' )
230- document . documentElement . classList . add ( 'lia-theme-' + theme )
231- } , argument [ 'pdf-theme' ] )
232- }
283+ if ( argument [ 'pdf-theme' ] ) {
284+ try {
285+ await page . evaluate ( async ( theme ) => {
286+ document . documentElement . classList . remove ( 'lia-theme-default' )
287+ document . documentElement . classList . add ( 'lia-theme-' + theme )
288+ } , argument [ 'pdf-theme' ] )
289+ } catch ( e ) {
290+ throw new Error (
291+ `Failed to apply theme '${ argument [ 'pdf-theme' ] } ': ${ e } `
292+ )
293+ }
294+ }
233295
234- /*
235- await page.evaluate(async () => {
236- const style = document.createElement('style')
237- style.type = 'text/css'
238- const content = `
239- :root {
240-
241- --color-highlight: 2,255,0;
242- --color-background: 122,122,122;
243- --color-border: 0,0,0;
244- --color-highlight-dark: 0,0,0;
245- --color-highlight-menu: 0,0,0;
246- --color-text: 0,0,255;
247- --global-font-size: 1rem;
248- --font-size-multiplier: 2;
296+ if ( ! argument [ 'pdf-preview' ] ) {
297+ await helper . sleep ( argument [ 'pdf-timeout' ] || DEFAULT_TIMEOUT_MS )
298+ await toPDF ( argument , page )
299+ } else {
300+ console . log ( 'Preview mode enabled - browser will remain open' )
249301 }
250- `
251- style.appendChild(document.createTextNode(content))
252- const promise = new Promise((resolve, reject) => {
253- style.onload = resolve
254- style.onerror = reject
255- })
256- document.head.appendChild(style)
257- await promise
258- })
302+ } catch ( e ) {
303+ const error = e as Error
304+ console . error ( 'PDF export failed:' , error . message )
305+ throw new Error ( `Failed to export PDF: ${ error . message } ` )
306+ } finally {
307+ // Clean up resources based on mode
308+ if ( argument [ 'pdf-preview' ] ) {
309+ // In preview mode, keep browser open but inform user
310+ console . log ( 'Browser kept open for preview. Close manually when done.' )
311+ } else {
312+ // In normal mode, always close browser and page
313+ if ( page ) {
314+ try {
315+ await page . close ( )
316+ } catch ( closeError ) {
317+ console . error ( 'Failed to close page:' , closeError )
318+ }
319+ }
259320
260- console.warn(argument)
261- */
262- if ( ! argument [ 'pdf-preview' ] ) {
263- await helper . sleep ( argument [ 'pdf-timeout' ] || 60000 )
264- await toPDF ( argument , browser , page )
321+ if ( browser ) {
322+ try {
323+ await browser . close ( )
324+ } catch ( closeError ) {
325+ console . error ( 'Failed to close browser:' , closeError )
326+ }
327+ }
328+ }
265329 }
266330}
267331
268- async function toPDF ( argument : any , browser : any , page : any ) {
332+ /**
333+ * Generates a PDF file from a Puppeteer page.
334+ *
335+ * Emulates screen media type for proper rendering and applies all PDF-specific
336+ * settings like margins, format, scaling, etc.
337+ *
338+ * @param argument - PDF export configuration options
339+ * @param page - Puppeteer page instance containing the rendered content
340+ * @throws {Error } If PDF generation fails or media type emulation fails
341+ */
342+ async function toPDF ( argument : PdfExportArguments , page : Page ) {
269343 try {
270344 await page . emulateMediaType ( 'screen' )
345+ } catch ( e ) {
346+ throw new Error ( `Failed to emulate media type: ${ e } ` )
347+ }
348+
349+ try {
271350 await page . pdf ( {
272351 path : argument . output + '.pdf' ,
273- format : argument [ 'pdf-format' ] || 'a4' ,
274- printBackground : argument [ 'pdf-printBackground' ] || true ,
275- displayHeaderFooter : argument [ 'pdf-displayHeaderFooter' ] || false ,
352+ format : ( argument [ 'pdf-format' ] as any ) || DEFAULT_FORMAT ,
353+ printBackground :
354+ argument [ 'pdf-printBackground' ] ?? DEFAULT_PRINT_BACKGROUND ,
355+ displayHeaderFooter :
356+ argument [ 'pdf-displayHeaderFooter' ] ?? DEFAULT_DISPLAY_HEADER_FOOTER ,
276357 margin : {
277- top : argument [ 'pdf-margin-top' ] || 80 ,
278- bottom : argument [ 'pdf-margin-bottom' ] || 80 ,
279- left : argument [ 'pdf-margin-left' ] || 30 ,
280- right : argument [ 'pdf-margin-right' ] || 30 ,
358+ top : argument [ 'pdf-margin-top' ] || DEFAULT_MARGIN_TOP ,
359+ bottom : argument [ 'pdf-margin-bottom' ] || DEFAULT_MARGIN_BOTTOM ,
360+ left : argument [ 'pdf-margin-left' ] || DEFAULT_MARGIN_LEFT ,
361+ right : argument [ 'pdf-margin-right' ] || DEFAULT_MARGIN_RIGHT ,
281362 } ,
282- scale : argument [ 'pdf-scale' ] || 1 ,
363+ scale : argument [ 'pdf-scale' ] ?? DEFAULT_SCALE ,
283364 headerTemplate : argument [ 'pdf-headerTemplate' ] ,
284- footerTemplate : argument [ 'pdf-footerTemplate' ] || '' ,
285- landscape : argument [ 'pdf-landscape' ] || false ,
286- width : argument [ 'pdf-width' ] || '' ,
287- height : argument [ 'pdf-height' ] || '' ,
288- //preferCSSPageSize: argument['pdf-preferCSSPageSize'] || '',
289- omitBackground : argument [ 'pdf-omitBackground' ] || false ,
365+ footerTemplate : argument [ 'pdf-footerTemplate' ] ?? '' ,
366+ landscape : argument [ 'pdf-landscape' ] ?? DEFAULT_LANDSCAPE ,
367+ width : argument [ 'pdf-width' ] ?? '' ,
368+ height : argument [ 'pdf-height' ] ?? '' ,
369+ pageRanges : argument [ 'pdf-pageRanges' ] ,
370+ preferCSSPageSize : argument [ 'pdf-preferCSSPageSize' ] ,
371+ omitBackground : argument [ 'pdf-omitBackground' ] ?? DEFAULT_OMIT_BACKGROUND ,
290372 } )
373+ console . log ( `PDF successfully generated: ${ argument . output } .pdf` )
291374 } catch ( e ) {
292- console . warn ( 'failed to print to pdf' , e )
375+ const error = e as Error
376+ throw new Error (
377+ `Failed to generate PDF at '${ argument . output } .pdf': ${ error . message } `
378+ )
293379 }
294-
295- await browser . close ( )
296380}
0 commit comments