diff options
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 106 |
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()); +} |
