diff --git a/epub/.gitignore b/epub/.gitignore new file mode 100644 index 000000000..44774968b --- /dev/null +++ b/epub/.gitignore @@ -0,0 +1,5 @@ +dist/ +node_modules/ +*.epub +test_*.js +validation-report.json diff --git a/epub/README.md b/epub/README.md new file mode 100644 index 000000000..1be24744d --- /dev/null +++ b/epub/README.md @@ -0,0 +1,58 @@ +# Hello Algorithm EPUB Converter + +Convert [Hello Algorithm](https://github.com/krahets/hello-algo) Markdown documentation to EPUB e-books. + +## Installation + +```bash +npm install +``` + +## Usage + +### Basic Usage + +```bash +# Build Chinese version with C++ code (default) +npm run build + +# Build with specific programming language +npm run build -- -l python + +# Build Traditional Chinese version +npm run build -- -d zh-hant -l python -o hello-algo-zh-hant-python.epub + +# Build English version +npm run build -- -d en -l python -o hello-algo-en-python.epub + +# Build Japanese version +npm run build -- -d ja -l cpp -o hello-algo-ja-cpp.epub + +# Build all combinations +npm run build -- --all --release-version 1.2.0 +``` + +### Command Line Parameters + +| Parameter | Short | Description | Default | +|-----------|-------|-------------|---------| +| `--doc-language` | `-d` | Document language (zh, zh-hant, en, ja) | `zh` | +| `--output` | `-o` | Output EPUB file path | `./hello-algo.epub` | +| `--language` | `-l` | Programming language | `cpp` | +| `--all` | `-a` | Build all combinations | - | +| `--release-version` | - | Version number for output filename | `1.0.0` | +| `--validate` | - | Enable content integrity validation | `false` | +| `--help` | `-h` | Show help | - | +| `--version` | `-V` | Show version | - | + +### Supported Languages + +**Document Languages:** +- `zh` - Simplified Chinese (all programming languages) +- `zh-hant` - Traditional Chinese (all programming languages) +- `en` - English (cpp, java, python only) +- `ja` - Japanese (cpp, java, python only) + +**Programming Languages:** +- Supported by all document languages: `cpp`, `python`, `java` +- Supported by Chinese versions only: `csharp`, `go`, `swift`, `javascript`, `typescript`, `dart`, `rust`, `c`, `kotlin`, `ruby`, `zig` diff --git a/epub/covers/hello-algo-cover-en.jpg b/epub/covers/hello-algo-cover-en.jpg new file mode 100644 index 000000000..f65fad98b Binary files /dev/null and b/epub/covers/hello-algo-cover-en.jpg differ diff --git a/epub/covers/hello-algo-cover-zh-hant.jpg b/epub/covers/hello-algo-cover-zh-hant.jpg new file mode 100644 index 000000000..0ba7ca92a Binary files /dev/null and b/epub/covers/hello-algo-cover-zh-hant.jpg differ diff --git a/epub/covers/hello-algo-cover-zh.jpg b/epub/covers/hello-algo-cover-zh.jpg new file mode 100644 index 000000000..209efe63b Binary files /dev/null and b/epub/covers/hello-algo-cover-zh.jpg differ diff --git a/epub/fonts/MathJax_Main-Regular.otf b/epub/fonts/MathJax_Main-Regular.otf new file mode 100644 index 000000000..5cfdff9a9 Binary files /dev/null and b/epub/fonts/MathJax_Main-Regular.otf differ diff --git a/epub/fonts/MathJax_Math-Regular.otf b/epub/fonts/MathJax_Math-Regular.otf new file mode 100644 index 000000000..9d9c1684a Binary files /dev/null and b/epub/fonts/MathJax_Math-Regular.otf differ diff --git a/epub/package-lock.json b/epub/package-lock.json new file mode 100644 index 000000000..a3ca142fc --- /dev/null +++ b/epub/package-lock.json @@ -0,0 +1,1735 @@ +{ + "name": "hello-algo-epub", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hello-algo-epub", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/commander": "^2.12.0", + "adm-zip": "^0.5.10", + "commander": "^14.0.2", + "epub-gen": "^0.1.0", + "fs-extra": "^11.2.0", + "highlight.js": "^11.11.1", + "js-yaml": "^4.1.0", + "marked": "^11.1.1", + "path": "^0.12.7" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.11.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/commander": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.0.tgz", + "integrity": "sha512-DDmRkovH7jPjnx7HcbSnqKg2JeNANyxNZeUvB0iE+qKBLN+vzN5iSIwt+J2PFSmBuYEut4mgQvI/fTX9YQH/vw==", + "license": "MIT", + "dependencies": { + "commander": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", + "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/archiver": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", + "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^2.6.3", + "buffer-crc32": "^0.2.1", + "glob": "^7.1.4", + "readable-stream": "^3.4.0", + "tar-stream": "^2.1.0", + "zip-stream": "^2.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", + "license": "MIT", + "dependencies": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", + "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^3.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^2.3.6" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "license": "MIT", + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc32-stream": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", + "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", + "license": "MIT", + "dependencies": { + "crc": "^3.4.4", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", + "license": "BSD-like", + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "node_modules/css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "license": "BSD-2-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "license": "MIT", + "dependencies": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "license": "BSD-2-Clause" + }, + "node_modules/epub-gen": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/epub-gen/-/epub-gen-0.1.0.tgz", + "integrity": "sha512-Xt2tP4XlDkZnrJCumP+3v4nEWqIN5JXNu0V5aUywwmKkhhIUrzRQ75igKFBbE2H0mUKlWnIpv2YMmzEa+RJeiw==", + "license": "MIT", + "dependencies": { + "archiver": "^3.0.0", + "cheerio": "^0.22.0", + "diacritics": "^1.3.0", + "ejs": "^2.6.1", + "entities": "^1.1.2", + "fs-extra": "^7.0.1", + "mime": "^2.4.0", + "q": "^1.5.1", + "rimraf": "^2.6.3", + "superagent": "^3.8.3", + "underscore": "^1.9.1", + "uslug": "^1.0.4" + } + }, + "node_modules/epub-gen/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/epub-gen/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/epub-gen/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "license": "MIT", + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", + "license": "MIT" + }, + "node_modules/lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "license": "MIT" + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "license": "MIT" + }, + "node_modules/lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", + "license": "MIT" + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/marked": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", + "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "license": "MIT", + "dependencies": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/superagent/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/superagent/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/superagent/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "license": "MIT or GPL-2.0", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uslug": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz", + "integrity": "sha512-Jrbpp/NS3TvIGNjfJT1sn3/BCeykoxR8GbNYW5lF6fUscLkbXFwj1b7m4DvIkHm8k3Qr6Co68lbTmoZTMGk/ow==", + "dependencies": { + "unorm": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zip-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", + "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^2.1.1", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/epub/package.json b/epub/package.json new file mode 100644 index 000000000..c2cbf70fc --- /dev/null +++ b/epub/package.json @@ -0,0 +1,34 @@ +{ + "name": "hello-algo-epub", + "version": "1.0.0", + "description": "Convert Hello Algorithm docs to EPUB", + "main": "dist/index.js", + "scripts": { + "build": "ts-node src/index.ts" + }, + "keywords": [ + "epub", + "markdown", + "ebook" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@types/commander": "^2.12.0", + "adm-zip": "^0.5.10", + "commander": "^14.0.2", + "epub-gen": "^0.1.0", + "fs-extra": "^11.2.0", + "highlight.js": "^11.11.1", + "js-yaml": "^4.1.0", + "marked": "^11.1.1", + "path": "^0.12.7" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.11.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/epub/src/epub.ts b/epub/src/epub.ts new file mode 100644 index 000000000..6d573ec7e --- /dev/null +++ b/epub/src/epub.ts @@ -0,0 +1,371 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Chapter, ImageInfo, EpubGenOptions } from './types'; +import { markdownToHtml, extractImagePaths, readImage, getCustomCSS } from './markdown'; + +const Epub = require('epub-gen'); + +export interface EpubOptions { + title: string; + author: string; + publisher?: string; + description?: string; + language?: string; + cover?: string; + codeLanguage?: string; // Programming language for code examples + docLanguage?: string; // Document language (zh, zh-hant, en, ja) + version?: string; // Version number +} + +/** + * Get table of contents title based on document language + */ +function getTocTitle(docLanguage?: string): string { + const tocTitles: { [key: string]: string } = { + 'zh': '目录', + 'zh-hant': '目錄', + 'en': 'Table of Contents', + 'ja': '目次', + }; + return tocTitles[docLanguage || 'zh'] || '目录'; +} + +/** + * Generate homepage HTML + */ +function generateTitlePage(title: string, author: string, docLanguage?: string, version?: string, codeLanguage?: string): string { + const lang = docLanguage || 'zh'; + + // Code reviewer mapping by language and document language + const codeReviewers: { [docLang: string]: { [codeLang: string]: string } } = { + 'zh': { + 'python': '靳宇栋(@krahets)', + 'cpp': '宫兰景(@Gonglja)', + 'java': '靳宇栋(@krahets)', + 'csharp': '@hpstory', + 'go': '刘代富(@Reanon)', + 'swift': '@nuomi1', + 'javascript': '谢发 (@justin-tse)', + 'typescript': '谢发 (@justin-tse)', + 'dart': '刘玉新(@gvenusleo)', + 'rust': '伍志豪(@night-cruise)、荣怡(@rongyi)', + 'c': '宫兰景(@Gonglja)', + 'ruby': '阮春科秀(@khoaxuantu)', + 'kotlin': '陈东辉(@curtishd)', + }, + 'zh-hant': { + 'python': '靳宇棟(@krahets)', + 'cpp': '宮蘭景(@Gonglja)', + 'java': '靳宇棟(@krahets)', + 'csharp': '@hpstory', + 'go': '劉代富(@Reanon)', + 'swift': '@nuomi1', + 'javascript': '謝發 (@justin-tse)', + 'typescript': '謝發 (@justin-tse)', + 'dart': '劉玉新(@gvenusleo)', + 'rust': '伍志豪(@night-cruise)、榮怡(@rongyi)', + 'c': '宮蘭景(@Gonglja)', + 'ruby': '阮春科秀(@khoaxuantu)', + 'kotlin': '陳東輝(@curtishd)', + }, + 'en': { + 'python': 'Yudong Jin (@krahets)', + 'cpp': 'Lanjing Gong (@Gonglja)', + 'java': 'Yudong Jin (@krahets)', + 'csharp': '@hpstory', + 'go': 'Daifu Liu (@Reanon)', + 'swift': '@nuomi1', + 'javascript': 'Fa Xie (@justin-tse)', + 'typescript': 'Fa Xie (@justin-tse)', + 'dart': 'Yuxin Liu (@gvenusleo)', + 'rust': 'Zhihao Wu (@night-cruise), Yi Rong (@rongyi)', + 'c': 'Lanjing Gong (@Gonglja)', + 'ruby': 'Chunke Xiu Ruan (@khoaxuantu)', + 'kotlin': 'Donghui Chen (@curtishd)', + }, + 'ja': { + 'python': '靳宇棟(@krahets)', + 'cpp': '宮蘭景(@Gonglja)', + 'java': '靳宇棟(@krahets)', + 'csharp': '@hpstory', + 'go': '劉代富(@Reanon)', + 'swift': '@nuomi1', + 'javascript': '謝発 (@justin-tse)', + 'typescript': '謝発 (@justin-tse)', + 'dart': '劉玉新(@gvenusleo)', + 'rust': '伍志豪(@night-cruise)、栄怡(@rongyi)', + 'c': '宮蘭景(@Gonglja)', + 'ruby': '阮春科秀(@khoaxuantu)', + 'kotlin': '陳東輝(@curtishd)', + }, + }; + + // Multilingual text configuration + const i18n: { [key: string]: { subtitle: string; authorPrefix: string; authorName: string; codeReviewPrefix: string; readOnline: string; codeRepo: string; versionPrefix: string } } = { + 'zh': { + subtitle: '动画图解、一键运行的数据结构与算法教程', + authorPrefix: '作者:', + authorName: '靳宇栋 (@krahets)', + codeReviewPrefix: '代码审阅:', + readOnline: '在线阅读', + codeRepo: '代码仓库', + versionPrefix: '版本', + }, + 'zh-hant': { + subtitle: '動畫圖解、一鍵運行的資料結構與演算法教程', + authorPrefix: '作者:', + authorName: '靳宇棟 (@krahets)', + codeReviewPrefix: '程式碼審閱:', + readOnline: '線上閱讀', + codeRepo: '程式碼倉庫', + versionPrefix: '版本', + }, + 'en': { + subtitle: 'Data structures and algorithms crash course with animated illustrations and off-the-shelf code', + authorPrefix: 'Author: ', + authorName: 'Yudong Jin (@krahets)', + codeReviewPrefix: 'Code Review: ', + readOnline: 'Read Online', + codeRepo: 'Code Repository', + versionPrefix: 'Version', + }, + 'ja': { + subtitle: 'アニメーション図解、ワンクリック実行のデータ構造とアルゴリズム教程', + authorPrefix: '著者:', + authorName: '靳宇棟 (@krahets)', + codeReviewPrefix: 'コードレビュー:', + readOnline: 'オンライン閲覧', + codeRepo: 'コードリポジトリ', + versionPrefix: 'バージョン', + }, + }; + + const text = i18n[lang] || i18n['zh']; + const versionText = version ? `${text.versionPrefix} ${version}` : ''; + const codeReviewer = codeLanguage && codeReviewers[lang] && codeReviewers[lang][codeLanguage] + ? codeReviewers[lang][codeLanguage] + : ''; + + return ` +
+

