aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.zig
blob: a6bbe317f47eecc9bf6b7dfc95e7df6b54b2314c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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());
}