aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.zig
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-05-28 23:27:10 +0200
committerSimen A. W. Olsen <hello@simenandre.no>2026-05-28 23:27:10 +0200
commit1eaf6a9d883a47444d0d68f4ab88bfba62c321d0 (patch)
treea4ff9ee96580c8173620b1419537308367ebd399 /src/main.zig
downloadziglang-docs-epub-1eaf6a9d883a47444d0d68f4ab88bfba62c321d0.tar.gz
ziglang-docs-epub-1eaf6a9d883a47444d0d68f4ab88bfba62c321d0.zip
feat: add ziglang-docs-epub tool
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig106
1 files changed, 106 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..a6bbe31
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,106 @@
+//! ziglang-docs-epub: turn the Zig language reference into one EPUB per version.
+//!
+//! Usage:
+//! ziglang-docs-epub [version...]
+//! no args build every version in index.json that has a docs URL
+//! version build only the named version(s), e.g. `0.15.2 master`
+//!
+//! Output is written to the `epubs/` directory as `zig-<version>.epub`.
+
+const std = @import("std");
+const assert = std.debug.assert;
+
+const fetch = @import("fetch.zig");
+const index = @import("index.zig");
+const html = @import("html.zig");
+const epub = @import("epub.zig");
+
+const out_dir = "epubs";
+const title = "Zig Language Reference";
+/// A valid EPUB is always larger than this; used to sanity-check output.
+const min_epub_size = 256;
+
+pub fn main() !void {
+ var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
+ defer _ = gpa_state.deinit();
+ const gpa = gpa_state.allocator();
+
+ const args = try std.process.argsAlloc(gpa);
+ defer std.process.argsFree(gpa, args);
+ const filters = args[1..]; // empty == build all
+
+ var client: std.http.Client = .{ .allocator = gpa };
+ defer client.deinit();
+
+ var versions = index.fetchAll(gpa, &client) catch |err| {
+ std.log.err("failed to fetch version index: {s}", .{@errorName(err)});
+ return err;
+ };
+ defer versions.deinit();
+
+ var dir = try std.fs.cwd().makeOpenPath(out_dir, .{});
+ defer dir.close();
+
+ var built: usize = 0;
+ var skipped: usize = 0;
+
+ assert(versions.items.len <= index.max_versions);
+ for (versions.items) |v| {
+ if (!wanted(filters, v.name)) continue;
+
+ buildVersion(gpa, &client, dir, v) catch |err| {
+ std.log.warn("skip {s}: {s}", .{ v.name, @errorName(err) });
+ skipped += 1;
+ continue;
+ };
+ built += 1;
+ }
+
+ std.log.info("done: {d} built, {d} skipped", .{ built, skipped });
+ if (built == 0) std.log.warn("no epubs were produced", .{});
+}
+
+/// Fetch, transform, package and write the EPUB for a single version.
+fn buildVersion(
+ gpa: std.mem.Allocator,
+ client: *std.http.Client,
+ dir: std.fs.Dir,
+ v: index.Version,
+) !void {
+ const page = try fetch.get(gpa, client, v.docs_url);
+ defer gpa.free(page);
+
+ const xhtml = try html.toXhtml(gpa, page);
+ defer gpa.free(xhtml);
+
+ const bytes = try epub.build(gpa, .{ .title = title, .version = v.name, .xhtml = xhtml });
+ defer gpa.free(bytes);
+
+ assert(std.mem.startsWith(u8, bytes, "PK"));
+ assert(bytes.len > min_epub_size);
+
+ const name = try std.fmt.allocPrint(gpa, "zig-{s}.epub", .{v.name});
+ defer gpa.free(name);
+ try dir.writeFile(.{ .sub_path = name, .data = bytes });
+
+ std.log.info("built {s} ({d} bytes)", .{ name, bytes.len });
+}
+
+/// True if `name` should be built given the CLI `filters` (empty == all).
+fn wanted(filters: []const []const u8, name: []const u8) bool {
+ if (filters.len == 0) return true;
+ for (filters) |f| {
+ if (std.mem.eql(u8, f, name)) return true;
+ }
+ return false;
+}
+
+test "wanted matches filters or accepts all when empty" {
+ try std.testing.expect(wanted(&.{}, "0.15.2"));
+ try std.testing.expect(wanted(&.{"0.15.2"}, "0.15.2"));
+ try std.testing.expect(!wanted(&.{"master"}, "0.15.2"));
+}
+
+test {
+ std.testing.refAllDecls(@This());
+}