Hello 算法

+ +

${text.subtitle}

+ +

${text.authorPrefix}${text.authorName}

+ + ${codeReviewer ? `

${text.codeReviewPrefix}${codeReviewer}

` : ''} + + ${versionText ? `

${versionText}

` : ''} + +
+

+ ${text.readOnline}
+ www.hello-algo.com +

+

+ ${text.codeRepo}
+ github.com/krahets/hello-algo +

+
+
+ `; +} + +/** + * Generate EPUB e-book + */ +export async function generateEpub( + chapters: Chapter[], + docsDir: string, + outputPath: string, + options: EpubOptions +): Promise { + console.log('Starting EPUB generation...'); + + // Create temporary directory for images + const tempDir = path.join(path.dirname(outputPath), 'temp_images'); + fs.ensureDirSync(tempDir); + + // Prepare content + const content: any[] = []; + const imageMap: { [original: string]: string } = {}; + let imageIndex = 0; + + // Add homepage (before table of contents) + const titlePageTitles: { [key: string]: string } = { + 'zh': '首页', + 'zh-hant': '首頁', + 'en': 'Home', + 'ja': 'ホーム', + }; + const titlePageTitle = titlePageTitles[options.docLanguage || 'zh'] || '首页'; + + content.push({ + title: titlePageTitle, + data: generateTitlePage(options.title, options.author, options.docLanguage, options.version, options.codeLanguage), + beforeToc: true, // Before table of contents + excludeFromToc: false, // Include in table of contents + level: 0, + number: '', + }); + + for (const chapter of chapters) { + if (!chapter.content) { + continue; + } + + console.log(`Processing chapter: ${chapter.title}`); + + // Read chapter content + const markdown = chapter.content; + const chapterDir = path.dirname(chapter.path); + + // Extract images and copy to temporary directory + const imagePaths = extractImagePaths(markdown, chapter.path); + for (const { original, fullPath } of imagePaths) { + if (!imageMap[original]) { + // Copy image to temporary directory + const ext = path.extname(fullPath); + const newFileName = `img_${imageIndex}${ext}`; + const tempImagePath = path.join(tempDir, newFileName); + fs.copyFileSync(fullPath, tempImagePath); + imageMap[original] = tempImagePath; + imageIndex++; + } + + // Replace image paths in markdown with absolute paths (using file:// protocol) + const tempImagePath = imageMap[original]; + chapter.content = chapter.content.replace( + new RegExp(`!\\[[^\\]]*\\]\\(${escapeRegex(original)}\\)`, 'g'), + `![${path.basename(fullPath)}](file://${tempImagePath})` + ); + } + + // Use chapter title directly (MD documents already have numbering) + const displayTitle = chapter.title.trim(); + + // Convert to HTML + const codeLanguage = options.codeLanguage || 'cpp'; + const html = markdownToHtml(chapter.content, chapterDir, codeLanguage, options.docLanguage, chapter.path); + + content.push({ + title: displayTitle, + data: html, + // Do not use beforeToc, let all chapters arrange naturally in order + // Chapters are already arranged according to mkdocs.yml order, parent chapters first, child chapters follow + beforeToc: false, + excludeFromToc: false, + // Add level information for generating nested table of contents + level: chapter.level, + parentTitle: chapter.parentTitle, + number: chapter.number, + }); + } + + // Prepare font file paths + const fontsDir = path.join(__dirname, '..', 'fonts'); + const fonts: string[] = []; + + // Add math fonts (shared by all versions) + const mathJaxMathPath = path.join(fontsDir, 'MathJax_Math-Regular.otf'); + const mathJaxMainPath = path.join(fontsDir, 'MathJax_Main-Regular.otf'); + + // Add code font (shared by all versions) + const jetbrainsMonoPath = path.join(fontsDir, 'JetBrainsMonoNerdFont-Regular.ttf'); + + // Select body font based on document language + const docLang = options.docLanguage || 'zh'; + let serifFontPath: string | null = null; + let serifItalicFontPath: string | null = null; + + if (docLang === 'zh' || docLang === 'zh-hant') { + // Chinese version uses Noto Serif SC Regular + serifFontPath = path.join(fontsDir, 'NotoSerifSC-Regular.ttf'); + } else if (docLang === 'en') { + // English version uses Roboto Serif + serifFontPath = path.join(fontsDir, 'RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf'); + serifItalicFontPath = path.join(fontsDir, 'RobotoSerif-Italic-VariableFont_GRAD,opsz,wdth,wght.ttf'); + } else if (docLang === 'ja') { + // Japanese version uses Noto Serif JP + serifFontPath = path.join(fontsDir, 'NotoSerifJP-VariableFont_wght.ttf'); + } + + // Add math fonts + if (fs.existsSync(mathJaxMathPath)) { + fonts.push(mathJaxMathPath); + } else { + console.warn(`Warning: Font file does not exist: ${mathJaxMathPath}`); + } + if (fs.existsSync(mathJaxMainPath)) { + fonts.push(mathJaxMainPath); + } else { + console.warn(`Warning: Font file does not exist: ${mathJaxMainPath}`); + } + + // Add body font + if (serifFontPath && fs.existsSync(serifFontPath)) { + fonts.push(serifFontPath); + } else if (serifFontPath) { + console.warn(`Warning: Font file does not exist: ${serifFontPath}`); + } + + // Add italic font (if available) + if (serifItalicFontPath && fs.existsSync(serifItalicFontPath)) { + fonts.push(serifItalicFontPath); + } else if (serifItalicFontPath) { + console.warn(`Warning: Font file does not exist: ${serifItalicFontPath}`); + } + + // Add code font + if (fs.existsSync(jetbrainsMonoPath)) { + fonts.push(jetbrainsMonoPath); + } else { + console.warn(`Warning: Font file does not exist: ${jetbrainsMonoPath}`); + } + + // Prepare EPUB options + const epubOptions: EpubGenOptions = { + title: options.title, + author: options.author, + publisher: options.publisher || 'Hello Algorithm', + description: options.description || '动画图解、一键运行的数据结构与算法教程', + language: options.language || 'zh-CN', + content: content, + verbose: true, + // Disable automatic chapter title addition (we already handle this in Markdown) + appendChapterTitles: false, + // Use custom templates to generate nested table of contents + customNcxTocTemplatePath: path.join(__dirname, '..', 'templates', 'toc.ncx.ejs'), + customHtmlTocTemplatePath: path.join(__dirname, '..', 'templates', 'toc.xhtml.ejs'), + // Inject custom CSS (based on document language) + css: getCustomCSS(options.docLanguage), + // Set cover + cover: options.cover, + // Add font files + fonts: fonts, + // Set table of contents title based on document language + tocTitle: getTocTitle(options.docLanguage), + }; + + // Generate EPUB + try { + epubOptions.output = outputPath; + const epub = new Epub(epubOptions); + await epub.promise; + console.log(`EPUB generated successfully: ${outputPath}`); + + // Clean up temporary directory + fs.removeSync(tempDir); + } catch (error) { + console.error('Error generating EPUB:', error); + // Clean up temporary directory + fs.removeSync(tempDir); + throw error; + } +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + diff --git a/epub/src/index.ts b/epub/src/index.ts new file mode 100644 index 000000000..2c1f90a91 --- /dev/null +++ b/epub/src/index.ts @@ -0,0 +1,535 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { execSync } from 'child_process'; +import { Command } from 'commander'; +import { parseMkdocsConfig, flattenNav, readChapterContent, parseVersion } from './parser'; +import { generateEpub } from './epub'; +import { Chapter, HeadingInfo } from './types'; +import { extractHeadings } from './markdown'; +import { validateEpubHeadings } from './validator'; + +// Supported programming languages list +const SUPPORTED_LANGUAGES = [ + 'cpp', 'python', 'java', 'csharp', 'go', 'swift', + 'javascript', 'typescript', 'dart', 'rust', 'c', + 'kotlin', 'ruby', 'zig' +]; + +// Document language configuration +interface DocLanguageConfig { + mkdocsPath: string; + docsDir: string; + title: string; + description: string; + language: string; + supportedCodeLanguages?: string[]; // Supported programming languages list +} + +/** + * Generate output filename + */ +function generateOutputFilename(docLanguage: string, codeLanguage: string, workDir: string, projectRoot: string): string { + // Get document language configuration + const docConfig = DOC_LANGUAGE_CONFIG[docLanguage]; + const mkdocsPath = path.resolve(workDir, docConfig.mkdocsPath); + + // Parse version from mkdocs.yml + const version = parseVersion(mkdocsPath); + + const filename = `hello-algo_${version}_${docLanguage}_${codeLanguage}.epub`; + const outputDir = path.join(projectRoot, 'build', 'epub', 'outputs'); + fs.ensureDirSync(outputDir); + return path.join(outputDir, filename); +} + +/** + * Get git repository remote URL + */ +function getGitRemoteUrl(): string { + try { + const url = execSync('git remote get-url origin', { encoding: 'utf-8', cwd: process.cwd() }).trim(); + return url; + } catch (error) { + console.error('Error: Unable to get git remote URL'); + console.error('Please ensure the current directory is a git repository or manually specify the repository URL'); + throw error; + } +} + +/** + * Recursively copy directory, skipping existing files + */ +function copyDirectoryRecursive(src: string, dest: string): void { + if (!fs.existsSync(src)) { + return; + } + + const stat = fs.statSync(src); + + if (stat.isDirectory()) { + // Ensure target directory exists + fs.ensureDirSync(dest); + + // Read all contents of source directory + const entries = fs.readdirSync(src); + + for (const entry of entries) { + const srcPath = path.join(src, entry); + const destPath = path.join(dest, entry); + + const entryStat = fs.statSync(srcPath); + + if (entryStat.isDirectory()) { + // Recursively handle subdirectories + copyDirectoryRecursive(srcPath, destPath); + } else { + // Copy file if target doesn't exist + if (!fs.existsSync(destPath)) { + fs.copyFileSync(srcPath, destPath); + } + } + } + } else { + // If it's a file, copy directly (if target doesn't exist) + if (!fs.existsSync(dest)) { + fs.copyFileSync(src, dest); + } + } +} + +/** + * Prepare docs branch working directory + * 1. Clone docs branch to build/epub/hello-algo directory + * 2. Copy necessary files and directories (skip existing files) + */ +async function prepareDocsBranch(): Promise { + // Get project root directory (assuming running from epub directory, or current directory is project root) + const currentDir = process.cwd(); + let projectRoot: string; + + // If current directory is epub directory, go up to find project root + if (path.basename(currentDir) === 'epub') { + projectRoot = path.dirname(currentDir); + } else { + // Otherwise assume current directory is project root + projectRoot = currentDir; + } + + const buildEpubDir = path.join(projectRoot, 'build', 'epub', 'hello-algo'); + + console.log('Preparing docs branch working directory...'); + console.log(`Project root directory: ${projectRoot}`); + console.log(`Target directory: ${buildEpubDir}`); + + // Create build/epub directory (if it doesn't exist) + const buildDir = path.join(projectRoot, 'build', 'epub'); + fs.ensureDirSync(buildDir); + + // Get git repository URL + const repoUrl = getGitRemoteUrl(); + + // Check if build/epub/hello-algo directory already exists + if (fs.existsSync(buildEpubDir)) { + const gitDir = path.join(buildEpubDir, '.git'); + + // Check if it's a git repository + if (fs.existsSync(gitDir)) { + console.log('Detected existing git repository, using git pull to update...'); + try { + // First switch to docs branch (if not on docs branch) + execSync('git checkout docs', { + stdio: 'inherit', + cwd: buildEpubDir + }); + + // Execute git pull to update + execSync('git pull', { + stdio: 'inherit', + cwd: buildEpubDir + }); + + console.log('✓ docs branch update completed'); + } catch (error) { + console.error('Error: git pull update failed, trying to re-clone...'); + // If pull fails, delete directory and re-clone + fs.removeSync(buildEpubDir); + // Continue with clone logic below + } + } else { + // If directory exists but is not a git repository, delete and re-clone + console.log('Detected existing directory but not a git repository, deleting and re-cloning...'); + fs.removeSync(buildEpubDir); + } + } + + // If directory doesn't exist or has been deleted, execute clone + if (!fs.existsSync(buildEpubDir)) { + try { + console.log(`Cloning docs branch: ${repoUrl}`); + console.log(`Target: ${buildEpubDir}`); + + // Execute git clone, using shallow clone for speed + execSync(`git clone --branch docs --depth 1 "${repoUrl}" "${buildEpubDir}"`, { + stdio: 'inherit', + cwd: projectRoot + }); + + console.log('✓ docs branch clone completed'); + } catch (error) { + console.error('Error: Failed to clone docs branch'); + throw error; + } + } + + // Files and directories to copy + const itemsToCopy = ['mkdocs.yml', 'docs', 'en', 'ja', 'overrides', 'zh-hant']; + + console.log('\nStarting file copy...'); + + for (const item of itemsToCopy) { + const sourcePath = path.join(projectRoot, item); + const targetPath = path.join(buildEpubDir, item); + + // Check if source file/directory exists + if (!fs.existsSync(sourcePath)) { + console.log(`⚠️ Skipping ${item} (source file does not exist)`); + continue; + } + + try { + const stat = fs.statSync(sourcePath); + + if (stat.isDirectory()) { + // For directories, recursively copy, skipping existing files + copyDirectoryRecursive(sourcePath, targetPath); + console.log(`✓ Copied directory: ${item}`); + } else { + // For files, copy if target doesn't exist + if (!fs.existsSync(targetPath)) { + fs.copyFileSync(sourcePath, targetPath); + console.log(`✓ Copied file: ${item}`); + } else { + console.log(`⊘ Skipping ${item} (target file already exists)`); + } + } + } catch (error) { + console.error(`⚠️ Error copying ${item}:`, error instanceof Error ? error.message : String(error)); + // Continue processing other files + } + } + + console.log('\n✓ File copy completed'); + console.log(`Working directory: ${buildEpubDir}\n`); + + return buildEpubDir; +} + +const DOC_LANGUAGE_CONFIG: { [key: string]: DocLanguageConfig } = { + 'zh': { + mkdocsPath: 'mkdocs.yml', + docsDir: 'docs', // Relative to project root directory + title: 'Hello 算法', + description: '动画图解、一键运行的数据结构与算法教程', + language: 'zh-CN', + // Chinese version supports all languages + }, + 'zh-hant': { + mkdocsPath: 'zh-hant/mkdocs.yml', + docsDir: 'zh-hant/docs', // Relative to project root directory + title: 'Hello 演算法', + description: '動畫圖解、一鍵執行的資料結構與演算法教程', + language: 'zh-Hant', + // Traditional Chinese version supports all languages + }, + 'en': { + mkdocsPath: 'en/mkdocs.yml', + docsDir: 'en/docs', // Relative to project root directory + title: 'Hello Algo', + description: 'Data Structures and Algorithms Crash Course with Animated Illustrations and Off-the-Shelf Code', + language: 'en', + supportedCodeLanguages: ['cpp', 'java', 'python'], + }, + 'ja': { + mkdocsPath: 'ja/mkdocs.yml', + docsDir: 'ja/docs', // Relative to project root directory + title: 'Hello アルゴリズム', + description: 'アニメーションで図解、ワンクリック実行のデータ構造とアルゴリズムチュートリアル', + language: 'ja', + supportedCodeLanguages: ['cpp', 'java', 'python'], + } +}; + +/** + * Build a single EPUB file + */ +async function buildEpub( + docLanguage: string, + codeLanguage: string, + outputPath: string, + workDir: string, + validate: boolean = false +): Promise<{ success: boolean; error?: string }> { + try { + // Get document language configuration + const docConfig = DOC_LANGUAGE_CONFIG[docLanguage]; + if (!docConfig) { + return { success: false, error: `Unsupported document language: ${docLanguage}` }; + } + + // Verify if programming language is supported by this document language + if (docConfig.supportedCodeLanguages && !docConfig.supportedCodeLanguages.includes(codeLanguage)) { + return { success: false, error: `Document language "${docLanguage}" does not support programming language "${codeLanguage}"` }; + } + + // Config file path (workDir is now project root directory) + const mkdocsPath = path.resolve(workDir, docConfig.mkdocsPath); + + // Calculate project root directory (workDir is project root directory) + const repoDir = workDir; + + // Document directory (absolute path) + const docsDir = path.join(repoDir, docConfig.docsDir); + + // Check if directory exists + if (!fs.existsSync(docsDir)) { + return { success: false, error: `Document directory does not exist: ${docsDir}` }; + } + + if (!fs.existsSync(mkdocsPath)) { + return { success: false, error: `Config file does not exist: ${mkdocsPath}` }; + } + + // Parse configuration + const nav = parseMkdocsConfig(mkdocsPath); + // Parse version from mkdocs.yml + const version = parseVersion(mkdocsPath); + + // Flatten navigation structure + const chapters = flattenNav(nav, docsDir); + + // Read chapter content and extract headings + const allHeadings: HeadingInfo[] = []; + + for (const chapter of chapters) { + chapter.content = readChapterContent(chapter); + + // Extract all headings from this chapter + if (chapter.content.trim().length > 0) { + const headings = extractHeadings(chapter.content, chapter.path); + allHeadings.push(...headings); + } + } + + // Filter out empty content + const validChapters = chapters.filter(ch => ch.content.trim().length > 0); + + // Cover image path (select based on document language) + const epubDir = path.join(__dirname, '..'); + const coverMap: { [key: string]: string } = { + 'zh': path.join(epubDir, 'covers', 'hello-algo-cover-zh.jpg'), + 'zh-hant': path.join(epubDir, 'covers', 'hello-algo-cover-zh-hant.jpg'), + 'en': path.join(epubDir, 'covers', 'hello-algo-cover-en.jpg'), + 'ja': path.join(epubDir, 'covers', 'hello-algo-cover-en.jpg'), // Temporarily use English cover + }; + const coverPath = coverMap[docLanguage] || coverMap['zh']; + + // Generate EPUB + await generateEpub(validChapters, docsDir, outputPath, { + title: docConfig.title, + author: 'krahets', + publisher: 'Hello Algorithm', + description: docConfig.description, + language: docConfig.language, + cover: coverPath, + codeLanguage: codeLanguage, + docLanguage: docLanguage, + version: version, + }); + + // Validate EPUB content integrity (only execute when enabled) + if (validate) { + const validation = await validateEpubHeadings(outputPath, allHeadings); + + if (!validation.success) { + return { success: true, error: `Validation warning: Missing ${validation.missingHeadings.length} headings` }; + } + } + + return { success: true }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +} + +async function main() { + // Create command line program + const program = new Command(); + + program + .name('hello-algo-epub') + .description('Convert Hello Algorithm documentation to EPUB e-book') + .version('1.0.0') + .option('-d, --doc-language ', 'Document language (zh, zh-hant, en, ja)', 'zh') + .option('-o, --output ', 'Output EPUB file path') + .option('-l, --language ', `Programming language (${SUPPORTED_LANGUAGES.join(', ')})`, 'cpp') + .option('-a, --all', 'Build all combinations of document languages and programming languages') + .option('--validate', 'Enable EPUB content integrity validation', false) + .parse(process.argv); + + const options = program.opts(); + + // Get project root directory + const currentDir = process.cwd(); + let projectRoot: string; + if (path.basename(currentDir) === 'epub') { + projectRoot = path.dirname(currentDir); + } else { + projectRoot = currentDir; + } + + // Prepare docs branch working directory + let workDir: string; + try { + workDir = await prepareDocsBranch(); + } catch (error) { + console.error('Failed to prepare docs branch working directory:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } + + // If using --all, execute batch build + if (options.all) { + // Validation: Cannot use --all with other options + // Check if these options were explicitly provided in command line arguments + const args = process.argv; + const hasDocLanguage = args.includes('-d') || args.includes('--doc-language'); + const hasLanguage = args.includes('-l') || args.includes('--language'); + const hasOutput = args.includes('-o') || args.includes('--output'); + + if (hasDocLanguage || hasLanguage || hasOutput) { + console.error('Error: Cannot specify --doc-language, --language, or --output when using --all'); + process.exit(1); + } + + // Generate all combinations + const builds: Array<{ docLanguage: string; codeLanguage: string }> = []; + + for (const docLang of Object.keys(DOC_LANGUAGE_CONFIG)) { + const docConfig = DOC_LANGUAGE_CONFIG[docLang]; + const codeLangs = docConfig.supportedCodeLanguages || SUPPORTED_LANGUAGES; + + for (const codeLang of codeLangs) { + builds.push({ docLanguage: docLang, codeLanguage: codeLang }); + } + } + + console.log(`Starting batch build of ${builds.length} EPUB files...\n`); + + const results: Array<{ docLanguage: string; codeLanguage: string; success: boolean; error?: string; outputPath: string }> = []; + + for (let i = 0; i < builds.length; i++) { + const { docLanguage, codeLanguage } = builds[i]; + const outputPath = generateOutputFilename(docLanguage, codeLanguage, workDir, projectRoot); + + console.log(`[${i + 1}/${builds.length}] Building: ${docLanguage}-${codeLanguage}`); + + const result = await buildEpub(docLanguage, codeLanguage, outputPath, workDir, options.validate); + + if (result.success) { + console.log(` ✅ Success: ${path.basename(outputPath)}`); + if (result.error) { + console.log(` ⚠️ ${result.error}`); + } + } else { + console.log(` ❌ Failed: ${result.error}`); + } + console.log(); + + results.push({ + docLanguage, + codeLanguage, + success: result.success, + error: result.error, + outputPath + }); + } + + // Display statistics + const successCount = results.filter(r => r.success).length; + const failCount = results.length - successCount; + + console.log('='.repeat(60)); + console.log('Build completed!'); + console.log(`Success: ${successCount}/${results.length}`); + console.log(`Failed: ${failCount}/${results.length}`); + console.log('='.repeat(60)); + + if (failCount > 0) { + console.log('\nFailed builds:'); + results.filter(r => !r.success).forEach(r => { + console.log(` - ${r.docLanguage}-${r.codeLanguage}: ${r.error}`); + }); + process.exit(1); + } + + return; + } + + // Single build logic + // Validate document language parameter + const supportedDocLanguages = Object.keys(DOC_LANGUAGE_CONFIG); + if (!supportedDocLanguages.includes(options.docLanguage)) { + console.error(`Error: Unsupported document language "${options.docLanguage}"`); + console.error(`Supported document languages: ${supportedDocLanguages.join(', ')}`); + process.exit(1); + } + + // Validate programming language parameter + if (!SUPPORTED_LANGUAGES.includes(options.language)) { + console.error(`Error: Unsupported programming language "${options.language}"`); + console.error(`Supported programming languages: ${SUPPORTED_LANGUAGES.join(', ')}`); + process.exit(1); + } + + // Get document language configuration + const docConfig = DOC_LANGUAGE_CONFIG[options.docLanguage]; + + // Validate combination of document language and programming language + if (docConfig.supportedCodeLanguages && !docConfig.supportedCodeLanguages.includes(options.language)) { + console.error(`Error: Document language "${options.docLanguage}" does not support programming language "${options.language}"`); + console.error(`${options.docLanguage} supported programming languages: ${docConfig.supportedCodeLanguages.join(', ')}`); + process.exit(1); + } + + // If output path not provided, auto-generate filename + const outputDir = path.join(projectRoot, 'build', 'epub', 'outputs'); + fs.ensureDirSync(outputDir); + + const outputPath = options.output + ? path.join(outputDir, path.basename(options.output)) + : path.join(outputDir, `hello-algo_${options.docLanguage}_${options.language}.epub`); + + console.log('Starting document processing...'); + console.log(`Document language: ${options.docLanguage}`); + console.log(`Programming language: ${options.language}`); + console.log(`Output file: ${outputPath}`); + console.log(); + + const result = await buildEpub(options.docLanguage, options.language, outputPath, workDir, options.validate); + + if (!result.success) { + console.error(`Build failed: ${result.error}`); + process.exit(1); + } + + if (result.error) { + console.warn(`⚠️ ${result.error}`); + } + + console.log('Completed!'); +} + +main().catch((error) => { + console.error('An error occurred:', error); + process.exit(1); +}); + diff --git a/epub/src/markdown.ts b/epub/src/markdown.ts new file mode 100644 index 000000000..20165f5be --- /dev/null +++ b/epub/src/markdown.ts @@ -0,0 +1,1457 @@ +import { marked } from 'marked'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { HeadingInfo } from './types'; +import { ImageInfo } from './types'; +import hljs from 'highlight.js'; + +/** + * Language configuration mapping + */ +interface LanguageConfig { + displayName: string; // Display name used in === "Language Name" + dirName: string; // Subdirectory name under codes/ directory + extension: string; // File extension + useSnakeCase: boolean; // Whether to use snake_case naming (false means camelCase) +} + +const LANGUAGE_MAP: { [key: string]: LanguageConfig } = { + cpp: { displayName: 'C++', dirName: 'cpp', extension: '.cpp', useSnakeCase: false }, + python: { displayName: 'Python', dirName: 'python', extension: '.py', useSnakeCase: true }, + java: { displayName: 'Java', dirName: 'java', extension: '.java', useSnakeCase: false }, + csharp: { displayName: 'C#', dirName: 'csharp', extension: '.cs', useSnakeCase: false }, + go: { displayName: 'Go', dirName: 'go', extension: '.go', useSnakeCase: false }, + swift: { displayName: 'Swift', dirName: 'swift', extension: '.swift', useSnakeCase: false }, + javascript: { displayName: 'JS', dirName: 'javascript', extension: '.js', useSnakeCase: false }, + typescript: { displayName: 'TS', dirName: 'typescript', extension: '.ts', useSnakeCase: false }, + dart: { displayName: 'Dart', dirName: 'dart', extension: '.dart', useSnakeCase: false }, + rust: { displayName: 'Rust', dirName: 'rust', extension: '.rs', useSnakeCase: true }, + c: { displayName: 'C', dirName: 'c', extension: '.c', useSnakeCase: true }, + kotlin: { displayName: 'Kotlin', dirName: 'kotlin', extension: '.kt', useSnakeCase: false }, + ruby: { displayName: 'Ruby', dirName: 'ruby', extension: '.rb', useSnakeCase: true }, + zig: { displayName: 'Zig', dirName: 'zig', extension: '.zig', useSnakeCase: true }, +}; + +/** + * Map markdown code block language identifiers to highlight.js supported language identifiers + * This mapping ensures code highlighting works correctly + */ +function normalizeLanguageForHighlight(lang: string | undefined): string { + if (!lang || lang.trim() === '') { + return ''; + } + + // Remove title attribute (if exists) + let normalized = lang.replace(/\s+title="[^"]*"/, '').trim().toLowerCase(); + + // Language identifier mapping table: markdown identifier -> highlight.js identifier + const langMap: { [key: string]: string } = { + 'py': 'python', + 'python': 'python', + 'cpp': 'cpp', + 'c++': 'cpp', + 'cxx': 'cpp', + 'java': 'java', + 'cs': 'csharp', + 'csharp': 'csharp', + 'c#': 'csharp', + 'go': 'go', + 'golang': 'go', + 'swift': 'swift', + 'js': 'javascript', + 'javascript': 'javascript', + 'ts': 'typescript', + 'typescript': 'typescript', + 'dart': 'dart', + 'rust': 'rust', + 'rs': 'rust', + 'c': 'c', + 'kt': 'kotlin', + 'kotlin': 'kotlin', + 'rb': 'ruby', + 'ruby': 'ruby', + 'zig': 'zig', + }; + + // If exists in mapping table, return mapped value + if (langMap[normalized]) { + return langMap[normalized]; + } + + // Otherwise return original value (may be supported by highlight.js) + return normalized; +} + +/** + * Get comment prefix based on programming language + */ +function getCommentPrefix(lang: string): string { + // Python uses # + if (lang === 'python' || lang === 'py') { + return '#'; + } + // Other languages use // + return '//'; +} + +/** + * Configure marked renderer + */ +function configureMarked() { + const renderer = new marked.Renderer(); + + // Custom heading rendering (heading numbers already provided by MD documents) + renderer.heading = (text: string, level: number) => { + // Generate anchor ID (remove special characters and spaces) + const id = text + .replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '') // Keep Chinese, letters, numbers and spaces + .trim() + .replace(/\s+/g, '-') // Replace spaces with hyphens + .toLowerCase(); + return `${text}\n`; + }; + + // Custom link rendering, remove links to .md files (invalid in EPUB) + renderer.link = (href: string, title: string | null, text: string) => { + // If link points to .md file, only return text content (remove hyperlink) + if (href && href.endsWith('.md')) { + return text; + } + // Other links remain unchanged + const titleAttr = title ? ` title="${title}"` : ''; + return `${text}`; + }; + + // Custom image rendering, for later image extraction + renderer.image = (href: string, title: string | null, text: string) => { + return `${text || ''}`; + }; + + // Custom code block rendering, supports title attribute and syntax highlighting + renderer.code = (code: string, language: string | undefined, escaped: boolean) => { + let lang = language || ''; + let highlightedCode = code; + let filename: string | null = null; + + // Extract title from language parameter (format: cpp title="heap.cpp") + // marked passes the entire infostring as the language parameter + if (language) { + const titleMatch = language.match(/title="([^"]+)"/); + if (titleMatch) { + filename = titleMatch[1]; + } + } + + // Normalize language identifier to ensure compatibility with highlight.js + const normalizedLang = normalizeLanguageForHighlight(lang); + + // Use highlight.js for syntax highlighting + try { + if (normalizedLang && normalizedLang !== '' && hljs.getLanguage(normalizedLang)) { + // Use normalized language identifier for highlighting + const result = hljs.highlight(code, { language: normalizedLang }); + highlightedCode = result.value; + lang = normalizedLang; + } else { + // If language not supported, use auto-detection + const result = hljs.highlightAuto(code); + highlightedCode = result.value; + lang = result.language || normalizedLang || 'text'; + } + } catch (error) { + console.warn(`Syntax highlighting failed, using original code: ${error}`); + highlightedCode = escaped ? code : escapeHtml(code); + lang = normalizedLang || 'text'; + } + + // If has filename, add filename as comment at the beginning of code + if (filename && filename.trim() !== '') { + const commentPrefix = getCommentPrefix(lang); + const codeWithFilename = `${commentPrefix} ${escapeHtml(filename)}\n\n${highlightedCode}`; + return `
${codeWithFilename}
`; + } + + // Normal code block without filename + return `
${highlightedCode}
`; + }; + + // Custom table rendering, maintain original format + renderer.table = (header: string, body: string) => { + return `\n\n${header}\n\n${body}\n
`; + }; + + marked.setOptions({ + renderer, + gfm: true, + breaks: false, + pedantic: false, + }); +} + +function escapeHtml(text: string): string { + const map: { [key: string]: string } = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return text.replace(/[&<>"']/g, (m) => map[m]); +} + +/** + * Remove YAML frontmatter (--- ... --- block at the beginning of file) + */ +function removeYamlFrontmatter(markdown: string): string { + // Remove YAML frontmatter at the beginning of file (--- ... ---) + return markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, ''); +} + +/** + * Remove all end-of-line attribute block markers (like {data-toc-label="..."} or { class="animation-figure" }) + */ +function removeAttributeBlocks(markdown: string): string { + // Only remove "attribute block-like" braces: + // - Contains "=" inside (like data-toc-label="...", class="...", etc.) + // - Avoid damaging LaTeX syntax (e.g., \begin{aligned}, \text{...} that don't contain "=") + // Note: Only process in non-code block areas to avoid mistakenly deleting code block content + const lines = markdown.split('\n'); + const result: string[] = []; + let inCodeBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // 检查是否是代码块开始/结束标记 + if (line.match(/^\s*```/)) { + inCodeBlock = !inCodeBlock; + result.push(line); + continue; + } + + // If inside code block, keep original line without removing attribute blocks + if (inCodeBlock) { + result.push(line); + continue; + } + + // Only remove end-of-line attribute blocks in non-code block areas + // Precise matching: only remove specific format attribute blocks + // Matching formats: + // - {data-toc-label="..."} + // - { class="animation-figure" } + // - {class="cover-image"} + // Use more precise regex, only match common HTML/Markdown attribute names + // Match: optional space + { + optional space + attribute name + optional space + = + optional space + "value" + optional space + } + optional space + end of line + const cleanedLine = line.replace(/\s*\{\s*(?:data-toc-label|class)\s*=\s*"[^"]*"\s*\}\s*$/, ''); + result.push(cleanedLine); + } + + return result.join('\n'); +} + +/** + * Decrease heading levels (lower all headings by one level) + * Skip content in code blocks + */ +function decreaseHeadingLevels(markdown: string): string { + const lines = markdown.split('\n'); + const result: string[] = []; + let inCodeBlock = false; + + for (const line of lines) { + // 检查是否是代码块开始/结束标记 + if (line.match(/^\s*```/)) { + inCodeBlock = !inCodeBlock; + result.push(line); + continue; + } + + // If inside code block, keep original line + if (inCodeBlock) { + result.push(line); + continue; + } + + // Match heading line: ^#{1,6}\s+ + const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + const level = headingMatch[1].length; + const content = headingMatch[2]; + + // If already H6, keep unchanged; otherwise increase one level + if (level < 6) { + result.push('#' + headingMatch[1] + ' ' + content); + } else { + result.push(line); + } + } else { + result.push(line); + } + } + + return result.join('\n'); +} + +/** + * Convert Markdown to HTML + * @param markdown Markdown text + * @param baseDir Base directory + * @param language Programming language (default 'cpp') + * @param docLanguage Document language (optional, for admonition title localization: zh, zh-hant, en, ja) + * @param filePath File path (optional, used to determine if it's index.md) + */ +export function markdownToHtml(markdown: string, baseDir: string, language: string = 'cpp', docLanguage?: string, filePath?: string): string { + configureMarked(); + + // First clean up unwanted markers + markdown = removeYamlFrontmatter(markdown); + markdown = removeAttributeBlocks(markdown); + + // If not index.md file, decrease heading levels + if (filePath && path.basename(filePath, '.md') !== 'index') { + markdown = decreaseHeadingLevels(markdown); + } + + // Process multi-language code blocks, only keep specified language version + markdown = processMultiLanguageCodeBlocks(markdown, language); + + // Process tabbed content (=== "<1>", === "ArrayStack" etc. non-programming language tabs) + markdown = processTabbedContent(markdown); + + // Process special admonition syntax (!!! abstract, !!! success, etc.) + const admonitionResult = processAdmonitions(markdown, docLanguage); + markdown = admonitionResult.markdown; + const admonitionPlaceholders = admonitionResult.placeholders; + + // Support

