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
107
108
109
110
111
112
113
114
115
|
//! 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);
var doc = try html.transform(gpa, page);
defer doc.deinit();
// Some older docs pages have malformed upstream HTML that can't be repaired
// without a full HTML5 tree-builder; skip them rather than emit invalid EPUB3.
if (!html.isWellFormed(doc.content)) return error.NotWellFormed;
const bytes = try epub.build(gpa, .{
.title = title,
.version = v.name,
.xhtml = doc.content,
.toc = doc.toc,
});
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());
}
|