22/**
33 * Build EPUB from docs/ markdown files using Pandoc.
44 * Outputs: epub/git-going-with-github.epub
5+ *
6+ * Preprocessing steps before pandoc:
7+ * 1. Strip podcast callout blockquotes (contain PODCASTS.md links)
8+ * 2. Rewrite internal docs .md links to heading-based #anchors
9+ * 3. Remove or rewrite links to files outside docs/ (DAY1_AGENDA, README, etc.)
510 */
611
712const { execSync } = require ( 'child_process' ) ;
813const fs = require ( 'fs' ) ;
914const path = require ( 'path' ) ;
15+ const os = require ( 'os' ) ;
1016
1117const ROOT = path . resolve ( __dirname , '..' ) ;
1218const DOCS = path . join ( ROOT , 'docs' ) ;
13- const OUT = path . join ( ROOT , 'epub' , 'git-going-with-github.epub' ) ;
19+ const EPUB_OUT = path . join ( ROOT , 'epub' , 'git-going-with-github.epub' ) ;
20+ const DOCX_OUT = path . join ( ROOT , 'epub' , 'git-going-with-github.docx' ) ;
1421const METADATA = path . join ( ROOT , 'epub' , 'metadata.yaml' ) ;
15- const CSS = path . join ( ROOT , 'epub' , 'epub.css' ) ;
22+ const EPUB_CSS = path . join ( ROOT , 'epub' , 'epub.css' ) ;
23+ const TMP = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'epub-' ) ) ;
1624
1725// Ordered file list: course-guide first, then 00-16, then appendices a-z
1826function getDocFiles ( ) {
@@ -33,20 +41,81 @@ function getDocFiles() {
3341 . map ( f => path . join ( DOCS , f ) ) ;
3442}
3543
44+ // Build a map of doc filename -> heading anchor for cross-chapter links
45+ // e.g. "04-working-with-issues.md" -> "#working-with-issues"
46+ function buildAnchorMap ( files ) {
47+ const map = { } ;
48+ for ( const f of files ) {
49+ const basename = path . basename ( f ) ;
50+ const content = fs . readFileSync ( f , 'utf-8' ) ;
51+ const h1 = content . match ( / ^ # \s + ( .+ ) $ / m) ;
52+ if ( h1 ) {
53+ // Convert heading to pandoc anchor: lowercase, spaces to hyphens, strip non-word chars
54+ const anchor = h1 [ 1 ]
55+ . toLowerCase ( )
56+ . replace ( / [ ^ \w \s - ] / g, '' )
57+ . replace ( / \s + / g, '-' )
58+ . replace ( / - + / g, '-' )
59+ . trim ( ) ;
60+ map [ basename ] = '#' + anchor ;
61+ }
62+ }
63+ return map ;
64+ }
65+
66+ // Preprocess a single markdown file
67+ function preprocess ( content , anchorMap ) {
68+ // 1. Remove podcast callout blockquotes
69+ // Pattern: > **Listen to Episode N:** ... line(s)
70+ content = content . replace ( / ^ > [ \t ] * \* \* L i s t e n t o E p i s o d e [ ^ \n ] * \n / gm, '' ) ;
71+
72+ // 2. Rewrite internal docs cross-links: [text](04-working-with-issues.md) -> [text](#anchor)
73+ // Also handles anchors: [text](04-working-with-issues.md#section) -> [text](#section)
74+ content = content . replace (
75+ / \[ ( [ ^ \] ] + ) \] \( ( [ ^ ) ] + \. m d ) ( # [ ^ ) ] * ) ? \) / g,
76+ ( match , text , mdFile , anchor ) => {
77+ // Strip any leading path components to get just the basename
78+ const basename = path . basename ( mdFile ) ;
79+
80+ // If it's a docs/ internal file we know about, rewrite the link
81+ if ( anchorMap [ basename ] ) {
82+ const target = anchor || anchorMap [ basename ] ;
83+ return `[${ text } ](${ target } )` ;
84+ }
85+
86+ // External docs (DAY1_AGENDA, README, learning-room, etc.) — keep text, remove link
87+ return text ;
88+ }
89+ ) ;
90+
91+ return content ;
92+ }
93+
3694const files = getDocFiles ( ) ;
95+ const anchorMap = buildAnchorMap ( files ) ;
3796
3897console . log ( `Building EPUB from ${ files . length } files...\n` ) ;
3998files . forEach ( f => console . log ( ' ' , path . relative ( ROOT , f ) ) ) ;
4099
41- const fileArgs = files . map ( f => `"${ f } "` ) . join ( ' ' ) ;
100+ // Write preprocessed files to tmp dir
101+ const tmpFiles = files . map ( f => {
102+ const content = fs . readFileSync ( f , 'utf-8' ) ;
103+ const cleaned = preprocess ( content , anchorMap ) ;
104+ const tmpPath = path . join ( TMP , path . basename ( f ) ) ;
105+ fs . writeFileSync ( tmpPath , cleaned , 'utf-8' ) ;
106+ return tmpPath ;
107+ } ) ;
42108
43- const cmd = [
109+ const fileArgs = tmpFiles . map ( f => `"${ f } "` ) . join ( ' ' ) ;
110+
111+ // --- EPUB ---
112+ const epubCmd = [
44113 'pandoc' ,
45114 '--from markdown+smart' ,
46115 '--to epub3' ,
47- `--output "${ OUT } "` ,
116+ `--output "${ EPUB_OUT } "` ,
48117 `--metadata-file "${ METADATA } "` ,
49- `--css "${ CSS } "` ,
118+ `--css "${ EPUB_CSS } "` ,
50119 '--toc' ,
51120 '--toc-depth=2' ,
52121 '--split-level=1' ,
@@ -55,13 +124,37 @@ const cmd = [
55124 fileArgs
56125] . join ( ' \\\n ' ) ;
57126
58- console . log ( '\nRunning pandoc...\n' ) ;
127+ console . log ( '\nRunning pandoc (EPUB)...\n' ) ;
128+ try {
129+ execSync ( epubCmd , { stdio : 'inherit' , cwd : ROOT } ) ;
130+ const size = ( fs . statSync ( EPUB_OUT ) . size / 1024 ) . toFixed ( 1 ) ;
131+ console . log ( `\nEPUB written: epub/git-going-with-github.epub (${ size } KB)` ) ;
132+ } catch ( err ) {
133+ console . error ( '\nPandoc EPUB failed. Is pandoc installed? Run: brew install pandoc' ) ;
134+ process . exit ( 1 ) ;
135+ }
136+
137+ // --- Word (.docx) ---
138+ const docxCmd = [
139+ 'pandoc' ,
140+ '--from markdown+smart' ,
141+ '--to docx' ,
142+ `--output "${ DOCX_OUT } "` ,
143+ `--metadata-file "${ METADATA } "` ,
144+ '--toc' ,
145+ '--toc-depth=2' ,
146+ '--wrap=none' ,
147+ fileArgs
148+ ] . join ( ' \\\n ' ) ;
59149
150+ console . log ( '\nRunning pandoc (Word)...\n' ) ;
60151try {
61- execSync ( cmd , { stdio : 'inherit' , cwd : ROOT } ) ;
62- const size = ( fs . statSync ( OUT ) . size / 1024 ) . toFixed ( 1 ) ;
63- console . log ( `\nDone. EPUB written to : epub/git-going-with-github.epub (${ size } KB)` ) ;
152+ execSync ( docxCmd , { stdio : 'inherit' , cwd : ROOT } ) ;
153+ const size = ( fs . statSync ( DOCX_OUT ) . size / 1024 ) . toFixed ( 1 ) ;
154+ console . log ( `\nWord written: epub/git-going-with-github.docx (${ size } KB)` ) ;
64155} catch ( err ) {
65- console . error ( '\nPandoc failed. Is pandoc installed? Run: brew install pandoc ' ) ;
156+ console . error ( '\nPandoc Word failed.' ) ;
66157 process . exit ( 1 ) ;
158+ } finally {
159+ fs . rmSync ( TMP , { recursive : true , force : true } ) ;
67160}
0 commit comments