syntax, convert it to use CSS class + markdown = markdown.replace(//g, '

'); + + // Use placeholders to protect math formulas from being escaped by marked + // Use pure alphanumeric placeholders (avoid special characters being parsed by Markdown) + const mathPlaceholders = new Map(); + let placeholderCounter = 0; + + // 先提取块级公式 $$...$$ + markdown = markdown.replace(/\$\$([\s\S]+?)\$\$/g, (match, formula) => { + const placeholder = `XMATHBLOCKX${placeholderCounter}X`; + const rendered = processDisplayMath(formula); + mathPlaceholders.set(placeholder, rendered); + placeholderCounter++; + return placeholder; + }); + + // 再提取行内公式 $...$ + markdown = markdown.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { + const placeholder = `XMATHINLINEX${placeholderCounter}X`; + const rendered = processInlineMath(formula); + mathPlaceholders.set(placeholder, rendered); + placeholderCounter++; + return placeholder; + }); + + // Parse Markdown + let html = marked.parse(markdown) as string; + + // 替换回数学公式的 HTML + for (const [placeholder, rendered] of mathPlaceholders.entries()) { + html = html.replace(new RegExp(placeholder, 'g'), rendered); + } + + // Parse and replace back admonition content placeholders + for (const [placeholder, content] of admonitionPlaceholders.entries()) { + // Process math formulas in content + let processedContent = content; + const contentMathPlaceholders = new Map(); + let contentPlaceholderCounter = 0; + + // First extract block-level formulas $$...$$ + processedContent = processedContent.replace(/\$\$([\s\S]+?)\$\$/g, (match, formula) => { + const mathPlaceholder = `XMATHBLOCKX${contentPlaceholderCounter}X`; + const rendered = processDisplayMath(formula); + contentMathPlaceholders.set(mathPlaceholder, rendered); + contentPlaceholderCounter++; + return mathPlaceholder; + }); + + // Then extract inline formulas $...$ + processedContent = processedContent.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { + const mathPlaceholder = `XMATHINLINEX${contentPlaceholderCounter}X`; + const rendered = processInlineMath(formula); + contentMathPlaceholders.set(mathPlaceholder, rendered); + contentPlaceholderCounter++; + return mathPlaceholder; + }); + + // Parse Markdown in content + let parsedContent = marked.parse(processedContent) as string; + + // Replace back math formula HTML + for (const [mathPlaceholder, rendered] of contentMathPlaceholders.entries()) { + parsedContent = parsedContent.replace(new RegExp(mathPlaceholder, 'g'), rendered); + } + + html = html.replace(new RegExp(placeholder, 'g'), parsedContent); + } + + return wrapHtmlContent(html); +} + +/** + * Process tabbed content (non-programming language tabs, such as step descriptions, different implementation methods, etc.) + * All this content should be preserved and converted to appropriate HTML format + */ +function processTabbedContent(markdown: string): string { + // Match === "non-programming language tags" content blocks + // These tags include: <1>, <2>, ..., ArrayStack, LinkedListStack, push(), pop(), etc. + const lines = markdown.split('\n'); + const result: string[] = []; + let i = 0; + + // Define programming language tags (these should not be processed by processTabbedContent) + const programmingLanguages = [ + 'Python', 'C++', 'Java', 'C#', 'Go', 'Swift', 'JS', 'TS', + 'Dart', 'Rust', 'C', 'Kotlin', 'Ruby', 'Zig' + ]; + + while (i < lines.length) { + const line = lines[i]; + + // Check if it's a tag line + const tabMatch = line.match(/^===\s+"([^"]+)"/); + + // Only process non-programming language tags + if (tabMatch && !programmingLanguages.includes(tabMatch[1])) { + const tabName = tabMatch[1]; + + i++; + + // Collect all indented content under this tag + while (i < lines.length) { + const contentLine = lines[i]; + + // If encounter next === tag, end current tag content + if (contentLine.match(/^===/)) { + break; + } + + // If encounter non-indented heading or paragraph, end current tag content + if (contentLine.trim() && !contentLine.match(/^\s/) && contentLine.match(/^[^#\s]/)) { + break; + } + + // Remove 4 space indentation (MkDocs tabbed content usually has 4 space indentation) + result.push(contentLine.replace(/^ /, '')); + i++; + } + + continue; + } + + // 普通行,直接添加 + result.push(line); + i++; + } + + return result.join('\n'); +} + +function processMultiLanguageCodeBlocks(markdown: string, language: string = 'cpp'): string { + // Get target language display name + const langConfig = LANGUAGE_MAP[language]; + const targetLangName = langConfig ? langConfig.displayName : 'C++'; + + // Define programming language tags to filter (only these tags will be treated as multi-language code blocks) + const programmingLanguages = [ + 'Python', 'C++', 'Java', 'C#', 'Go', 'Swift', 'JS', 'TS', + 'Dart', 'Rust', 'C', 'Kotlin', 'Ruby', 'Zig' + ]; + + // Match entire multi-language code block group (from first === to next non-indented line or file end) + // Use more precise matching: consecutive === "Language" blocks until encountering non-indented line + const lines = markdown.split('\n'); + const result: string[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Check if it's a language tag line + const langMatch = line.match(/^===\s+"([^"]+)"/); + + // Only process programming language tags, other tags (like <1>, <2>, ArrayStack, etc.) remain unchanged + if (langMatch && programmingLanguages.includes(langMatch[1])) { + // Found the start of a multi-language code block group + const blocks: Array<{ lang: string; lines: string[] }> = []; + let currentLang = langMatch[1]; + let currentBlockLines: string[] = []; + let inCodeBlock = false; + + // Collect all language blocks + i++; // Skip first language tag line + + while (i < lines.length) { + const currentLine = lines[i]; + + // Check if it's next language tag (must be outside code block) + if (!inCodeBlock) { + const nextLangMatch = currentLine.match(/^===\s+"([^"]+)"/); + if (nextLangMatch) { + // Only continue processing when it's a programming language tag + if (programmingLanguages.includes(nextLangMatch[1])) { + // Save current block + if (currentBlockLines.length > 0) { + blocks.push({ lang: currentLang, lines: currentBlockLines }); + } + // Start new block + currentLang = nextLangMatch[1]; + currentBlockLines = []; + i++; + continue; + } else { + // Encounter non-programming language tag (like <1>, ArrayStack, etc.), code block group ends + if (currentBlockLines.length > 0) { + blocks.push({ lang: currentLang, lines: currentBlockLines }); + } + break; + } + } + } + + // Check if it's code block start/end (match any indentation ```) + + if (currentLine.match(/^\s*```/)) { + inCodeBlock = !inCodeBlock; + currentBlockLines.push(currentLine); + i++; + continue; + } + + // When inside code block, all content should be collected + if (inCodeBlock) { + currentBlockLines.push(currentLine); + i++; + continue; + } + + // When outside code block, check if code block group ends + // If encounter non-indented non-empty line (and not next language tag), code block group ends + if (currentLine.trim()) { + // If it's heading line (starts with #), code block group ends + if (currentLine.match(/^#/)) { + // Heading line, code block group ends + if (currentBlockLines.length > 0) { + blocks.push({ lang: currentLang, lines: currentBlockLines }); + } + break; + } else if (!currentLine.match(/^\s/) && !currentLine.match(/^===/) && !currentLine.match(/^\?\?\?/) && !currentLine.match(/^```/)) { + // Normal paragraph (non-indented, non-language tag, non-admonition, non-code block), code block group ends + if (currentBlockLines.length > 0) { + blocks.push({ lang: currentLang, lines: currentBlockLines }); + } + break; + } + } + + // Add to current block (including empty lines and indented lines outside code block) + currentBlockLines.push(currentLine); + i++; + } + + // If loop ends, save last block + if (currentBlockLines.length > 0) { + blocks.push({ lang: currentLang, lines: currentBlockLines }); + } + + // Find target language version + const targetBlock = blocks.find(b => b.lang === targetLangName); + + if (targetBlock) { + // Only keep target language version code block (remove indentation) + for (const blockLine of targetBlock.lines) { + // Remove leading 4 space indentation + result.push(blockLine.replace(/^ /, '')); + } + } else if (blocks.length > 0) { + // If no target language version, keep first version + for (const blockLine of blocks[0].lines) { + result.push(blockLine.replace(/^ /, '')); + } + } + + // i has already been updated in the loop, continue processing next line + continue; + } + + // 普通行,直接添加 + result.push(line); + i++; + } + + return result.join('\n'); +} + +/** + * Process inline math formulas + */ +function processInlineMath(formula: string): string { + const content = processMathContent(formula.trim()); + return `${content}`; +} + +/** + * Process block-level math formulas + */ +function processDisplayMath(formula: string): string { + let result = formula.trim(); + + // Process aligned environment + if (result.includes('\\begin{aligned}')) { + result = result.replace(/\\begin\{aligned\}([\s\S]*?)\\end\{aligned\}/g, (match, content) => { + return processAlignedMath(content); + }); + } else { + result = processMathContent(result); + } + + return `

${result}
`; +} + +/** + * Process aligned math environment + */ +function processAlignedMath(content: string): string { + // Process \newline + let aligned = content.trim().replace(/\\newline/g, '\n'); + + // Split by line + const lines = aligned.split('\n').filter((line: string) => line.trim()); + + if (lines.length === 0) { + return processMathContent(content); + } + + // Process each line + const processedLines = lines.map((line: string) => { + if (line.includes('&')) { + // Has alignment symbol, split by & + return line.split('&').map((part: string) => processMathContent(part.trim())); + } else { + // No alignment symbol + return [processMathContent(line.trim())]; + } + }); + + // Build table + let html = ''; + for (const line of processedLines) { + html += ''; + for (let i = 0; i < line.length; i++) { + const cell = line[i] || ''; + const align = i === 0 ? 'right' : 'left'; + html += ``; + } + html += ''; + } + html += '
${cell}
'; + + return html; +} + +/** + * Process math formula content (LaTeX to HTML) + */ +function processMathContent(latex: string): string { + let result = latex.trim(); + + // First process text and roman font (before processing superscripts/subscripts to avoid nested braces issue) + result = result.replace(/\\text\s*\{([^}]+)\}/g, '$1'); + result = result.replace(/\\mathrm\s*\{([^}]+)\}/g, '$1'); + + // Process function names + result = result.replace(/\\log/g, 'log'); + result = result.replace(/\\ln/g, 'ln'); + result = result.replace(/\\sin/g, 'sin'); + result = result.replace(/\\cos/g, 'cos'); + result = result.replace(/\\tan/g, 'tan'); + + // Process subscripts + result = result.replace(/_\{([^}]+)\}/g, '$1'); + result = result.replace(/_([a-zA-Z0-9])/g, '$1'); + + // Process superscripts + result = result.replace(/\^\{([^}]+)\}/g, '$1'); + result = result.replace(/\^([a-zA-Z0-9])/g, '$1'); + + // Process fractions + result = result.replace(/\\frac\s*\{([^}]+)\}\s*\{([^}]+)\}/g, '$1⁄$2'); + + // Process square root + result = result.replace(/\\sqrt\s*\{([^}]+)\}/g, '√$1'); + + // Process Greek letters + const greekMap: { [key: string]: string } = { + '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ', + '\\epsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ', '\\mu': 'μ', + '\\pi': 'π', '\\sigma': 'σ', '\\tau': 'τ', '\\phi': 'φ', + '\\omega': 'ω', '\\Theta': 'Θ', '\\Omega': 'Ω' + }; + + for (const [tex, unicode] of Object.entries(greekMap)) { + result = result.replace(new RegExp(tex.replace(/\\/g, '\\\\'), 'g'), unicode); + } + + // Process special symbols + result = result.replace(/\\times/g, '×'); + result = result.replace(/\\div/g, '÷'); + result = result.replace(/\\cdot/g, '·'); + result = result.replace(/\\ast/g, '∗'); + result = result.replace(/\\pm/g, '±'); + result = result.replace(/\\mp/g, '∓'); + result = result.replace(/\\leq/g, '≤'); + result = result.replace(/\\geq/g, '≥'); + result = result.replace(/\\neq/g, '≠'); + result = result.replace(/\\ne/g, '≠'); + result = result.replace(/\\approx/g, '≈'); + result = result.replace(/\\equiv/g, '≡'); + result = result.replace(/\\sim/g, '∼'); + result = result.replace(/\\propto/g, '∝'); + result = result.replace(/\\infty/g, '∞'); + result = result.replace(/\\sum/g, '∑'); + result = result.replace(/\\prod/g, '∏'); + result = result.replace(/\\int/g, '∫'); + result = result.replace(/\\dots/g, '...'); + result = result.replace(/\\ldots/g, '…'); + result = result.replace(/\\lfloor/g, '⌊'); + result = result.replace(/\\rfloor/g, '⌋'); + result = result.replace(/\\lceil/g, '⌈'); + result = result.replace(/\\rceil/g, '⌉'); + result = result.replace(/\\in/g, '∈'); + result = result.replace(/\\notin/g, '∉'); + result = result.replace(/\\subset/g, '⊂'); + result = result.replace(/\\supset/g, '⊃'); + result = result.replace(/\\subseteq/g, '⊆'); + result = result.replace(/\\supseteq/g, '⊇'); + result = result.replace(/\\cup/g, '∪'); + result = result.replace(/\\cap/g, '∩'); + result = result.replace(/\\emptyset/g, '∅'); + result = result.replace(/\\forall/g, '∀'); + result = result.replace(/\\exists/g, '∃'); + result = result.replace(/\\land/g, '∧'); + result = result.replace(/\\lor/g, '∨'); + result = result.replace(/\\neg/g, '¬'); + + // Process space commands + result = result.replace(/\\quad/g, ' '); + result = result.replace(/\\qquad/g, ' '); + result = result.replace(/\\,/g, ' '); + result = result.replace(/\\;/g, ' '); + + // Process arrow symbols + result = result.replace(/\\rightarrow/g, '→'); + result = result.replace(/\\leftarrow/g, '←'); + result = result.replace(/\\Rightarrow/g, '⇒'); + result = result.replace(/\\Leftarrow/g, '⇐'); + result = result.replace(/\\leftrightarrow/g, '↔'); + result = result.replace(/\\Leftrightarrow/g, '⇔'); + result = result.replace(/\\to/g, '→'); + result = result.replace(/\\gets/g, '←'); + + // Process LaTeX bracket commands (remove \left and \right directly, keep brackets themselves) + result = result.replace(/\\left\s*/g, ''); + result = result.replace(/\\right\s*/g, ''); + + // Process escaped braces (use placeholders to protect) + result = result.replace(/\\\{/g, 'LEFTBRACE'); + result = result.replace(/\\\}/g, 'RIGHTBRACE'); + + // Clean brackets and backslashes + result = result.replace(/\{|\}/g, ''); + result = result.replace(/\\/g, ''); + + // Restore escaped braces + result = result.replace(/LEFTBRACE/g, '{'); + result = result.replace(/RIGHTBRACE/g, '}'); + + // Clean consecutive extra spaces (but preserve spaces inside HTML tags) + result = result.replace(/(?]*)\s{2,}(?![^<]*>)/g, ' '); + + // No longer convert letters to italics, keep original letters + + return result; +} + + +/** + * Process MkDocs admonition syntax + * Supports two formats: + * 1. !!! note (use default title) + * 2. !!! note "Custom title" (use custom title) + */ +function processAdmonitions(markdown: string, docLanguage?: string): { markdown: string; placeholders: Map } { + const lines = markdown.split('\n'); + const result: string[] = []; + const placeholders = new Map(); + let placeholderCounter = 0; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Check if it's admonition start + const admonitionMatch = line.match(/^!!!\s+(\w+)(?:\s+"([^"]+)")?\s*$/); + + if (admonitionMatch) { + const type = admonitionMatch[1]; + const customTitle = admonitionMatch[2]; + const title = customTitle || getAdmonitionTitle(type, docLanguage); + + i++; // Skip admonition marker line + + // Collect all indented content (starting with 4 spaces) + const contentLines: string[] = []; + while (i < lines.length) { + const contentLine = lines[i]; + + // If it's indented line (starting with 4 spaces), belongs to admonition content + if (contentLine.match(/^ /)) { + // Remove first 4 spaces + contentLines.push(contentLine.substring(4)); + i++; + } + // If it's empty line, may also belong to admonition content (need to check next line) + else if (contentLine.trim() === '') { + // Look ahead, if next line is still indented, this empty line belongs to admonition + if (i + 1 < lines.length && lines[i + 1].match(/^ /)) { + contentLines.push(''); + i++; + } else { + // Otherwise admonition ends + break; + } + } + // Non-indented non-empty line, admonition ends + else { + break; + } + } + + // Generate admonition HTML structure, content part uses placeholder + const content = contentLines.join('\n').trim(); + const placeholder = `XADMONITIONCONTENTX${placeholderCounter}X`; + placeholders.set(placeholder, content); + placeholderCounter++; + + result.push(`
`); + // If title is empty, don't display title + if (title) { + result.push(`
${title}
`); + } + result.push(placeholder); + result.push(`
`); + + continue; + } + + // 普通行 + result.push(line); + i++; + } + + return { markdown: result.join('\n'), placeholders }; +} + +function getAdmonitionTitle(type: string, docLanguage?: string): string { + const lang = docLanguage || 'zh'; + + const titles: { [key: string]: { [key: string]: string } } = { + 'zh': { + abstract: '摘要', + success: '成功', + info: '信息', + warning: '警告', + danger: '危险', + note: '注意', + tip: '提示', + question: '问题', + quote: '引用', + }, + 'zh-hant': { + abstract: '摘要', + success: '成功', + info: '資訊', + warning: '警告', + danger: '危險', + note: '注意', + tip: '提示', + question: '問題', + quote: '引用', + }, + 'en': { + abstract: 'Summary', + success: 'Success', + info: 'Info', + warning: 'Warning', + danger: 'Danger', + note: 'Note', + tip: 'Hint', + question: 'Question', + quote: 'Quote', + }, + 'ja': { + abstract: '要約', + success: '成功', + info: '情報', + warning: '警告', + danger: '危険', + note: '注意', + tip: 'ヒント', + question: '質問', + quote: '引用', + }, + }; + + // If type is in dictionary, use dictionary value; if dictionary value is empty string, it means this type doesn't need a title + // If type is not in dictionary, return type itself + const title = titles[lang]?.[type]; + if (title !== undefined) { + return title; + } + const fallbackTitle = titles['zh']?.[type]; + if (fallbackTitle !== undefined) { + return fallbackTitle; + } + return type; +} + +/** + * Get custom CSS styles + */ +export function getCustomCSS(docLanguage?: string): string { + const lang = docLanguage || 'zh'; + + // Select font configuration based on document language + let serifFontName = 'Noto Serif SC'; + let serifFontFile = 'NotoSerifSC-Regular.ttf'; + let serifItalicFontFile: string | null = null; + let bodyFontFamily = '"Noto Serif SC", "Noto Serif CJK SC", "Source Han Serif SC", serif'; + + if (lang === 'en') { + serifFontName = 'Roboto Serif'; + serifFontFile = 'RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf'; + serifItalicFontFile = 'RobotoSerif-Italic-VariableFont_GRAD,opsz,wdth,wght.ttf'; + bodyFontFamily = '"Roboto Serif", serif'; + } else if (lang === 'ja') { + serifFontName = 'Noto Serif JP'; + serifFontFile = 'NotoSerifJP-VariableFont_wght.ttf'; + bodyFontFamily = '"Noto Serif JP", "Noto Serif CJK JP", serif'; + } else if (lang === 'zh' || lang === 'zh-hant') { + // Use default Noto Serif SC Regular + } + + return ` + /* Define embedded fonts */ + @font-face { + font-family: "MathJax_Math"; + src: url(./fonts/MathJax_Math-Regular.otf); + font-style: normal; + font-weight: normal; + } + @font-face { + font-family: "MathJax_Main"; + src: url(./fonts/MathJax_Main-Regular.otf); + font-style: normal; + font-weight: normal; + } + @font-face { + font-family: "${serifFontName}"; + src: url(./fonts/${serifFontFile}); + font-style: normal; + font-weight: normal; + } + ${serifItalicFontFile ? `@font-face { + font-family: "${serifFontName}"; + src: url(./fonts/${serifItalicFontFile}); + font-style: italic; + font-weight: normal; + }` : ''} + @font-face { + font-family: "JetBrains Mono"; + src: url(./fonts/JetBrainsMonoNerdFont-Regular.ttf); + font-style: normal; + font-weight: normal; + } + + body { + font-family: ${bodyFontFamily}; + line-height: 1.6; + max-width: 800px; + margin: 0 auto; + padding: 20px; + color: #333; + } + /* Link styles */ + a { + color: #1581CB; + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + /* Center-aligned paragraphs (compatible with

syntax, converted to .text-center) */ + .text-center { + text-align: center; + } + /* Reduce bold text font weight */ + strong, b { + font-weight: 600; + } + h1, h2, h3, h4, h5, h6 { + color: #24292e; + margin-top: 1.2em; + margin-bottom: 0.6em; + font-weight: 600; + line-height: 1.25; + } + /* Chapter title (Chapter X) */ + h1 { + font-size: 1.5em; + text-align: center; + } + /* Section title (X.Y) */ + h2 { font-size: 1.3em; } + /* Main heading within Markdown document */ + h3 { font-size: 1.15em; } + /* Subheading within Markdown document */ + h4 { font-size: 1.05em; } + h5 { font-size: 1.0em; } + h6 { font-size: 1.0em; } + /* Inline code */ + code { + background-color: #f0f0f0; + padding: 2px 6px; + border-radius: 3px; + border: 1px solid #d0d0d0; + font-family: "JetBrains Mono", "Consolas", "Monaco", "Courier New", monospace; + font-size: 0.82em; + color: #333; + } + + /* Code blocks */ + pre { + padding: 3px 3px; + background-color: #f5f5f5; + border: 1.5px solid #d0d0d0; + border-radius: 6px; + margin: 16px 0; + line-height: 1.65; + page-break-inside: auto !important; + break-inside: auto !important; + } + + pre code { + background-color: #f5f5f5; + padding: 14px 16px; + border: none; + white-space: pre-wrap; + word-wrap: break-word; + word-break: break-all; + overflow-wrap: break-word; + font-family: "JetBrains Mono", "Consolas", "Monaco", "Courier New", monospace; + font-size: 0.78em; + color: #24292e; + display: block; + } + img { + max-width: 100%; + height: auto; + display: block; + margin: 20px auto; + border-radius: 8px; + } + blockquote { + border-left: 4px solid #3498db; + margin: 0; + padding-left: 20px; + color: #666; + } + table { + border-collapse: collapse; + width: 100%; + margin: 20px 0; + border: 1px solid #ddd; + font-size: 0.8em; + } + th, td { + border: 1px solid #ddd; + padding: 12px; + text-align: left; + vertical-align: top; + } + th { + background-color: #f2f2f2; + font-weight: bold; + text-align: center; + } + tbody tr:nth-child(even) { + background-color: #f9f9f9; + } + table code { + background-color: #f4f4f4; + padding: 2px 4px; + border-radius: 3px; + font-size: 1em; + } + .math-inline { + white-space: nowrap; + font-family: "MathJax_Math", "MathJax_Main", "STIX Two Math", "Latin Modern Math", "Computer Modern", "Times New Roman", Times, serif; + font-size: 1.05em; + vertical-align: baseline; + line-height: 1.4; + font-weight: normal; + letter-spacing: 0.01em; + } + .math-inline i { + font-style: italic; + font-family: "MathJax_Math", "MathJax_Main", "STIX Two Math", "Latin Modern Math", "Computer Modern", "Times New Roman", Times, serif; + } + .math-block { + text-align: center; + margin: 20px 0; + padding: 10px; + font-family: "MathJax_Math", "MathJax_Main", "STIX Two Math", "Latin Modern Math", "Computer Modern", "Times New Roman", Times, serif; + font-size: 1.05em; + } + .math-block i { + font-style: italic; + font-family: "MathJax_Math", "MathJax_Main", "STIX Two Math", "Latin Modern Math", "Computer Modern", "Times New Roman", Times, serif; + } + .math-function { + font-style: normal; + font-weight: normal; + } + .math-text { + font-style: normal; + font-weight: normal; + } + .math-roman { + font-style: normal; + font-weight: normal; + } + .math-aligned { + margin: 10px auto; + border-collapse: collapse; + border: none; + width: auto; + display: table; + } + .math-aligned td { + border: none; + padding: 6px 12px; + vertical-align: middle; + font-family: "MathJax_Math", "MathJax_Main", "STIX Two Math", "Latin Modern Math", "Computer Modern", "Times New Roman", Times, serif; + font-size: 1.05em; + } + .math-aligned i { + font-style: italic; + } + .math-inline sup, + .math-block sup { + font-size: 0.7em; + vertical-align: super; + } + .math-inline sub, + .math-block sub { + font-size: 0.7em; + vertical-align: sub; + } + .admonition { + margin: 20px 0; + padding: 12px 16px 8px 16px; + border: 2px solid #6d85df; + border-radius: 6px; + background-color: #f8f9fa; + } + .admonition-title { + font-weight: 600; + margin-bottom: 8px; + color: #2c3e50; + } + .admonition.note { + border-color: #53bbb1; + background-color: #f6fbfb; + } + .admonition.tip { + border-color: #53bbb1; + background-color: #f6fbfb; + } + .admonition.success { + border-color: #82bb81; + background-color: #f8fcf9; + } + .admonition.warning { + border-color: #ffc107; + background-color: #fff3cd; + } + .admonition.danger { + border-color: #dc3545; + background-color: #f8d7da; + } + .admonition.info { + border-color: #17a2b8; + background-color: #d1ecf1; + } + .admonition.abstract { + border-color: #6d85df; + background-color: #f8f9fa; + } + .admonition.question { + border-color: #82bb81; + background-color: #f8fcf9; + } + .admonition.quote { + border-color: #898989; + background-color: #f9f9f9; + } + ul, ol { + padding-left: 30px; + } + li { + margin: 5px 0; + } + + /* Syntax highlighting styles (GitHub Light theme) */ + .hljs { + display: block; + padding: 14px 16px; + color: #24292e; + background: #f5f5f5; + } + + .hljs-comment, + .hljs-quote { + color: #6a737d; + font-style: italic; + } + + .hljs-keyword, + .hljs-selector-tag, + .hljs-subst { + color: #d73a49; + font-weight: bold; + } + + .hljs-number, + .hljs-literal, + .hljs-variable, + .hljs-template-variable, + .hljs-tag .hljs-attr { + color: #005cc5; + } + + .hljs-string, + .hljs-doctag { + color: #032f62; + } + + .hljs-title, + .hljs-section, + .hljs-selector-id { + color: #6f42c1; + font-weight: bold; + } + + .hljs-subst { + font-weight: normal; + } + + .hljs-type, + .hljs-class .hljs-title { + color: #d73a49; + font-weight: bold; + } + + .hljs-tag, + .hljs-name, + .hljs-attribute { + color: #22863a; + font-weight: normal; + } + + .hljs-regexp, + .hljs-link { + color: #032f62; + } + + .hljs-symbol, + .hljs-bullet { + color: #e36209; + } + + .hljs-built_in, + .hljs-builtin-name { + color: #005cc5; + } + + .hljs-meta { + color: #6a737d; + font-weight: bold; + } + + .hljs-deletion { + background: #ffeef0; + } + + .hljs-addition { + background: #f0fff4; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: bold; + } +`; +} + +/** + * Wrap HTML content (simplified version, styles provided by CSS file) + */ +function wrapHtmlContent(html: string): string { + return html; +} + +/** + * Extract all headings from Markdown text + * @param markdown Markdown text + * @param chapterPath Chapter path (for identification) + * @returns Heading list + */ +export function extractHeadings(markdown: string, chapterPath: string): HeadingInfo[] { + const headings: HeadingInfo[] = []; + const lines = markdown.split('\n'); + let inCodeBlock = false; + let inFencedBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check code block boundaries (three backticks or tildes) + if (line.match(/^\s*```/) || line.match(/^\s*~~~/)) { + inCodeBlock = !inCodeBlock; + continue; + } + + // Check indented code blocks (4 spaces) + if (line.match(/^ /) && !inCodeBlock) { + inFencedBlock = true; + continue; + } else if (inFencedBlock && !line.match(/^ /) && line.trim()) { + inFencedBlock = false; + } + + // Skip content in code blocks + if (inCodeBlock || inFencedBlock) { + continue; + } + + // Match ATX-style headings: # ## ### etc. + const match = line.match(/^(#{1,6})\s+(.+)$/); + if (match) { + const level = match[1].length; + let text = match[2].trim(); + + // Remove Markdown formatting and math formulas from heading, keep plain text + text = text + .replace(/\$[^$]+\$/g, '') // Remove inline math formulas + .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold + .replace(/\*(.+?)\*/g, '$1') // Remove italic + .replace(/`(.+?)`/g, '$1') // Remove code + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links, keep text + .trim(); + + if (text) { + headings.push({ + level, + text, + chapterPath, + lineNumber: i + 1 + }); + } + } + } + + return headings; +} + +/** + * Extract image paths from Markdown + */ +export function extractImagePaths(markdown: string, markdownFilePath: string): Array<{ original: string; fullPath: string }> { + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + const paths: Array<{ original: string; fullPath: string }> = []; + let match; + const markdownDir = path.dirname(markdownFilePath); + const docsRoot = path.resolve(markdownDir, '..'); + + while ((match = imageRegex.exec(markdown)) !== null) { + const imagePath = match[2]; + // Handle relative paths + if (!imagePath.startsWith('http://') && !imagePath.startsWith('https://') && !imagePath.startsWith('data:')) { + // Try multiple possible paths + const possiblePaths = [ + path.resolve(markdownDir, imagePath), // Relative to current markdown file + path.resolve(docsRoot, imagePath), // Relative to docs root directory + path.resolve(markdownDir, '..', imagePath), // Parent directory + ]; + + for (const fullPath of possiblePaths) { + if (fs.existsSync(fullPath)) { + paths.push({ original: imagePath, fullPath }); + break; + } + } + } + } + + return paths; +} + +/** + * Read image file + */ +export function readImage(imagePath: string): ImageInfo | null { + try { + const data = fs.readFileSync(imagePath); + const ext = path.extname(imagePath).toLowerCase(); + const mimeTypes: { [key: string]: string } = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + }; + const mimeType = mimeTypes[ext] || 'image/png'; + + return { + src: path.basename(imagePath), + data, + mimeType, + }; + } catch (error) { + console.warn(`Unable to read image: ${imagePath}`, error); + return null; + } +} + +/** + * Convert image to base64 data URI + */ +export function imageToDataUri(imageInfo: ImageInfo): string { + const base64 = imageInfo.data.toString('base64'); + return `data:${imageInfo.mimeType};base64,${base64}`; +} + diff --git a/epub/src/parser.ts b/epub/src/parser.ts new file mode 100644 index 000000000..4e7c0a91b --- /dev/null +++ b/epub/src/parser.ts @@ -0,0 +1,168 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; +import { NavItem, Chapter, MkDocsNavItem } from './types'; + +/** + * Parse mkdocs.yml file to extract version number + */ +export function parseVersion(configPath: string): string { + const content = fs.readFileSync(configPath, 'utf-8'); + const config = yaml.load(content) as any; + return config.version || '1.0.0'; +} + +/** + * Parse mkdocs.yml file to extract navigation structure + */ +export function parseMkdocsConfig(configPath: string): MkDocsNavItem[] { + const content = fs.readFileSync(configPath, 'utf-8'); + const config = yaml.load(content) as any; + return config.nav || []; +} + +/** + * Flatten navigation structure, extract all chapter paths while preserving hierarchy + * Based on mkdocs.yml structure: + * - Parent titles (e.g., "Chapter 0 Preface") contain arrays of child items + * - Child items can be string paths (index.md) or objects (sub-chapters) + */ +export function flattenNav(nav: MkDocsNavItem[], basePath: string = ''): Chapter[] { + const chapters: Chapter[] = []; + let order = 0; + + function traverse(items: MkDocsNavItem[], parentTitle: string = '', parentNumber: string = '', level: number = 0) { + for (const item of items) { + if (typeof item === 'string') { + // Simple path string (usually index.md, as chapter homepage) + const filePath = path.join(basePath, item); + if (fs.existsSync(filePath)) { + // Try to extract title from file content + const content = fs.readFileSync(filePath, 'utf-8'); + const titleMatch = content.match(/^#\s+(.+)$/m); + const title = titleMatch ? titleMatch[1].trim() : path.basename(item, '.md'); + + // index.md uses parent chapter's title and number + chapters.push({ + title: parentTitle || title, // If parent title exists, use it; otherwise use file title + path: filePath, + content: '', + order: order++, + level: level, + number: parentNumber || undefined, + parentTitle: undefined // index.md is top-level chapter + }); + } + } else if (typeof item === 'object' && item !== null) { + // Handle object format: { "title": "path" } or { "title": [...] } + if (Array.isArray(item)) { + traverse(item, parentTitle, parentNumber, level); + } else { + const keys = Object.keys(item); + if (keys.length === 1) { + const key = keys[0]; + const value = item[key]; + + if (typeof value === 'string') { + // { "title": "path" } format - this is a sub-chapter (e.g., "0.1   About this book": "path") + const filePath = path.join(basePath, value); + if (fs.existsSync(filePath)) { + const fullTitle = key.replace(/ /g, ' ').trim(); + + // Extract chapter number, e.g., "0.1   About this book" -> "0.1" + const numberMatch = fullTitle.match(/^(\d+(?:\.\d+)?)\s+/); + let chapterNumber = numberMatch ? numberMatch[1] : undefined; + + // Extract title (remove number part) + const title = fullTitle.replace(/^\d+(?:\.\d+)?\s+/, '').trim(); + + chapters.push({ + title: title, + path: filePath, + content: '', + order: order++, + level: level + 1, // Sub-chapter level +1 + number: chapterNumber, + parentTitle: parentTitle || undefined + }); + } + } else if (Array.isArray(value)) { + // { "title": [...] } format - this is parent chapter title and child items (e.g., "Chapter 0 Preface": [...]) + const chapterTitle = key.replace(/ /g, ' ').trim(); + + // Extract chapter number from title, e.g., "Chapter 0   Preface" -> "0" + let chapterNumber = ''; + const numberMatch = chapterTitle.match(/第\s*(\d+)\s*章/); + if (numberMatch) { + chapterNumber = numberMatch[1]; + } else { + // If no number found, try to infer from first sub-chapter (e.g., infer "0" from "0.1") + for (const subItem of value) { + if (typeof subItem === 'string') { + continue; + } else if (typeof subItem === 'object' && subItem !== null && !Array.isArray(subItem)) { + const subObj = subItem as { [key: string]: string | MkDocsNavItem[] }; + const subKeys = Object.keys(subObj); + if (subKeys.length === 1) { + const subKey = subKeys[0]; + const subValue = subObj[subKey]; + if (typeof subValue === 'string') { + const subTitle = subKey.replace(/ /g, ' ').trim(); + const subNumberMatch = subTitle.match(/^(\d+)\.\d+/); + if (subNumberMatch) { + chapterNumber = subNumberMatch[1]; + break; + } + } + } + } + } + } + + // Recursively process child items, maintain hierarchy + traverse(value, chapterTitle, chapterNumber, level); + } + } else if ('path' in item && typeof item.path === 'string') { + // Object with path property + const filePath = path.join(basePath, item.path); + if (fs.existsSync(filePath)) { + chapters.push({ + title: (typeof item.title === 'string' ? item.title : undefined) || path.basename(item.path, '.md'), + path: filePath, + content: '', + order: order++, + level: level, + number: parentNumber || undefined, + parentTitle: parentTitle || undefined + }); + } + } else if ('title' in item && 'children' in item && Array.isArray(item.children) && typeof item.title === 'string') { + // Object with title and children properties + traverse(item.children, item.title, parentNumber, level); + } + } + } + } + } + + traverse(nav); + + // Filter out chapters related to chapter_paperbook + return chapters.filter(chapter => { + // Check if path contains chapter_paperbook + return !chapter.path.includes('chapter_paperbook'); + }); +} + +/** + * Read chapter content + */ +export function readChapterContent(chapter: Chapter): string { + try { + return fs.readFileSync(chapter.path, 'utf-8'); + } catch (error) { + console.warn(`Unable to read file: ${chapter.path}`, error); + return ''; + } +} + diff --git a/epub/src/types.ts b/epub/src/types.ts new file mode 100644 index 000000000..0fa8f4d2d --- /dev/null +++ b/epub/src/types.ts @@ -0,0 +1,52 @@ + +export interface NavItem { + title?: string; + path?: string; + children?: NavItem[]; +} + +// Navigation item type in MkDocs config file +export type MkDocsNavItem = string | { [key: string]: string | MkDocsNavItem[] } | MkDocsNavItem[]; + +export interface Chapter { + title: string; + path: string; + content: string; + order: number; + level: number; // Hierarchy level: 0=top-level chapter, 1=sub-chapter + number?: string; // Chapter number, e.g., "0", "0.1", "1", "1.1" + parentTitle?: string; // Parent chapter title +} + +export interface ImageInfo { + src: string; + data: Buffer; + mimeType: string; +} + +// Heading information +export interface HeadingInfo { + level: number; // Heading level (1-6) + text: string; // Heading text + chapterPath: string; // Path to the chapter + lineNumber?: number; // Line number (if available) +} + +// Option types for epub-gen library +export interface EpubGenOptions { + title: string; + author: string; + publisher?: string; + description?: string; + language?: string; + content: Chapter[]; + verbose?: boolean; + appendChapterTitles?: boolean; + customNcxTocTemplatePath?: string; + customHtmlTocTemplatePath?: string; + css?: string; + cover?: string; + output?: string; + fonts?: string[]; + tocTitle?: string; // Table of contents title +} diff --git a/epub/src/validator.ts b/epub/src/validator.ts new file mode 100644 index 000000000..6d5e924fa --- /dev/null +++ b/epub/src/validator.ts @@ -0,0 +1,137 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { HeadingInfo } from './types'; +const AdmZip = require('adm-zip'); + +/** + * Validate if EPUB contains all headings + * @param epubPath EPUB file path + * @param expectedHeadings Expected heading list + * @returns Validation result + */ +export async function validateEpubHeadings( + epubPath: string, + expectedHeadings: HeadingInfo[] +): Promise<{ success: boolean; missingHeadings: HeadingInfo[]; summary: string }> { + + console.log(`\nStarting EPUB content validation...`); + console.log(`Expected heading count: ${expectedHeadings.length}`); + + try { + // Extract EPUB + const zip = new AdmZip(epubPath); + const zipEntries = zip.getEntries(); + + // Extract all XHTML content + let allHtmlContent = ''; + for (const entry of zipEntries) { + if (entry.entryName.endsWith('.xhtml') || entry.entryName.endsWith('.html')) { + const content = entry.getData().toString('utf8'); + allHtmlContent += content + '\n'; + } + } + + // Check if each heading exists + const missingHeadings: HeadingInfo[] = []; + const foundHeadings: HeadingInfo[] = []; + + for (const heading of expectedHeadings) { + const found = checkHeadingInHtml(allHtmlContent, heading); + if (found) { + foundHeadings.push(heading); + } else { + missingHeadings.push(heading); + } + } + + // Generate report + const successRate = ((foundHeadings.length / expectedHeadings.length) * 100).toFixed(2); + + let summary = `\n${'='.repeat(60)}\n`; + summary += `EPUB Content Validation Report\n`; + summary += `${'='.repeat(60)}\n`; + summary += `Total headings: ${expectedHeadings.length}\n`; + summary += `Found: ${foundHeadings.length}\n`; + summary += `Missing: ${missingHeadings.length}\n`; + summary += `Completeness rate: ${successRate}%\n`; + summary += `${'='.repeat(60)}\n`; + + if (missingHeadings.length > 0) { + summary += `\n⚠️ Missing headings:\n`; + summary += `${'='.repeat(60)}\n`; + + // Group by chapter + const byChapter = new Map(); + for (const heading of missingHeadings) { + if (!byChapter.has(heading.chapterPath)) { + byChapter.set(heading.chapterPath, []); + } + byChapter.get(heading.chapterPath)!.push(heading); + } + + for (const [chapterPath, headings] of byChapter) { + summary += `\n📄 ${chapterPath}\n`; + for (const heading of headings) { + const indent = ' '.repeat(heading.level - 1); + summary += `${indent}${'#'.repeat(heading.level)} ${heading.text}`; + if (heading.lineNumber) { + summary += ` (line ${heading.lineNumber})`; + } + summary += `\n`; + } + } + summary += `\n`; + } else { + summary += `\n✅ All headings are included in the EPUB!\n\n`; + } + + console.log(summary); + + return { + success: missingHeadings.length === 0, + missingHeadings, + summary + }; + + } catch (error) { + console.error('Error during validation:', error); + return { + success: false, + missingHeadings: expectedHeadings, + summary: `Validation failed: ${error}` + }; + } +} + +/** + * Check if heading exists in HTML + */ +function checkHeadingInHtml(html: string, heading: HeadingInfo): boolean { + // Remove HTML tags and entities, keep only text + const cleanHtml = html + .replace(/]*>[\s\S]*?<\/script>/gi, '') // Remove scripts + .replace(/]*>[\s\S]*?<\/style>/gi, '') // Remove styles + .replace(/<[^>]+>/g, ' ') // Remove all HTML tags + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + // Decode numeric entities + .replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => { + return String.fromCharCode(parseInt(hex, 16)); + }) + .replace(/&#(\d+);/g, (match, dec) => { + return String.fromCharCode(parseInt(dec, 10)); + }); + + // Normalize text: remove extra spaces + const normalizedHtml = cleanHtml.replace(/\s+/g, ' ').toLowerCase(); + const normalizedHeading = heading.text.replace(/\s+/g, ' ').toLowerCase(); + + // Check if heading text is included + return normalizedHtml.includes(normalizedHeading); +} + + + diff --git a/epub/templates/toc.ncx.ejs b/epub/templates/toc.ncx.ejs new file mode 100644 index 000000000..d0820b67f --- /dev/null +++ b/epub/templates/toc.ncx.ejs @@ -0,0 +1,96 @@ + + + + + + + + + + + <%= title %> + + + <%= author %> + + + <% + var playOrder = 1; + + // 构建层级结构 + var hierarchicalContent = []; + var currentParent = null; + + content.forEach(function(item, index) { + if (!item.excludeFromToc && !item.beforeToc) { + // 检查是否是顶级章节(level 0 或没有 parentTitle) + if (item.level === 0 || !item.parentTitle) { + // 开始新的父章节 + currentParent = { + title: item.title, + href: item.href, + level: 0, + number: item.number, + children: [] + }; + hierarchicalContent.push(currentParent); + } else if (item.level === 1 && currentParent) { + // 添加到当前父章节的子项 + currentParent.children.push({ + title: item.title, + href: item.href, + level: 1, + number: item.number + }); + } else { + // 其他情况,作为独立章节 + hierarchicalContent.push({ + title: item.title, + href: item.href, + level: item.level || 0, + number: item.number, + children: [] + }); + } + } + }); + + // 渲染嵌套的导航点 + function renderNavPoint(chapter, depth) { + var id = "navpoint_" + playOrder; + var href = chapter.href; + var title = chapter.title || 'Untitled'; + var children = chapter.children || []; + var number = chapter.number; + var level = chapter.level || 0; + var currentOrder = playOrder++; + + // 对于顶级章节(level 0),标题已经包含"第X章",不需要再加编号 + // 只对子章节(level 1)添加编号 + var displayTitle = (level === 1 && number ? number + ' ' : '') + title; + + var result = ' \n'; + result += ' \n'; + result += ' ' + displayTitle.replace(/&/g, '&').replace(//g, '>') + '\n'; + result += ' \n'; + result += ' \n'; + + // 递归渲染子章节 + if (children.length > 0) { + children.forEach(function(child) { + result += renderNavPoint(child, depth + 1); + }); + } + + result += ' \n'; + return result; + } + + // 渲染所有导航点 + hierarchicalContent.forEach(function(chapter) { + %><%- renderNavPoint(chapter, 0) %><% + }); + %> + + + diff --git a/epub/templates/toc.xhtml.ejs b/epub/templates/toc.xhtml.ejs new file mode 100644 index 000000000..b08c9eed3 --- /dev/null +++ b/epub/templates/toc.xhtml.ejs @@ -0,0 +1,115 @@ + + + + + <%= title %> + + + + + +

<%= tocTitle %>

+ + + + + diff --git a/epub/tsconfig.json b/epub/tsconfig.json new file mode 100644 index 000000000..b432dc98e --- /dev/null +++ b/epub/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +