From 1eaf6a9d883a47444d0d68f4ab88bfba62c321d0 Mon Sep 17 00:00:00 2001 From: "Simen A. W. Olsen" Date: Thu, 28 May 2026 23:27:10 +0200 Subject: feat: add ziglang-docs-epub tool --- .forgejo/workflows/build-epubs.yml | 54 +++++++ .gitignore | 2 + LICENSE | 201 ++++++++++++++++++++++++ README.md | 52 +++++++ build.zig | 33 ++++ build.zig.zon | 80 ++++++++++ epubs/.gitkeep | 0 epubs/zig-0.1.1.epub | Bin 0 -> 201853 bytes epubs/zig-0.10.0.epub | Bin 0 -> 1130432 bytes epubs/zig-0.10.1.epub | Bin 0 -> 1133513 bytes epubs/zig-0.11.0.epub | Bin 0 -> 1081055 bytes epubs/zig-0.12.0.epub | Bin 0 -> 922795 bytes epubs/zig-0.12.1.epub | Bin 0 -> 933356 bytes epubs/zig-0.13.0.epub | Bin 0 -> 933399 bytes epubs/zig-0.14.0.epub | Bin 0 -> 964357 bytes epubs/zig-0.14.1.epub | Bin 0 -> 967595 bytes epubs/zig-0.15.1.epub | Bin 0 -> 970943 bytes epubs/zig-0.15.2.epub | Bin 0 -> 970121 bytes epubs/zig-0.16.0.epub | Bin 0 -> 983739 bytes epubs/zig-0.2.0.epub | Bin 0 -> 238448 bytes epubs/zig-0.3.0.epub | Bin 0 -> 525326 bytes epubs/zig-0.4.0.epub | Bin 0 -> 656333 bytes epubs/zig-0.5.0.epub | Bin 0 -> 719277 bytes epubs/zig-0.6.0.epub | Bin 0 -> 709449 bytes epubs/zig-0.7.0.epub | Bin 0 -> 763214 bytes epubs/zig-0.7.1.epub | Bin 0 -> 763304 bytes epubs/zig-0.8.0.epub | Bin 0 -> 799052 bytes epubs/zig-0.8.1.epub | Bin 0 -> 802163 bytes epubs/zig-0.9.0.epub | Bin 0 -> 1113158 bytes epubs/zig-0.9.1.epub | Bin 0 -> 1113237 bytes epubs/zig-master.epub | Bin 0 -> 967090 bytes src/epub.zig | 129 ++++++++++++++++ src/fetch.zig | 30 ++++ src/html.zig | 309 +++++++++++++++++++++++++++++++++++++ src/index.zig | 103 +++++++++++++ src/main.zig | 106 +++++++++++++ src/zip.zig | 192 +++++++++++++++++++++++ 37 files changed, 1291 insertions(+) create mode 100644 .forgejo/workflows/build-epubs.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 epubs/.gitkeep create mode 100644 epubs/zig-0.1.1.epub create mode 100644 epubs/zig-0.10.0.epub create mode 100644 epubs/zig-0.10.1.epub create mode 100644 epubs/zig-0.11.0.epub create mode 100644 epubs/zig-0.12.0.epub create mode 100644 epubs/zig-0.12.1.epub create mode 100644 epubs/zig-0.13.0.epub create mode 100644 epubs/zig-0.14.0.epub create mode 100644 epubs/zig-0.14.1.epub create mode 100644 epubs/zig-0.15.1.epub create mode 100644 epubs/zig-0.15.2.epub create mode 100644 epubs/zig-0.16.0.epub create mode 100644 epubs/zig-0.2.0.epub create mode 100644 epubs/zig-0.3.0.epub create mode 100644 epubs/zig-0.4.0.epub create mode 100644 epubs/zig-0.5.0.epub create mode 100644 epubs/zig-0.6.0.epub create mode 100644 epubs/zig-0.7.0.epub create mode 100644 epubs/zig-0.7.1.epub create mode 100644 epubs/zig-0.8.0.epub create mode 100644 epubs/zig-0.8.1.epub create mode 100644 epubs/zig-0.9.0.epub create mode 100644 epubs/zig-0.9.1.epub create mode 100644 epubs/zig-master.epub create mode 100644 src/epub.zig create mode 100644 src/fetch.zig create mode 100644 src/html.zig create mode 100644 src/index.zig create mode 100644 src/main.zig create mode 100644 src/zip.zig diff --git a/.forgejo/workflows/build-epubs.yml b/.forgejo/workflows/build-epubs.yml new file mode 100644 index 0000000..676d3c2 --- /dev/null +++ b/.forgejo/workflows/build-epubs.yml @@ -0,0 +1,54 @@ +name: build-epubs + +on: + schedule: + - cron: "0 6 * * *" # daily, to catch master/new releases + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: docker + container: + image: docker.io/debian:trixie-slim + steps: + - name: Install prerequisites + run: | + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl xz-utils git + + - uses: actions/checkout@v4 + + - name: Install Zig 0.15.2 + run: | + set -eux + arch="$(uname -m)" + tarball="zig-${arch}-linux-0.15.2.tar.xz" + curl -fSL "https://ziglang.org/download/0.15.2/${tarball}" -o /tmp/zig.tar.xz + mkdir -p /opt/zig + tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1 + echo "/opt/zig" >> "$GITHUB_PATH" + + - name: Build EPUBs + run: zig build run + + - name: Commit updated EPUBs + run: | + set -eux + git config user.name "ziggy" + git config user.email "ziggy@noreply.codeberg.org" + git add epubs/ + if git diff --cached --quiet; then + echo "No EPUB changes." + exit 0 + fi + git commit -m "build: update epubs [skip ci]" + # Uses the runner token by default; set CI_PUSH_TOKEN if it lacks push rights. + if [ -n "${CI_PUSH_TOKEN:-}" ]; then + git push "https://x-access-token:${CI_PUSH_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" HEAD:main + else + git push origin HEAD:main + fi + env: + CI_PUSH_TOKEN: ${{ secrets.CI_PUSH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dca1103 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +.zig-cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..249e8cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Simen A. W. Olsen + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a5ea18 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# ziglang-docs-epub + +Turns the [Zig language reference](https://ziglang.org/documentation/) into an +**EPUB**, one per Zig version. CI runs it on a schedule and commits the resulting +`.epub` files back into [`epubs/`](epubs/). + +## Why + +The reference ships as a single self-contained HTML page. This tool repackages it +as a standards-compliant EPUB3 so you can read it offline in any e-reader. + +## How it works + +Four decoupled concerns, each a small module that does one thing: + +| Module | Responsibility | +| ------------- | --------------------------------------------------------- | +| `index.zig` | discover versions + docs URLs from `download/index.json` | +| `fetch.zig` | HTTP GET via `std.http.Client` | +| `html.zig` | transform the page HTML5 → well-formed XHTML5 | +| `epub.zig` | assemble the EPUB3 container | +| `zip.zig` | minimal, reproducible ZIP writer (stored entries) | + +No external runtime dependencies — Zig's standard library provides HTTP and +CRC32. EPUB entries are stored uncompressed (Zig 0.15.2's std deflate +*compressor* is unfinished), which keeps the writer trivial and fully +spec-compliant at the cost of larger files. Output is **byte-for-byte +reproducible** (fixed ZIP timestamps), so CI only commits when the upstream docs +actually change. + +## Usage + +Requires **Zig 0.15.2**. + +```sh +zig build run # build every version that has docs +zig build run -- 0.15.2 # build only specific version(s) +zig build test # run unit tests +``` + +EPUBs land in `epubs/zig-.epub`. + +## Continuous integration + +`.forgejo/workflows/build-epubs.yml` runs on [Codeberg](https://codeberg.org) +(Forgejo Actions): daily, on push to `main`, and on manual dispatch. It installs +Zig, runs the tool, and commits any changed EPUBs with `[skip ci]`. + +## License + +Licensed under the [Apache License 2.0](LICENSE). + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..4456d5e --- /dev/null +++ b/build.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "ziglang-docs-epub", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(exe); + + const run = b.addRunArtifact(exe); + run.step.dependOn(b.getInstallStep()); + if (b.args) |args| run.addArgs(args); + const run_step = b.step("run", "Build EPUBs (optionally pass version filters)"); + run_step.dependOn(&run.step); + + const tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..f37216e --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,80 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .ziglang_docs_epub, + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0xbbe24946ce75b1f, // Changing this has security and trust implications. + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.15.2", + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "README.md", + "LICENSE", + }, +} diff --git a/epubs/.gitkeep b/epubs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/epubs/zig-0.1.1.epub b/epubs/zig-0.1.1.epub new file mode 100644 index 0000000..42c91ec Binary files /dev/null and b/epubs/zig-0.1.1.epub differ diff --git a/epubs/zig-0.10.0.epub b/epubs/zig-0.10.0.epub new file mode 100644 index 0000000..49a331d Binary files /dev/null and b/epubs/zig-0.10.0.epub differ diff --git a/epubs/zig-0.10.1.epub b/epubs/zig-0.10.1.epub new file mode 100644 index 0000000..b153576 Binary files /dev/null and b/epubs/zig-0.10.1.epub differ diff --git a/epubs/zig-0.11.0.epub b/epubs/zig-0.11.0.epub new file mode 100644 index 0000000..7aecf9c Binary files /dev/null and b/epubs/zig-0.11.0.epub differ diff --git a/epubs/zig-0.12.0.epub b/epubs/zig-0.12.0.epub new file mode 100644 index 0000000..c72d0d1 Binary files /dev/null and b/epubs/zig-0.12.0.epub differ diff --git a/epubs/zig-0.12.1.epub b/epubs/zig-0.12.1.epub new file mode 100644 index 0000000..d1be901 Binary files /dev/null and b/epubs/zig-0.12.1.epub differ diff --git a/epubs/zig-0.13.0.epub b/epubs/zig-0.13.0.epub new file mode 100644 index 0000000..bfd1991 Binary files /dev/null and b/epubs/zig-0.13.0.epub differ diff --git a/epubs/zig-0.14.0.epub b/epubs/zig-0.14.0.epub new file mode 100644 index 0000000..53a9137 Binary files /dev/null and b/epubs/zig-0.14.0.epub differ diff --git a/epubs/zig-0.14.1.epub b/epubs/zig-0.14.1.epub new file mode 100644 index 0000000..a533700 Binary files /dev/null and b/epubs/zig-0.14.1.epub differ diff --git a/epubs/zig-0.15.1.epub b/epubs/zig-0.15.1.epub new file mode 100644 index 0000000..68bb208 Binary files /dev/null and b/epubs/zig-0.15.1.epub differ diff --git a/epubs/zig-0.15.2.epub b/epubs/zig-0.15.2.epub new file mode 100644 index 0000000..bce9cd8 Binary files /dev/null and b/epubs/zig-0.15.2.epub differ diff --git a/epubs/zig-0.16.0.epub b/epubs/zig-0.16.0.epub new file mode 100644 index 0000000..154ea4b Binary files /dev/null and b/epubs/zig-0.16.0.epub differ diff --git a/epubs/zig-0.2.0.epub b/epubs/zig-0.2.0.epub new file mode 100644 index 0000000..34f1342 Binary files /dev/null and b/epubs/zig-0.2.0.epub differ diff --git a/epubs/zig-0.3.0.epub b/epubs/zig-0.3.0.epub new file mode 100644 index 0000000..f34192d Binary files /dev/null and b/epubs/zig-0.3.0.epub differ diff --git a/epubs/zig-0.4.0.epub b/epubs/zig-0.4.0.epub new file mode 100644 index 0000000..1d9afa6 Binary files /dev/null and b/epubs/zig-0.4.0.epub differ diff --git a/epubs/zig-0.5.0.epub b/epubs/zig-0.5.0.epub new file mode 100644 index 0000000..2b6ed40 Binary files /dev/null and b/epubs/zig-0.5.0.epub differ diff --git a/epubs/zig-0.6.0.epub b/epubs/zig-0.6.0.epub new file mode 100644 index 0000000..78ade26 Binary files /dev/null and b/epubs/zig-0.6.0.epub differ diff --git a/epubs/zig-0.7.0.epub b/epubs/zig-0.7.0.epub new file mode 100644 index 0000000..81e36ff Binary files /dev/null and b/epubs/zig-0.7.0.epub differ diff --git a/epubs/zig-0.7.1.epub b/epubs/zig-0.7.1.epub new file mode 100644 index 0000000..02ccf63 Binary files /dev/null and b/epubs/zig-0.7.1.epub differ diff --git a/epubs/zig-0.8.0.epub b/epubs/zig-0.8.0.epub new file mode 100644 index 0000000..8105508 Binary files /dev/null and b/epubs/zig-0.8.0.epub differ diff --git a/epubs/zig-0.8.1.epub b/epubs/zig-0.8.1.epub new file mode 100644 index 0000000..43d73f5 Binary files /dev/null and b/epubs/zig-0.8.1.epub differ diff --git a/epubs/zig-0.9.0.epub b/epubs/zig-0.9.0.epub new file mode 100644 index 0000000..47ae0b9 Binary files /dev/null and b/epubs/zig-0.9.0.epub differ diff --git a/epubs/zig-0.9.1.epub b/epubs/zig-0.9.1.epub new file mode 100644 index 0000000..212a0fd Binary files /dev/null and b/epubs/zig-0.9.1.epub differ diff --git a/epubs/zig-master.epub b/epubs/zig-master.epub new file mode 100644 index 0000000..4b59c34 Binary files /dev/null and b/epubs/zig-master.epub differ diff --git a/src/epub.zig b/src/epub.zig new file mode 100644 index 0000000..0bdf563 --- /dev/null +++ b/src/epub.zig @@ -0,0 +1,129 @@ +//! Package transformed XHTML content into a valid EPUB3 file (a ZIP with a +//! specific structure). The `mimetype` entry must be first and stored uncompressed. + +const std = @import("std"); +const assert = std.debug.assert; +const zip = @import("zip.zig"); + +/// Fixed modification timestamp for reproducible output. +const modified = "2021-01-01T00:00:00Z"; + +pub const Options = struct { + title: []const u8, + /// Version name, used in the title and as the unique identifier basis. + version: []const u8, + lang: []const u8 = "en", + /// Well-formed XHTML5 body (output of html.toXhtml). + xhtml: []const u8, +}; + +/// Build an EPUB3 archive from `opts`. Caller owns the returned bytes. +pub fn build(allocator: std.mem.Allocator, opts: Options) ![]u8 { + assert(opts.version.len > 0); + assert(opts.xhtml.len > 0); + + var w = zip.Writer.init(allocator); + defer w.deinit(); + + // 1. mimetype — MUST be the first entry and stored uncompressed. + try w.addStored("mimetype", "application/epub+zip"); + + // 2. container.xml — points the reader at the package document. + try w.addStored("META-INF/container.xml", container_xml); + + // 3. content.opf — package metadata, manifest, spine. + const opf = try buildOpf(allocator, opts); + defer allocator.free(opf); + try w.addStored("OEBPS/content.opf", opf); + + // 4. nav.xhtml — minimal EPUB3 navigation document. + const nav = try buildNav(allocator, opts); + defer allocator.free(nav); + try w.addStored("OEBPS/nav.xhtml", nav); + + // 5. index.xhtml — the transformed reference content. + try w.addStored("OEBPS/index.xhtml", opts.xhtml); + + const bytes = try w.finish(); + assert(std.mem.startsWith(u8, bytes, "PK")); + return bytes; +} + +const container_xml = + \\ + \\ + \\ + \\ + \\ + \\ + \\ +; + +fn buildOpf(allocator: std.mem.Allocator, opts: Options) ![]u8 { + return std.fmt.allocPrint(allocator, + \\ + \\ + \\ + \\ urn:ziglang-docs:{s} + \\ {s} ({s}) + \\ {s} + \\ {s} + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + , .{ opts.version, opts.title, opts.version, opts.lang, modified }); +} + +fn buildNav(allocator: std.mem.Allocator, opts: Options) ![]u8 { + return std.fmt.allocPrint(allocator, + \\ + \\ + \\ {s} ({s}) + \\ + \\ + \\ + \\ + \\ + , .{ opts.lang, opts.title, opts.version, opts.title, opts.version }); +} + +test "epub starts with PK and mimetype, and round-trips" { + const gpa = std.testing.allocator; + const bytes = try build(gpa, .{ + .title = "Zig Language Reference", + .version = "0.15.2", + .xhtml = "\nhi", + }); + defer gpa.free(bytes); + + try std.testing.expect(std.mem.startsWith(u8, bytes, "PK")); + // mimetype must be the first entry, stored, with its content right after the + // local header + filename (offset 30 + len("mimetype") == 38). + try std.testing.expectEqualStrings("mimetype", bytes[30..38]); + try std.testing.expectEqualStrings("application/epub+zip", bytes[38..58]); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(.{ .sub_path = "b.epub", .data = bytes }); + var file = try tmp.dir.openFile("b.epub", .{}); + defer file.close(); + var buf: [4096]u8 = undefined; + var fr = file.reader(&buf); + try std.zip.extract(tmp.dir, &fr, .{}); + + const opf = try tmp.dir.readFileAlloc(gpa, "OEBPS/content.opf", 1 << 16); + defer gpa.free(opf); + try std.testing.expect(std.mem.indexOf(u8, opf, "0.15.2") != null); +} diff --git a/src/fetch.zig b/src/fetch.zig new file mode 100644 index 0000000..706a435 --- /dev/null +++ b/src/fetch.zig @@ -0,0 +1,30 @@ +//! Thin HTTP GET helper over std.http.Client. + +const std = @import("std"); +const assert = std.debug.assert; + +/// Largest response body we accept (the docs page is ~1 MB; allow generous slack). +pub const max_body = 32 * 1024 * 1024; + +pub const Error = error{ HttpStatus, BodyTooLarge } || std.mem.Allocator.Error; + +/// GET `url` and return the response body. Caller owns the returned slice. +/// Returns an error on any non-200 status so callers can skip that version. +pub fn get(allocator: std.mem.Allocator, client: *std.http.Client, url: []const u8) ![]u8 { + assert(url.len > 0); + + var body: std.Io.Writer.Allocating = .init(allocator); + defer body.deinit(); + + const result = try client.fetch(.{ + .location = .{ .url = url }, + .response_writer = &body.writer, + }); + + if (result.status != .ok) return Error.HttpStatus; + if (body.writer.end > max_body) return Error.BodyTooLarge; + + const out = try body.toOwnedSlice(); + assert(out.len > 0); + return out; +} diff --git a/src/html.zig b/src/html.zig new file mode 100644 index 0000000..56c5d91 --- /dev/null +++ b/src/html.zig @@ -0,0 +1,309 @@ +//! Transform the Zig docs single-page HTML5 into well-formed XHTML5 for EPUB. +//! +//! The page is self-contained (embedded