//! 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-.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()); }