diff --git a/.gitignore b/.gitignore index b79279f..46fdfbe 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ TODOs.md .eslintcache .direnv/ .pre-commit-config.yaml +.temp +*.epub \ No newline at end of file diff --git a/docs/en/nix-store/host-your-own-binary-cache-server.md b/docs/en/nix-store/host-your-own-binary-cache-server.md index c6475cb..b65df2c 100644 --- a/docs/en/nix-store/host-your-own-binary-cache-server.md +++ b/docs/en/nix-store/host-your-own-binary-cache-server.md @@ -220,7 +220,7 @@ automatically removed after a specified number of days. This is useful for keeping the cache size manageable and ensuring that outdated binaries are not stored indefinitely. -## References {#references} +## References - [Blog post by Jeff on Nix binary caches](https://jcollie.github.io/nixos/2022/04/27/nixos-binary-cache-2022.html) - [Binary cache in the NixOS wiki](https://wiki.nixos.org/wiki/Binary_Cache) diff --git a/epub-export.ts b/epub-export.ts new file mode 100644 index 0000000..23b2885 --- /dev/null +++ b/epub-export.ts @@ -0,0 +1,167 @@ +import config from "./docs/.vitepress/config" +import fs from "fs" +import path from "path" +import { exec } from "child_process" + +const sidebar: { + text: string + items?: { text: string; link: string }[] +}[] = config.locales!.root.themeConfig!.sidebar as any + +// -------- helpers -------- + +/** + * Reduce fence opener to plain ```lang (strip any {...}/attributes). + * Also map shell/console → bash. If no lang, keep plain ``` only. + */ +function normalizeFenceOpeners(md: string): string { + return md.split(/(```[\s\S]*?```)/g).map(block => { + if (!block.startsWith("```")) return block + + const lines = block.split("\n") + const opener = lines[0] + const m = opener.match(/^```([^\n]*)$/) + if (!m) return block + + let info = m[1].trim() + + // Attribute form: ```{.nix ...} → extract first class as lang + if (info.startsWith("{")) { + const mm = info.match(/\.([a-zA-Z0-9_-]+)/) + const lang = mm ? mm[1] : "" + const mapped = (lang === "shell" || lang === "console") ? "bash" : lang + lines[0] = "```" + (mapped || "") + return lines.join("\n") + } + + // Info-string form: ```lang{...} or ```lang + const mm = info.match(/^([a-zA-Z0-9_-]+)(\{[^}]*\})?$/) // ignore tail + if (!mm) { + // unknown → leave as-is + lines[0] = "```" + info + return lines.join("\n") + } + + let lang = mm[1] + if (lang === "shell" || lang === "console") lang = "bash" + + lines[0] = "```" + (lang || "") + return lines.join("\n") + }).join("") +} + +/** Add left-gutter line numbers as literal text (e.g., " 1 | …") inside fenced blocks. */ +function addLineNumbersToFences(md: string): string { + return md.split(/(```[\s\S]*?```)/g).map(block => { + if (!block.startsWith("```")) return block + + const lines = block.split("\n") + // find closing fence + let closeIdx = lines.length - 1 + while (closeIdx > 0 && !lines[closeIdx].startsWith("```")) closeIdx-- + + const opener = lines[0] + const body = lines.slice(1, closeIdx) + const width = Math.max(1, String(body.length).length) + + const numbered = body.map((l, i) => `${String(i + 1).padStart(width, " ")} | ${l}`) + const tail = lines.slice(closeIdx) // includes closing fence + return [opener, ...numbered, ...tail].join("\n") + }).join("") +} + +/** Apply XHTML + path fixes only outside fenced code blocks. */ +function sanitizeOutsideCode(md: string): string { + return md.split(/(```[\s\S]*?```)/g).map(part => { + if (part.startsWith("```")) return part + return part + .replace(//g, "
") + .replace(/]*?)(?/g, "") + .replace(/!\[([^\]]*)\]\(\/([^)]*)\)/g, "![$1]($2)") // MD images /foo → foo + .replace(/src="\/([^"]+)"/g, 'src="$1"') // HTML → "foo" + }).join("") +} + +// -------- setup .temp -------- + +const tempDir = ".temp" +if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true }) +fs.mkdirSync(tempDir, { recursive: true }) + +// --- Generate file list --- +let fileList: string[] = [] + +for (const category of sidebar) { + if (category.items) { + for (const item of category.items) { + if (item.link && item.link.endsWith(".md")) { + const filePath = path.join("en", item.link).replace(/\\/g, "/") + fileList.push(filePath) + } + } + } +} + +console.log("Files to include:", fileList) + +// --- Copy and patch Markdown files into .temp --- +for (const relFile of fileList) { + + const srcPath = path.join("docs", relFile) + const dstPath = path.join(tempDir, relFile) + + fs.mkdirSync(path.dirname(dstPath), { recursive: true }) + let content = fs.readFileSync(srcPath, "utf8") + + // 1) Strip attributes/ranges: end up with plain ```lang (alias shell→bash) + content = normalizeFenceOpeners(content) + + // 2) XHTML + path fixes only outside code + content = sanitizeOutsideCode(content) + + // 3) Inline line numbers (start at 1) + content = addLineNumbersToFences(content) + + fs.writeFileSync(dstPath, content) +} + +// --- Write Kindle CSS fix --- +const css = ` +/* Fix Kindle extra spacing in Pandoc-highlighted code blocks */ +code.sourceCode > span { display: inline !important; } /* override inline-block */ +pre > code.sourceCode > span { display: inline !important; } /* extra safety */ +pre { line-height: 1.2 !important; margin: 0 !important; } /* tighten & remove gaps */ +pre code { display: block; padding: 0; margin: 0; } +pre, code { font-variant-ligatures: none; } /* avoid odd ligature spacing */ +pre > code.sourceCode { white-space: pre; } /* don’t pre-wrap lines */ +`; +fs.writeFileSync(path.join(tempDir, "epub-fixes.css"), css); + + +// --- Run Pandoc --- +const outputFileName = "../nixos-and-flakes-book.epub" +const pandocCommand = `pandoc ${fileList.join(" ")} \ + -o ${outputFileName} \ + --from=markdown+gfm_auto_identifiers+pipe_tables+raw_html+tex_math_dollars+fenced_code_blocks+fenced_code_attributes \ + --to=epub3 \ + --standalone \ + --toc --toc-depth=2 \ + --number-sections \ + --embed-resources \ + --highlight-style=tango \ + --css=epub-fixes.css \ + --metadata=title:"NixOS and Flakes Book" \ + --metadata=author:"Ryan Yin" \ + --resource-path=.:../docs/public:en` + +console.log("🚀 Executing pandoc:", pandocCommand) + +exec(pandocCommand, { cwd: tempDir }, (error, stdout, stderr) => { + if (error) { + console.error(`❌ Pandoc failed: ${error}`) + return + } + if (stdout) console.log(stdout) + if (stderr) console.error(stderr) + console.log("✅ EPUB generated:", outputFileName) +}) diff --git a/package.json b/package.json index f7e0392..84c96ed 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", - "export-pdf": "press-export-pdf export ./docs --outFile ./nixos-and-flakes-book.pdf" + "export-pdf": "press-export-pdf export ./docs --outFile ./nixos-and-flakes-book.pdf", + "export-epub": "npx tsx epub-export.ts" }, "dependencies": { "@searking/markdown-it-cjk-breaks": "2.0.1-0",