Skip to content

Commit b3fdc52

Browse files
taylorarndtCopilot
andcommitted
Build EPUB + Word (.docx) formats via pandoc
- Remove WeasyPrint/PDF (dependency issues) - Add Word .docx output using pandoc --to docx - Both formats built from same preprocessed markdown - CI workflow updated: uploads EPUB and Word artifacts (90-day) - .gitignore updated for .docx - pdf.css kept for reference but not used in build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 432f0d0 commit b3fdc52

4 files changed

Lines changed: 271 additions & 17 deletions

File tree

.github/workflows/build-epub.yml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build EPUB
1+
name: Build EPUB and PDF
22

33
on:
44
push:
@@ -7,6 +7,7 @@ on:
77
- 'docs/**'
88
- 'epub/metadata.yaml'
99
- 'epub/epub.css'
10+
- 'epub/pdf.css'
1011
- 'scripts/build-epub.js'
1112
workflow_dispatch:
1213

@@ -30,7 +31,7 @@ jobs:
3031
with:
3132
node-version: 20
3233

33-
- name: Build EPUB
34+
- name: Build EPUB and Word
3435
run: node scripts/build-epub.js
3536

3637
- name: Upload EPUB artifact
@@ -40,14 +41,21 @@ jobs:
4041
path: epub/git-going-with-github.epub
4142
retention-days: 90
4243

43-
- name: Commit EPUB to repository
44+
- name: Upload Word artifact
45+
uses: actions/upload-artifact@v4
46+
with:
47+
name: git-going-with-github-word
48+
path: epub/git-going-with-github.docx
49+
retention-days: 90
50+
51+
- name: Commit outputs to repository
4452
run: |
4553
git config user.name "github-actions[bot]"
4654
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
47-
git add epub/git-going-with-github.epub
55+
git add epub/git-going-with-github.epub epub/git-going-with-github.docx
4856
if [ -n "$(git status --porcelain)" ]; then
49-
git commit -m "chore: rebuild EPUB from latest docs"
57+
git commit -m "chore: rebuild EPUB and Word from latest docs"
5058
git push
5159
else
52-
echo "No EPUB changes to commit"
60+
echo "No changes to commit"
5361
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,4 @@ podcasts/tts/samples/
166166
*.onnx.json
167167
*.wav
168168
epub/git-going-with-github.epub
169+
epub/git-going-with-github.docx

epub/pdf.css

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/* GIT Going with GitHub — PDF Stylesheet (WeasyPrint) */
2+
3+
@page {
4+
size: letter;
5+
margin: 2.5cm 2cm 2.5cm 2cm;
6+
@bottom-center {
7+
content: counter(page);
8+
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
9+
font-size: 0.8em;
10+
color: #6b7280;
11+
}
12+
}
13+
14+
/* ---- Body & typography ---- */
15+
body {
16+
font-family: Georgia, "Times New Roman", serif;
17+
font-size: 11pt;
18+
line-height: 1.7;
19+
color: #1a1a1a;
20+
}
21+
22+
/* ---- Headings ---- */
23+
h1, h2, h3, h4, h5, h6 {
24+
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
25+
font-weight: 700;
26+
line-height: 1.3;
27+
margin-top: 1.5em;
28+
margin-bottom: 0.5em;
29+
page-break-after: avoid;
30+
}
31+
32+
h1 {
33+
font-size: 20pt;
34+
border-bottom: 2px solid #1a1a1a;
35+
padding-bottom: 0.3em;
36+
page-break-before: always;
37+
}
38+
39+
h1:first-child {
40+
page-break-before: avoid;
41+
}
42+
43+
h2 { font-size: 15pt; }
44+
h3 { font-size: 12pt; }
45+
h4 { font-size: 11pt; font-style: italic; }
46+
47+
/* ---- Paragraphs ---- */
48+
p {
49+
margin-top: 0;
50+
margin-bottom: 0.9em;
51+
orphans: 3;
52+
widows: 3;
53+
}
54+
55+
/* ---- Links ---- */
56+
a {
57+
color: #0550ae;
58+
text-decoration: underline;
59+
}
60+
61+
/* Print URLs after external links */
62+
a[href^="http"]::after {
63+
content: " (" attr(href) ")";
64+
font-size: 0.8em;
65+
color: #6b7280;
66+
word-break: break-all;
67+
}
68+
69+
/* ---- Code ---- */
70+
code, kbd, samp {
71+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
72+
font-size: 0.88em;
73+
background: #f3f4f6;
74+
padding: 0.1em 0.3em;
75+
border-radius: 3px;
76+
}
77+
78+
pre {
79+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
80+
font-size: 0.8em;
81+
background: #f3f4f6;
82+
border: 1px solid #d1d5db;
83+
border-radius: 4px;
84+
padding: 0.8em;
85+
white-space: pre-wrap;
86+
word-break: break-word;
87+
page-break-inside: avoid;
88+
}
89+
90+
pre code {
91+
background: none;
92+
padding: 0;
93+
border-radius: 0;
94+
font-size: 1em;
95+
}
96+
97+
/* ---- Blockquotes ---- */
98+
blockquote {
99+
margin: 1em 0 1em 1.5em;
100+
padding-left: 1em;
101+
border-left: 3px solid #9ca3af;
102+
color: #4b5563;
103+
font-style: italic;
104+
}
105+
106+
/* ---- Lists ---- */
107+
ul, ol {
108+
margin: 0.5em 0 0.9em 1.5em;
109+
padding: 0;
110+
}
111+
112+
li {
113+
margin-bottom: 0.3em;
114+
}
115+
116+
/* ---- Tables ---- */
117+
table {
118+
width: 100%;
119+
border-collapse: collapse;
120+
margin: 1em 0;
121+
font-size: 0.88em;
122+
page-break-inside: avoid;
123+
}
124+
125+
th, td {
126+
border: 1px solid #d1d5db;
127+
padding: 0.4em 0.6em;
128+
text-align: left;
129+
vertical-align: top;
130+
}
131+
132+
th {
133+
background: #f3f4f6;
134+
font-weight: 700;
135+
}
136+
137+
tr:nth-child(even) td {
138+
background: #fafafa;
139+
}
140+
141+
/* ---- Horizontal rules ---- */
142+
hr {
143+
border: none;
144+
border-top: 1px solid #d1d5db;
145+
margin: 1.5em 0;
146+
}
147+
148+
/* ---- Images ---- */
149+
img {
150+
max-width: 100%;
151+
height: auto;
152+
}

scripts/build-epub.js

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
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

712
const { execSync } = require('child_process');
813
const fs = require('fs');
914
const path = require('path');
15+
const os = require('os');
1016

1117
const ROOT = path.resolve(__dirname, '..');
1218
const 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');
1421
const 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
1826
function 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]*\*\*Listen to Episode[^\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+
/\[([^\]]+)\]\(([^)]+\.md)(#[^)]*)?\)/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+
3694
const files = getDocFiles();
95+
const anchorMap = buildAnchorMap(files);
3796

3897
console.log(`Building EPUB from ${files.length} files...\n`);
3998
files.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');
60151
try {
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

Comments
 (0)