From 1eaf6a9d883a47444d0d68f4ab88bfba62c321d0 Mon Sep 17 00:00:00 2001 From: "Simen A. W. Olsen" Date: Thu, 28 May 2026 23:27:10 +0200 Subject: feat: add ziglang-docs-epub tool --- src/index.zig | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/index.zig (limited to 'src/index.zig') diff --git a/src/index.zig b/src/index.zig new file mode 100644 index 0000000..7eb3e4b --- /dev/null +++ b/src/index.zig @@ -0,0 +1,103 @@ +//! Discover Zig versions and their docs URLs from ziglang.org/download/index.json. + +const std = @import("std"); +const assert = std.debug.assert; +const fetch = @import("fetch.zig"); + +pub const index_url = "https://ziglang.org/download/index.json"; + +/// Upper bound on versions we will process (NASA Power-of-10: bounded loops). +pub const max_versions = 64; + +pub const Version = struct { + /// Release name as it appears in index.json (e.g. "master", "0.15.2"). + name: []const u8, + /// Absolute URL of the single-page language reference. + docs_url: []const u8, +}; + +/// Owns the slice of versions and all their backing strings. +pub const Versions = struct { + allocator: std.mem.Allocator, + items: []Version, + + pub fn deinit(self: *Versions) void { + for (self.items) |v| { + self.allocator.free(v.name); + self.allocator.free(v.docs_url); + } + self.allocator.free(self.items); + } +}; + +/// Fetch and parse index.json, collecting every entry that has a `docs` URL. +pub fn fetchAll(allocator: std.mem.Allocator, client: *std.http.Client) !Versions { + const json = try fetch.get(allocator, client, index_url); + defer allocator.free(json); + return parse(allocator, json); +} + +/// Parse index.json bytes into owned versions. Split out for testability. +pub fn parse(allocator: std.mem.Allocator, json: []const u8) !Versions { + assert(json.len > 0); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, json, .{}); + defer parsed.deinit(); + + const root = switch (parsed.value) { + .object => |o| o, + else => return error.MalformedIndex, + }; + + var list: std.ArrayList(Version) = .empty; + errdefer { + for (list.items) |v| { + allocator.free(v.name); + allocator.free(v.docs_url); + } + list.deinit(allocator); + } + + var it = root.iterator(); + while (it.next()) |entry| { + if (list.items.len >= max_versions) break; + + const obj = switch (entry.value_ptr.*) { + .object => |o| o, + else => continue, + }; + const docs = switch (obj.get("docs") orelse continue) { + .string => |s| s, + else => continue, + }; + if (docs.len == 0) continue; + + const name = try allocator.dupe(u8, entry.key_ptr.*); + errdefer allocator.free(name); + const url = try allocator.dupe(u8, docs); + try list.append(allocator, .{ .name = name, .docs_url = url }); + } + + return .{ .allocator = allocator, .items = try list.toOwnedSlice(allocator) }; +} + +test "parses versions with docs urls and skips entries without" { + const gpa = std.testing.allocator; + const json = + \\{ + \\ "master": { "version": "0.16.0-dev", "docs": "https://ziglang.org/documentation/master/" }, + \\ "0.15.2": { "docs": "https://ziglang.org/documentation/0.15.2/" }, + \\ "0.1.0": { "notes": "no docs here" } + \\} + ; + var versions = try parse(gpa, json); + defer versions.deinit(); + + try std.testing.expectEqual(@as(usize, 2), versions.items.len); + var saw_master = false; + for (versions.items) |v| { + try std.testing.expect(v.docs_url.len > 0); + if (std.mem.eql(u8, v.name, "master")) saw_master = true; + } + try std.testing.expect(saw_master); +} -- cgit v1.2.3