-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathbuild-epub.js
More file actions
160 lines (141 loc) · 4.94 KB
/
build-epub.js
File metadata and controls
160 lines (141 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env node
/**
* Build EPUB from docs/ markdown files using Pandoc.
* Outputs: epub/git-going-with-github.epub
*
* Preprocessing steps before pandoc:
* 1. Strip podcast callout blockquotes (contain PODCASTS.md links)
* 2. Rewrite internal docs .md links to heading-based #anchors
* 3. Remove or rewrite links to files outside docs/ (DAY1_AGENDA, README, etc.)
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const ROOT = path.resolve(__dirname, '..');
const DOCS = path.join(ROOT, 'docs');
const EPUB_OUT = path.join(ROOT, 'epub', 'git-going-with-github.epub');
const DOCX_OUT = path.join(ROOT, 'epub', 'git-going-with-github.docx');
const METADATA = path.join(ROOT, 'epub', 'metadata.yaml');
const EPUB_CSS = path.join(ROOT, 'epub', 'epub.css');
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'epub-'));
// Ordered file list: course-guide first, then 00-16, then appendices a-z
function getDocFiles() {
const all = fs.readdirSync(DOCS)
.filter(f => f.endsWith('.md'))
.sort();
const courseGuide = all.filter(f => f === 'course-guide.md');
const chapters = all.filter(f => /^\d{2}-/.test(f));
const appendices = all.filter(f => f.startsWith('appendix-'));
const rest = all.filter(f =>
!courseGuide.includes(f) &&
!chapters.includes(f) &&
!appendices.includes(f)
);
return [...courseGuide, ...chapters, ...appendices, ...rest]
.map(f => path.join(DOCS, f));
}
// Build a map of doc filename -> heading anchor for cross-chapter links
// e.g. "04-working-with-issues.md" -> "#working-with-issues"
function buildAnchorMap(files) {
const map = {};
for (const f of files) {
const basename = path.basename(f);
const content = fs.readFileSync(f, 'utf-8');
const h1 = content.match(/^#\s+(.+)$/m);
if (h1) {
// Convert heading to pandoc anchor: lowercase, spaces to hyphens, strip non-word chars
const anchor = h1[1]
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
map[basename] = '#' + anchor;
}
}
return map;
}
// Preprocess a single markdown file
function preprocess(content, anchorMap) {
// 1. Remove podcast callout blockquotes
// Pattern: > **Listen to Episode N:** ... line(s)
content = content.replace(/^>[ \t]*\*\*Listen to Episode[^\n]*\n/gm, '');
// 2. Rewrite internal docs cross-links: [text](04-working-with-issues.md) -> [text](#anchor)
// Also handles anchors: [text](04-working-with-issues.md#section) -> [text](#section)
content = content.replace(
/\[([^\]]+)\]\(([^)]+\.md)(#[^)]*)?\)/g,
(match, text, mdFile, anchor) => {
// Strip any leading path components to get just the basename
const basename = path.basename(mdFile);
// If it's a docs/ internal file we know about, rewrite the link
if (anchorMap[basename]) {
const target = anchor || anchorMap[basename];
return `[${text}](${target})`;
}
// External docs (DAY1_AGENDA, README, learning-room, etc.) — keep text, remove link
return text;
}
);
return content;
}
const files = getDocFiles();
const anchorMap = buildAnchorMap(files);
console.log(`Building EPUB from ${files.length} files...\n`);
files.forEach(f => console.log(' ', path.relative(ROOT, f)));
// Write preprocessed files to tmp dir
const tmpFiles = files.map(f => {
const content = fs.readFileSync(f, 'utf-8');
const cleaned = preprocess(content, anchorMap);
const tmpPath = path.join(TMP, path.basename(f));
fs.writeFileSync(tmpPath, cleaned, 'utf-8');
return tmpPath;
});
const fileArgs = tmpFiles.map(f => `"${f}"`).join(' ');
// --- EPUB ---
const epubCmd = [
'pandoc',
'--from markdown+smart',
'--to epub3',
`--output "${EPUB_OUT}"`,
`--metadata-file "${METADATA}"`,
`--css "${EPUB_CSS}"`,
'--toc',
'--toc-depth=2',
'--split-level=1',
'--highlight-style=tango',
'--wrap=none',
fileArgs
].join(' \\\n ');
console.log('\nRunning pandoc (EPUB)...\n');
try {
execSync(epubCmd, { stdio: 'inherit', cwd: ROOT });
const size = (fs.statSync(EPUB_OUT).size / 1024).toFixed(1);
console.log(`\nEPUB written: epub/git-going-with-github.epub (${size} KB)`);
} catch (err) {
console.error('\nPandoc EPUB failed. Is pandoc installed? Run: brew install pandoc');
process.exit(1);
}
// --- Word (.docx) ---
const docxCmd = [
'pandoc',
'--from markdown+smart',
'--to docx',
`--output "${DOCX_OUT}"`,
`--metadata-file "${METADATA}"`,
'--toc',
'--toc-depth=2',
'--wrap=none',
fileArgs
].join(' \\\n ');
console.log('\nRunning pandoc (Word)...\n');
try {
execSync(docxCmd, { stdio: 'inherit', cwd: ROOT });
const size = (fs.statSync(DOCX_OUT).size / 1024).toFixed(1);
console.log(`\nWord written: epub/git-going-with-github.docx (${size} KB)`);
} catch (err) {
console.error('\nPandoc Word failed.');
process.exit(1);
} finally {
fs.rmSync(TMP, { recursive: true, force: true });
}