zig langref cli nate.tngl.io/zigman
0

Configure Feed

Select the types of activity you want to include in your feed.

initial commit: zigman — read the zig language reference in your terminal

a single static Zig binary that fetches the langref (one static HTML page with
stable id anchors), caches it, and prints any section as clean markdown — code,
tables, and lists preserved. no browser, no runtime, no external services.

- src/{main,root,render}.zig — CLI + library facade + html→markdown renderer
- search / list / show / version-pinned / XDG-cached
- `zig build release` cross-compiles macos+linux (aarch64/x86_64) into www/
- served as a tangled Git site; install via curl | sh

renderer structure-preservation inspired by trafilatura (not ported; see CREDITS.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

author
zzstoatzz
co-author
Claude Opus 4.8 (1M context)
date (Jun 14, 2026, 4:19 PM -0500) commit 64c75afb
+1163
+2
.gitignore
··· 1 + .zig-cache/ 2 + zig-out/
+16
CREDITS.md
··· 1 + # credits 2 + 3 + ## trafilatura 4 + 5 + `src/render.zig`'s design is **inspired by** [trafilatura](https://github.com/adbar/trafilatura) 6 + by Adrien Barbaresi — specifically its goal of preserving document *structure* when turning 7 + HTML into text (paragraphs, titles, lists, quotes, code, inline formatting). 8 + 9 + No trafilatura code is ported. trafilatura's core value is removing boilerplate (nav, ads, 10 + sidebars) from arbitrary web pages via readability/jusText-style density heuristics. zigman 11 + renders a single, already-clean fragment (one language-reference section, sliced by its `id` 12 + anchor), so that machinery isn't needed here. If zigman later reads arbitrary pages, a 13 + trafilatura-shaped extractor would slot in *ahead* of the renderer, behind the same `render` seam. 14 + 15 + trafilatura is licensed Apache-2.0 (v1.8.0+; earlier versions GPLv3+). Should any code ever be 16 + ported, it must be taken from an Apache-2.0 release and carry the required attribution.
+63
DISTRIBUTING.md
··· 1 + # distributing zigman 2 + 3 + zigman ships as a single static binary, served from this repo as a tangled 4 + [Git site](https://tangled.org) — no GitHub, no package registry, no CI. 5 + 6 + ## release flow 7 + 8 + ```sh 9 + zig build release 10 + ``` 11 + 12 + cross-compiles every target (macos & linux, aarch64 & x86_64), gzips each into 13 + `www/release/<arch>-<os>.tar.gz`, and copies `web/install.sh` + `web/index.html` 14 + alongside. binaries are ReleaseSafe + stripped (~1 MB each, smaller gzipped). 15 + 16 + > tarballs, not raw binaries: the static host treats an extensionless file under a 17 + > sub-path as a directory (308 → trailing slash → 404), so each target ships as a 18 + > `.tar.gz` that `install.sh` downloads and extracts. 19 + 20 + then commit the result: 21 + 22 + ```sh 23 + git add www && git commit -m "release: <version>" 24 + git push 25 + ``` 26 + 27 + ## one-time site setup 28 + 29 + in the repo's **Settings → Sites**: 30 + 31 + - **Branch**: the branch you pushed `www/` to (e.g. `main`) 32 + - **Deploy directory**: `/www` 33 + - **Site type**: Sub-path site → serves at `nate.tngl.io/zigman` 34 + - **Save** 35 + 36 + after the deploy lands, the install command works for anyone: 37 + 38 + ```sh 39 + curl -fsSL https://nate.tngl.io/zigman/install.sh | sh 40 + ``` 41 + 42 + `install.sh` derives `<arch>-<os>` from `uname` and downloads the matching binary to 43 + `$ZIGMAN_INSTALL` (default `~/.local/bin`). 44 + 45 + ## keeping binaries out of main history (optional) 46 + 47 + committing ~4 MB of binaries to `main` each release bloats history. to avoid that, 48 + push `www/` to a dedicated `site` branch instead and point Sites at it: 49 + 50 + ```sh 51 + zig build release # produces ./www 52 + git switch --orphan site 53 + git rm -rf . >/dev/null 2>&1 || true 54 + mv www/* . && rmdir www 55 + git add . && git commit -m "release: <version>" && git push -u origin site 56 + ``` 57 + 58 + then set Branch = `site`, Deploy directory = `/`. 59 + 60 + ## adding a target 61 + 62 + add an entry to the `targets` list in `build.zig`. the `dir` name must match 63 + `"$(uname -m | arch-normalized)-$(uname -s | os-normalized)"` so `install.sh` finds it.
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2026 zzstoatzz 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+39
README.md
··· 1 + # zigman 2 + 3 + read the [zig language reference](https://ziglang.org/documentation/master/) in your 4 + terminal — for humans who don't want a browser tab, and for agents that can't render one. 5 + 6 + the reference is a single static HTML page with stable per-section `id` anchors. zigman 7 + fetches it once, caches it, and prints any section as clean markdown (code, tables, lists 8 + preserved). no browser, no runtime, no external services — a single static binary. 9 + 10 + ## usage 11 + 12 + ```sh 13 + zigman comptime # show a section 14 + zigman search slice # list sections whose title/anchor matches 15 + zigman list # every section, indented by depth 16 + zigman -v 0.15.1 Optionals # a specific langref version (default: master) 17 + zigman --refresh comptime # ignore the cache and refetch 18 + ``` 19 + 20 + output is plain markdown on stdout, so it pipes: `zigman comptime | glow -`, `zigman list | fzf`. 21 + 22 + the reference is cached under `~/.cache/zigman/<version>.html`. pinned versions are immutable 23 + and cached forever; `master` is refetched once a day (or on `--refresh`). 24 + 25 + ## build 26 + 27 + ```sh 28 + zig build # binary at zig-out/bin/zigman 29 + zig build run -- comptime 30 + zig build test 31 + ``` 32 + 33 + requires zig 0.16+. the parser/renderer is also a library module (`src/root.zig`, exported as 34 + `zigman`) usable from other zig projects. 35 + 36 + ## credit 37 + 38 + the renderer's structure-preservation approach is inspired by 39 + [trafilatura](https://github.com/adbar/trafilatura); see [CREDITS.md](CREDITS.md).
+85
build.zig
··· 1 + const std = @import("std"); 2 + 3 + pub fn build(b: *std.Build) void { 4 + const target = b.standardTargetOptions(.{}); 5 + const optimize = b.standardOptimizeOption(.{}); 6 + 7 + // public library module (the langref parser/renderer) 8 + const mod = b.addModule("zigman", .{ 9 + .root_source_file = b.path("src/root.zig"), 10 + .target = target, 11 + .optimize = optimize, 12 + }); 13 + 14 + const exe = b.addExecutable(.{ 15 + .name = "zigman", 16 + .root_module = b.createModule(.{ 17 + .root_source_file = b.path("src/main.zig"), 18 + .target = target, 19 + .optimize = optimize, 20 + .imports = &.{.{ .name = "zigman", .module = mod }}, 21 + }), 22 + }); 23 + b.installArtifact(exe); 24 + 25 + const run = b.addRunArtifact(exe); 26 + run.step.dependOn(b.getInstallStep()); 27 + if (b.args) |args| run.addArgs(args); 28 + b.step("run", "run zigman").dependOn(&run.step); 29 + 30 + const tests = b.addTest(.{ .root_module = mod }); 31 + const run_tests = b.addRunArtifact(tests); 32 + b.step("test", "run unit tests").dependOn(&run_tests.step); 33 + 34 + // `zig build release --prefix www` cross-compiles every target into 35 + // www/release/<arch>-<os>/zigman and stages the install script + landing 36 + // page, ready to serve as a tangled Git site. Dir names match `uname` 37 + // output so web/install.sh can resolve the right binary. 38 + const release = b.step("release", "cross-compile distributable binaries into the site dir"); 39 + const Target = struct { query: []const u8, dir: []const u8 }; 40 + const targets = [_]Target{ 41 + .{ .query = "aarch64-macos", .dir = "aarch64-macos" }, 42 + .{ .query = "x86_64-macos", .dir = "x86_64-macos" }, 43 + .{ .query = "aarch64-linux-musl", .dir = "aarch64-linux" }, 44 + .{ .query = "x86_64-linux-musl", .dir = "x86_64-linux" }, 45 + }; 46 + const mk = b.addSystemCommand(&.{ "mkdir", "-p", "www/release" }); 47 + for (targets) |t| { 48 + const q = std.Target.Query.parse(.{ .arch_os_abi = t.query }) catch unreachable; 49 + const rt = b.resolveTargetQuery(q); 50 + const lib = b.createModule(.{ 51 + .root_source_file = b.path("src/root.zig"), 52 + .target = rt, 53 + .optimize = .ReleaseSafe, 54 + .strip = true, 55 + }); 56 + const rexe = b.addExecutable(.{ 57 + .name = "zigman", 58 + .root_module = b.createModule(.{ 59 + .root_source_file = b.path("src/main.zig"), 60 + .target = rt, 61 + .optimize = .ReleaseSafe, 62 + .strip = true, 63 + .imports = &.{.{ .name = "zigman", .module = lib }}, 64 + }), 65 + }); 66 + // stage the binary under zig-out, then gzip it into the served www tree. 67 + // an extensionless file under a sub-path is treated as a directory by the 68 + // static host, so binaries are distributed as per-target tarballs. 69 + const inst = b.addInstallArtifact(rexe, .{ 70 + .dest_dir = .{ .override = .{ .custom = b.fmt("release/{s}", .{t.dir}) } }, 71 + }); 72 + const tar = b.addSystemCommand(&.{ 73 + "tar", "-czf", 74 + b.fmt("www/release/{s}.tar.gz", .{t.dir}), "-C", 75 + b.fmt("zig-out/release/{s}", .{t.dir}), "zigman", 76 + }); 77 + tar.step.dependOn(&inst.step); 78 + tar.step.dependOn(&mk.step); 79 + release.dependOn(&tar.step); 80 + } 81 + // static site files (install.sh, index.html) sit alongside the tarballs 82 + const cp = b.addSystemCommand(&.{ "cp", "web/install.sh", "web/index.html", "www/" }); 83 + cp.step.dependOn(&mk.step); 84 + release.dependOn(&cp.step); 85 + }
+14
build.zig.zon
··· 1 + .{ 2 + .name = .zigman, 3 + .version = "0.0.1", 4 + .minimum_zig_version = "0.16.0", 5 + .fingerprint = 0xbf4b1b4800441282, 6 + .paths = .{ 7 + "build.zig", 8 + "build.zig.zon", 9 + "src", 10 + "README.md", 11 + "CREDITS.md", 12 + "LICENSE", 13 + }, 14 + }
+211
src/main.zig
··· 1 + const std = @import("std"); 2 + const Io = std.Io; 3 + const zigman = @import("root.zig"); 4 + const render = zigman.render; 5 + 6 + const usage = 7 + \\zigman — read the zig language reference in your terminal 8 + \\ 9 + \\usage: 10 + \\ zigman <section> show a section (e.g. zigman comptime) 11 + \\ zigman search <term> list sections whose title matches <term> 12 + \\ zigman list list every section 13 + \\ 14 + \\options: 15 + \\ -v, --version <ver> langref version (default: master) 16 + \\ -r, --refresh ignore cache and refetch 17 + \\ -h, --help show this help 18 + \\ 19 + \\the reference is fetched once and cached under ~/.cache/zigman. 20 + \\ 21 + ; 22 + 23 + const Args = struct { 24 + version: []const u8 = "master", 25 + refresh: bool = false, 26 + command: ?[]const u8 = null, // list | search | show 27 + operand: ?[]const u8 = null, // term or section anchor 28 + }; 29 + 30 + pub fn main(init: std.process.Init) !void { 31 + run(init) catch |e| switch (e) { 32 + // stdout closed early (e.g. piping into `head` or quitting `less`) 33 + error.WriteFailed => {}, 34 + else => return e, 35 + }; 36 + } 37 + 38 + fn run(init: std.process.Init) !void { 39 + const a = init.gpa; 40 + const io = init.io; 41 + 42 + var out_buf: [16 * 1024]u8 = undefined; 43 + var fw = std.Io.File.stdout().writer(io, &out_buf); 44 + const out = &fw.interface; 45 + var ew = std.Io.File.stderr().writer(io, &.{}); 46 + const err = &ew.interface; 47 + 48 + const parsed = parseArgs(init.minimal.args) catch { 49 + try err.writeAll(usage); 50 + try err.flush(); 51 + std.process.exit(2); 52 + }; 53 + if (parsed.command == null) { 54 + try out.writeAll(usage); 55 + try out.flush(); 56 + return; 57 + } 58 + 59 + if (!zigman.validVersion(parsed.version)) { 60 + try err.print("zigman: invalid version '{s}'\n", .{parsed.version}); 61 + try err.flush(); 62 + std.process.exit(2); 63 + } 64 + 65 + const dir = cacheDir(a, init.environ_map.*) catch { 66 + try err.writeAll("zigman: neither $XDG_CACHE_HOME nor $HOME is set\n"); 67 + try err.flush(); 68 + std.process.exit(1); 69 + }; 70 + defer a.free(dir); 71 + const html = loadLangref(a, io, dir, parsed.version, parsed.refresh) catch |e| { 72 + try err.print("zigman: could not load langref ({t})\n", .{e}); 73 + try err.flush(); 74 + std.process.exit(1); 75 + }; 76 + defer a.free(html); 77 + 78 + const cmd = parsed.command.?; 79 + if (std.mem.eql(u8, cmd, "list")) { 80 + try listSections(out, html, null); 81 + } else if (std.mem.eql(u8, cmd, "search")) { 82 + try listSections(out, html, parsed.operand.?); 83 + } else { // show 84 + const sec = zigman.sliceSection(html, parsed.operand.?) orelse { 85 + try err.print("zigman: no section '{s}' (try: zigman search {s})\n", .{ parsed.operand.?, parsed.operand.? }); 86 + try err.flush(); 87 + std.process.exit(1); 88 + }; 89 + const md = try render(a, sec); 90 + defer a.free(md); 91 + try out.writeAll(md); 92 + try out.writeByte('\n'); 93 + } 94 + try out.flush(); 95 + } 96 + 97 + fn parseArgs(args: std.process.Args) !Args { 98 + var it = args.iterate(); 99 + _ = it.skip(); // argv0 100 + var r: Args = .{}; 101 + var positionals: [2][]const u8 = undefined; 102 + var n: usize = 0; 103 + while (it.next()) |arg| { 104 + if (eqAny(arg, "-h", "--help")) return error.Help; 105 + if (eqAny(arg, "-r", "--refresh")) { 106 + r.refresh = true; 107 + } else if (eqAny(arg, "-v", "--version")) { 108 + r.version = it.next() orelse return error.MissingValue; 109 + } else if (arg.len > 0 and arg[0] == '-') { 110 + return error.UnknownFlag; 111 + } else { 112 + if (n >= positionals.len) return error.TooManyArgs; 113 + positionals[n] = arg; 114 + n += 1; 115 + } 116 + } 117 + if (n == 0) return r; // no command -> help 118 + const first = positionals[0]; 119 + if (std.mem.eql(u8, first, "list")) { 120 + r.command = "list"; 121 + } else if (std.mem.eql(u8, first, "search")) { 122 + if (n < 2) return error.MissingValue; 123 + r.command = "search"; 124 + r.operand = positionals[1]; 125 + } else if (std.mem.eql(u8, first, "show")) { 126 + if (n < 2) return error.MissingValue; 127 + r.command = "show"; 128 + r.operand = positionals[1]; 129 + } else { 130 + // bare section name: `zigman comptime` 131 + r.command = "show"; 132 + r.operand = first; 133 + } 134 + return r; 135 + } 136 + 137 + // ---------------------------------------------------------------------------- 138 + // langref loading: cache under ~/.cache/zigman/<version>.html 139 + // ---------------------------------------------------------------------------- 140 + 141 + const day_ns: i96 = 24 * 60 * 60 * std.time.ns_per_s; 142 + 143 + /// Resolve the zigman cache directory per the XDG Base Directory spec: 144 + /// `$XDG_CACHE_HOME/zigman` when that variable is set to an absolute path, 145 + /// otherwise `$HOME/.cache/zigman`. Caller owns the returned path. 146 + fn cacheDir(a: std.mem.Allocator, env: std.process.Environ.Map) ![]u8 { 147 + if (env.get("XDG_CACHE_HOME")) |xdg| { 148 + if (xdg.len > 0 and xdg[0] == '/') return std.fmt.allocPrint(a, "{s}/zigman", .{xdg}); 149 + } 150 + const home = env.get("HOME") orelse return error.NoCacheHome; 151 + return std.fmt.allocPrint(a, "{s}/.cache/zigman", .{home}); 152 + } 153 + 154 + fn loadLangref(a: std.mem.Allocator, io: Io, dir: []const u8, version: []const u8, refresh: bool) ![]u8 { 155 + const path = try std.fmt.allocPrint(a, "{s}/{s}.html", .{ dir, version }); 156 + defer a.free(path); 157 + 158 + const cwd = Io.Dir.cwd(); 159 + if (!refresh and cacheFresh(io, cwd, path, version)) { 160 + if (cwd.readFileAlloc(io, path, a, .unlimited)) |cached| { 161 + return cached; 162 + } else |_| {} 163 + } 164 + 165 + const url = try std.fmt.allocPrint(a, "https://ziglang.org/documentation/{s}/", .{version}); 166 + defer a.free(url); 167 + const html = try fetch(a, io, url); 168 + 169 + // best-effort cache write; a failure here shouldn't break the read 170 + cwd.createDirPath(io, dir) catch {}; 171 + cwd.writeFile(io, .{ .sub_path = path, .data = html }) catch {}; 172 + return html; 173 + } 174 + 175 + /// pinned versions are immutable, so any cache hit is fresh; `master` moves, 176 + /// so its cache is only fresh for a day. 177 + fn cacheFresh(io: Io, cwd: Io.Dir, path: []const u8, version: []const u8) bool { 178 + const st = cwd.statFile(io, path, .{}) catch return false; 179 + if (!std.mem.eql(u8, version, "master")) return true; 180 + const now = Io.Timestamp.now(io, .real).nanoseconds; 181 + return now - st.mtime.nanoseconds < day_ns; 182 + } 183 + 184 + fn fetch(a: std.mem.Allocator, io: Io, url: []const u8) ![]u8 { 185 + var client: std.http.Client = .{ .allocator = a, .io = io }; 186 + defer client.deinit(); 187 + var body: Io.Writer.Allocating = .init(a); 188 + errdefer body.deinit(); 189 + const res = try client.fetch(.{ .location = .{ .url = url }, .response_writer = &body.writer }); 190 + if (res.status != .ok) return error.HttpStatus; 191 + return body.toOwnedSlice(); 192 + } 193 + 194 + // ---------------------------------------------------------------------------- 195 + // section index: <hN id="..."> ... </hN> 196 + // ---------------------------------------------------------------------------- 197 + 198 + fn listSections(out: *Io.Writer, html: []const u8, filter: ?[]const u8) !void { 199 + var it = zigman.Sections.init(html); 200 + while (it.next()) |s| { 201 + if (filter) |f| { 202 + if (!zigman.containsIgnoreCase(s.id, f) and !zigman.containsIgnoreCase(s.title, f)) continue; 203 + } 204 + try out.splatByteAll(' ', (s.level - 1) * 2); 205 + try out.print("{s}\n", .{s.id}); 206 + } 207 + } 208 + 209 + fn eqAny(arg: []const u8, short: []const u8, long: []const u8) bool { 210 + return std.mem.eql(u8, arg, short) or std.mem.eql(u8, arg, long); 211 + }
+336
src/render.zig
··· 1 + //! HTML fragment -> markdown renderer for the zig language reference. 2 + //! 3 + //! Scope: this renders a *known-clean* fragment (one langref section, sliced 4 + //! by id anchor). It is NOT a general web extractor — there is no boilerplate 5 + //! to strip inside a section, so we don't need content-density heuristics. 6 + //! 7 + //! Inspired by trafilatura (https://github.com/adbar/trafilatura, Apache-2.0 8 + //! since v1.8.0) for its structure-preservation target: keep paragraphs, 9 + //! titles, lists, quotes, code, and inline formatting. No code is ported; the 10 + //! part of trafilatura that earns its keep (boilerplate removal) is exactly 11 + //! the part a single-document reference like this doesn't need. If zigman ever 12 + //! reads arbitrary pages, a trafilatura-shaped extractor slots in ahead of 13 + //! this renderer behind the same `render` seam. 14 + 15 + const std = @import("std"); 16 + const Writer = std.Io.Writer; 17 + 18 + /// Render an HTML section fragment to markdown. Caller owns the returned slice. 19 + pub fn render(allocator: std.mem.Allocator, html: []const u8) ![]u8 { 20 + var out: Writer.Allocating = .init(allocator); 21 + defer out.deinit(); 22 + var e: Emitter = .{ .w = &out.writer }; 23 + 24 + var i: usize = 0; 25 + while (i < html.len) { 26 + if (html[i] == '<') { 27 + const tag = parseTag(html[i..]) orelse { 28 + try e.text("<"); 29 + i += 1; 30 + continue; 31 + }; 32 + if (!tag.close and eql(tag.name, "a") and contains(tag.raw, "class=\"hdr\"")) { 33 + i += skipUntilClose(html[i..], "a"); 34 + continue; 35 + } 36 + if (!tag.close and eql(tag.name, "figcaption")) { 37 + i += skipUntilClose(html[i..], "figcaption"); 38 + continue; 39 + } 40 + if (!tag.close and eql(tag.name, "table")) { 41 + const span = skipUntilClose(html[i..], "table"); 42 + e.newlines(2); 43 + try e.flushWs(); 44 + try renderTable(allocator, e.w, html[i .. i + span]); 45 + e.wrote = true; 46 + e.newlines(2); 47 + i += span; 48 + continue; 49 + } 50 + try applyTag(&e, tag); 51 + i += tag.len; 52 + continue; 53 + } 54 + const end = std.mem.indexOfScalarPos(u8, html, i, '<') orelse html.len; 55 + try e.text(html[i..end]); 56 + i = end; 57 + } 58 + 59 + return allocator.dupe(u8, std.mem.trim(u8, out.written(), " \n")); 60 + } 61 + 62 + /// Coalesces whitespace so block tags can freely request blank lines / spaces 63 + /// without producing runs of empty lines or leading indentation. Inside a 64 + /// `<pre>`, bytes pass through verbatim. 65 + const Emitter = struct { 66 + w: *Writer, 67 + pending_nl: u8 = 0, // requested newlines, capped at 2, flushed before content 68 + pending_sp: bool = false, 69 + wrote: bool = false, 70 + in_pre: bool = false, 71 + 72 + fn newlines(e: *Emitter, n: u8) void { 73 + if (n > e.pending_nl) e.pending_nl = @min(n, 2); 74 + e.pending_sp = false; 75 + } 76 + fn space(e: *Emitter) void { 77 + e.pending_sp = true; 78 + } 79 + 80 + fn flushWs(e: *Emitter) !void { 81 + if (!e.wrote) { 82 + e.pending_nl = 0; 83 + e.pending_sp = false; 84 + return; 85 + } 86 + if (e.pending_nl > 0) { 87 + try e.w.splatByteAll('\n', e.pending_nl); 88 + } else if (e.pending_sp) { 89 + try e.w.writeByte(' '); 90 + } 91 + e.pending_nl = 0; 92 + e.pending_sp = false; 93 + } 94 + 95 + /// raw literal content (already markdown), flushing pending whitespace first. 96 + fn raw(e: *Emitter, s: []const u8) !void { 97 + if (s.len == 0) return; 98 + try e.flushWs(); 99 + try e.w.writeAll(s); 100 + e.wrote = true; 101 + } 102 + 103 + /// HTML text run: decode entities; outside <pre>, collapse whitespace and 104 + /// drop the § pilcrow; inside <pre>, preserve everything verbatim. 105 + fn text(e: *Emitter, run: []const u8) !void { 106 + if (e.in_pre) { 107 + // emit pending block boundary (the fence opener), then raw code 108 + try e.flushWs(); 109 + try decodeEntities(e.w, run); 110 + if (run.len > 0) e.wrote = true; 111 + return; 112 + } 113 + var k: usize = 0; 114 + while (k < run.len) { 115 + const c = run[k]; 116 + if (c == ' ' or c == '\n' or c == '\t' or c == '\r') { 117 + e.space(); 118 + k += 1; 119 + continue; 120 + } 121 + if (c == 0xC2 and k + 1 < run.len and run[k + 1] == 0xA7) { // § 122 + k += 2; 123 + continue; 124 + } 125 + try e.flushWs(); 126 + if (c == '&') { 127 + k += try writeEntity(e.w, run[k..]); 128 + } else { 129 + try e.w.writeByte(c); 130 + k += 1; 131 + } 132 + e.wrote = true; 133 + } 134 + } 135 + }; 136 + 137 + fn applyTag(e: *Emitter, tag: Tag) !void { 138 + const n = tag.name; 139 + if (headingLevel(n)) |lvl| { 140 + if (!tag.close) { 141 + e.newlines(2); 142 + try e.flushWs(); 143 + try e.w.splatByteAll('#', lvl); 144 + try e.w.writeByte(' '); 145 + e.wrote = true; 146 + } else e.newlines(2); 147 + } else if (eql(n, "p") or eql(n, "ul") or eql(n, "ol") or eql(n, "blockquote")) { 148 + e.newlines(2); 149 + } else if (eql(n, "li")) { 150 + if (!tag.close) { 151 + e.newlines(1); 152 + try e.raw("- "); 153 + } 154 + } else if (eql(n, "pre")) { 155 + if (!tag.close) { 156 + e.newlines(2); 157 + try e.raw("```zig\n"); 158 + e.in_pre = true; 159 + } else { 160 + e.in_pre = false; 161 + try e.raw("\n```"); 162 + e.newlines(2); 163 + } 164 + } else if (eql(n, "code") and !e.in_pre) { 165 + try e.raw("`"); 166 + } else if (eql(n, "br")) { 167 + e.newlines(1); 168 + } 169 + // span, a, em, i, b, figure, cite, … : structurally dropped 170 + } 171 + 172 + const Tag = struct { 173 + name: []const u8, 174 + close: bool, 175 + len: usize, 176 + raw: []const u8, 177 + }; 178 + 179 + fn parseTag(s: []const u8) ?Tag { 180 + if (s.len < 2 or s[0] != '<') return null; 181 + const gt = std.mem.indexOfScalar(u8, s, '>') orelse return null; 182 + var j: usize = 1; 183 + const close = s[j] == '/'; 184 + if (close) j += 1; 185 + const start = j; 186 + while (j < gt and isNameChar(s[j])) j += 1; 187 + return .{ .name = s[start..j], .close = close, .len = gt + 1, .raw = s[0 .. gt + 1] }; 188 + } 189 + 190 + fn decodeEntities(w: *Writer, raw: []const u8) !void { 191 + var k: usize = 0; 192 + while (k < raw.len) { 193 + if (raw[k] == '&') { 194 + k += try writeEntity(w, raw[k..]); 195 + } else { 196 + try w.writeByte(raw[k]); 197 + k += 1; 198 + } 199 + } 200 + } 201 + 202 + fn writeEntity(w: *Writer, s: []const u8) !usize { 203 + const map = .{ 204 + .{ "&quot;", "\"" }, .{ "&amp;", "&" }, .{ "&lt;", "<" }, 205 + .{ "&gt;", ">" }, .{ "&#39;", "'" }, .{ "&apos;", "'" }, 206 + .{ "&nbsp;", " " }, 207 + }; 208 + inline for (map) |m| { 209 + if (std.mem.startsWith(u8, s, m[0])) { 210 + try w.writeAll(m[1]); 211 + return m[0].len; 212 + } 213 + } 214 + try w.writeByte('&'); 215 + return 1; 216 + } 217 + 218 + /// Render a `<table>…</table>` region as a markdown table. The first row is 219 + /// treated as the header (langref tables lead with `<th>`). 220 + fn renderTable(allocator: std.mem.Allocator, w: *Writer, table: []const u8) !void { 221 + var first = true; 222 + var ncols: usize = 0; 223 + var ri: usize = 0; 224 + while (std.mem.indexOfPos(u8, table, ri, "<tr")) |tr| { 225 + const tr_end = tr + skipUntilClose(table[tr..], "tr"); 226 + const row = table[tr..tr_end]; 227 + 228 + var cells: usize = 0; 229 + try w.writeAll("|"); 230 + var ci: usize = 0; 231 + while (nextCell(row, ci)) |cell| { 232 + const md = try renderCell(allocator, cell.inner); 233 + defer allocator.free(md); 234 + try w.print(" {s} |", .{md}); 235 + cells += 1; 236 + ci = cell.next; 237 + } 238 + try w.writeByte('\n'); 239 + if (first) { 240 + ncols = cells; 241 + try w.writeAll("|"); 242 + for (0..ncols) |_| try w.writeAll(" --- |"); 243 + try w.writeByte('\n'); 244 + first = false; 245 + } 246 + ri = tr_end; 247 + } 248 + } 249 + 250 + const Cell = struct { inner: []const u8, next: usize }; 251 + 252 + fn nextCell(row: []const u8, from: usize) ?Cell { 253 + var i = from; 254 + while (std.mem.indexOfScalarPos(u8, row, i, '<')) |lt| { 255 + const tag = parseTag(row[lt..]) orelse { 256 + i = lt + 1; 257 + continue; 258 + }; 259 + if (!tag.close and (eql(tag.name, "td") or eql(tag.name, "th"))) { 260 + const inner_start = lt + tag.len; 261 + const close = skipUntilClose(row[inner_start..], tag.name); 262 + // close includes the closing tag; trim it back to inner content 263 + const inner = trimClose(row[inner_start .. inner_start + close], tag.name); 264 + return .{ .inner = inner, .next = inner_start + close }; 265 + } 266 + i = lt + tag.len; 267 + } 268 + return null; 269 + } 270 + 271 + fn trimClose(s: []const u8, name: []const u8) []const u8 { 272 + // s ends with "</name>"; drop it 273 + const tail = std.mem.lastIndexOf(u8, s, "</") orelse return s; 274 + _ = name; 275 + return s[0..tail]; 276 + } 277 + 278 + /// Render one table cell's inner HTML to a single inline string (no newlines, 279 + /// `|` escaped). Reuses the Emitter for code/entity/whitespace handling. 280 + fn renderCell(allocator: std.mem.Allocator, inner: []const u8) ![]u8 { 281 + var tmp: Writer.Allocating = .init(allocator); 282 + defer tmp.deinit(); 283 + var e: Emitter = .{ .w = &tmp.writer }; 284 + var i: usize = 0; 285 + while (i < inner.len) { 286 + if (inner[i] == '<') { 287 + const tag = parseTag(inner[i..]) orelse { 288 + try e.text("<"); 289 + i += 1; 290 + continue; 291 + }; 292 + if (eql(tag.name, "code")) try e.raw("`"); 293 + i += tag.len; 294 + continue; 295 + } 296 + const end = std.mem.indexOfScalarPos(u8, inner, i, '<') orelse inner.len; 297 + try e.text(inner[i..end]); 298 + i = end; 299 + } 300 + const flat = std.mem.trim(u8, tmp.written(), " \n"); 301 + // escape pipes so cells don't break the table 302 + var buf: std.ArrayList(u8) = .empty; 303 + errdefer buf.deinit(allocator); 304 + for (flat) |c| { 305 + if (c == '|') try buf.append(allocator, '\\'); 306 + if (c == '\n') { 307 + try buf.append(allocator, ' '); 308 + } else try buf.append(allocator, c); 309 + } 310 + return buf.toOwnedSlice(allocator); 311 + } 312 + 313 + fn skipUntilClose(s: []const u8, name: []const u8) usize { 314 + var i: usize = 0; 315 + while (std.mem.indexOfScalarPos(u8, s, i, '<')) |lt| { 316 + if (parseTag(s[lt..])) |t| { 317 + if (t.close and eql(t.name, name)) return lt + t.len; 318 + i = lt + t.len; 319 + } else i = lt + 1; 320 + } 321 + return s.len; 322 + } 323 + 324 + fn headingLevel(n: []const u8) ?u3 { 325 + if (n.len == 2 and n[0] == 'h' and n[1] >= '1' and n[1] <= '6') return @intCast(n[1] - '0'); 326 + return null; 327 + } 328 + fn isNameChar(c: u8) bool { 329 + return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9'); 330 + } 331 + fn eql(a: []const u8, b: []const u8) bool { 332 + return std.ascii.eqlIgnoreCase(a, b); 333 + } 334 + fn contains(h: []const u8, n: []const u8) bool { 335 + return std.mem.indexOf(u8, h, n) != null; 336 + }
+192
src/root.zig
··· 1 + //! zigman library: parse the zig language reference (a single static HTML page 2 + //! with stable per-section `id` anchors) into sections and clean markdown. 3 + //! 4 + //! The CLI in `main.zig` is a thin shell over this; everything here is pure 5 + //! (no I/O), so it is all unit-testable. 6 + 7 + const std = @import("std"); 8 + 9 + pub const render = @import("render.zig").render; 10 + 11 + /// A heading in the reference's table of contents. 12 + pub const Section = struct { 13 + level: u8, // 1..6 14 + id: []const u8, // anchor, e.g. "Sentinel-Terminated-Slices" 15 + title: []const u8, // visible text, e.g. "Sentinel-Terminated Slices" 16 + }; 17 + 18 + /// Iterates `<hN id="...">title</hN>` headings in document order. 19 + pub const Sections = struct { 20 + html: []const u8, 21 + pos: usize = 0, 22 + buf: [256]u8 = undefined, // backing store for the cleaned title 23 + 24 + pub fn init(html: []const u8) Sections { 25 + return .{ .html = html }; 26 + } 27 + 28 + pub fn next(it: *Sections) ?Section { 29 + while (std.mem.indexOfPos(u8, it.html, it.pos, "<h")) |h| { 30 + it.pos = h + 2; 31 + const level = headingLevel(it.html[h..]) orelse continue; 32 + const gt = std.mem.indexOfScalarPos(u8, it.html, h, '>') orelse return null; 33 + const id = attr(it.html[h..gt], "id") orelse continue; 34 + const close = std.mem.indexOfPos(u8, it.html, gt, "</h") orelse return null; 35 + const title = cleanText(&it.buf, it.html[gt + 1 .. close]); 36 + it.pos = close; 37 + return .{ .level = level, .id = id, .title = title }; 38 + } 39 + return null; 40 + } 41 + }; 42 + 43 + /// Returns the HTML fragment for the section with the given anchor: from its 44 + /// heading up to the next heading of equal-or-higher rank. `null` if absent. 45 + pub fn sliceSection(html: []const u8, anchor: []const u8) ?[]const u8 { 46 + var buf: [128]u8 = undefined; 47 + const needle = std.fmt.bufPrint(&buf, "id=\"{s}\"", .{anchor}) catch return null; 48 + const at = std.mem.indexOf(u8, html, needle) orelse return null; 49 + const tag_open = std.mem.lastIndexOfScalar(u8, html[0..at], '<') orelse return null; 50 + const level = headingLevel(html[tag_open..]) orelse return null; 51 + var i = at; 52 + while (std.mem.indexOfPos(u8, html, i, "<h")) |h| { 53 + if (h > tag_open) { 54 + if (headingLevel(html[h..])) |l| { 55 + if (l <= level) return html[tag_open..h]; 56 + } 57 + } 58 + i = h + 2; 59 + } 60 + return html[tag_open..]; 61 + } 62 + 63 + /// A langref version is used in both a URL and a cache file path, so constrain 64 + /// it to characters that can't escape either: "master" or `[0-9A-Za-z.-]+`. 65 + pub fn validVersion(v: []const u8) bool { 66 + if (v.len == 0 or v.len > 32) return false; 67 + for (v) |c| { 68 + const ok = (c >= '0' and c <= '9') or (c >= 'a' and c <= 'z') or 69 + (c >= 'A' and c <= 'Z') or c == '.' or c == '-'; 70 + if (!ok) return false; 71 + } 72 + return true; 73 + } 74 + 75 + pub fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool { 76 + if (needle.len == 0) return true; 77 + if (needle.len > haystack.len) return false; 78 + var i: usize = 0; 79 + while (i + needle.len <= haystack.len) : (i += 1) { 80 + if (std.ascii.eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return true; 81 + } 82 + return false; 83 + } 84 + 85 + pub fn headingLevel(s: []const u8) ?u8 { 86 + if (s.len < 3 or s[0] != '<' or s[1] != 'h') return null; 87 + if (s[2] < '1' or s[2] > '6') return null; 88 + return s[2] - '0'; 89 + } 90 + 91 + pub fn attr(tag: []const u8, name: []const u8) ?[]const u8 { 92 + var key_buf: [32]u8 = undefined; 93 + const key = std.fmt.bufPrint(&key_buf, "{s}=\"", .{name}) catch return null; 94 + const start = std.mem.indexOf(u8, tag, key) orelse return null; 95 + const vstart = start + key.len; 96 + const vend = std.mem.indexOfScalarPos(u8, tag, vstart, '"') orelse return null; 97 + return tag[vstart..vend]; 98 + } 99 + 100 + /// Visible text of a heading: drop tags and the § pilcrow. Truncates to `buf`. 101 + fn cleanText(buf: []u8, inner: []const u8) []const u8 { 102 + var n: usize = 0; 103 + var k: usize = 0; 104 + var in_tag = false; 105 + while (k < inner.len and n < buf.len) { 106 + const c = inner[k]; 107 + if (c == '<') { 108 + in_tag = true; 109 + } else if (c == '>') { 110 + in_tag = false; 111 + } else if (!in_tag) { 112 + if (c == 0xC2 and k + 1 < inner.len and inner[k + 1] == 0xA7) { 113 + k += 2; 114 + continue; 115 + } 116 + buf[n] = c; 117 + n += 1; 118 + } 119 + k += 1; 120 + } 121 + return std.mem.trim(u8, buf[0..n], " "); 122 + } 123 + 124 + // ---------------------------------------------------------------------------- 125 + // tests 126 + // ---------------------------------------------------------------------------- 127 + 128 + const testing = std.testing; 129 + 130 + test "render: prose with inline code and entities" { 131 + const md = try render(testing.allocator, "<p>a &lt; b and <code>len</code></p>"); 132 + defer testing.allocator.free(md); 133 + try testing.expectEqualStrings("a < b and `len`", md); 134 + } 135 + 136 + test "render: fenced code block, spans and entities decoded" { 137 + const html = 138 + \\<pre><code><span class="tok-kw">const</span> x = <span class="tok-str">&quot;hi&quot;</span>;</code></pre> 139 + ; 140 + const md = try render(testing.allocator, html); 141 + defer testing.allocator.free(md); 142 + try testing.expectEqualStrings("```zig\nconst x = \"hi\";\n```", md); 143 + } 144 + 145 + test "render: table becomes markdown rows" { 146 + const html = "<table><tr><th>A</th><th>B</th></tr><tr><td>1</td><td>2</td></tr></table>"; 147 + const md = try render(testing.allocator, html); 148 + defer testing.allocator.free(md); 149 + try testing.expectEqualStrings("| A | B |\n| --- | --- |\n| 1 | 2 |", md); 150 + } 151 + 152 + test "sliceSection: bounded by next equal-rank heading" { 153 + const html = "<h2 id=\"a\">A</h2><p>x</p><h2 id=\"b\">B</h2>"; 154 + const sec = sliceSection(html, "a").?; 155 + try testing.expectEqualStrings("<h2 id=\"a\">A</h2><p>x</p>", sec); 156 + try testing.expect(sliceSection(html, "missing") == null); 157 + } 158 + 159 + test "sliceSection: subsections stay within parent" { 160 + const html = "<h2 id=\"p\">P</h2><h3 id=\"c\">C</h3><p>y</p><h2 id=\"q\">Q</h2>"; 161 + const sec = sliceSection(html, "p").?; 162 + try testing.expectEqualStrings("<h2 id=\"p\">P</h2><h3 id=\"c\">C</h3><p>y</p>", sec); 163 + } 164 + 165 + test "Sections: yields level, id, cleaned title" { 166 + const html = "<h2 id=\"Slices\"><a href=\"#toc\">Slices</a> <a class=\"hdr\">§</a></h2>" ++ 167 + "<h3 id=\"Sub\">Sub Thing</h3>"; 168 + var it = Sections.init(html); 169 + const a = it.next().?; 170 + try testing.expectEqual(@as(u8, 2), a.level); 171 + try testing.expectEqualStrings("Slices", a.id); 172 + try testing.expectEqualStrings("Slices", a.title); 173 + const b = it.next().?; 174 + try testing.expectEqual(@as(u8, 3), b.level); 175 + try testing.expectEqualStrings("Sub Thing", b.title); 176 + try testing.expect(it.next() == null); 177 + } 178 + 179 + test "validVersion: accepts releases and master, rejects path escapes" { 180 + try testing.expect(validVersion("master")); 181 + try testing.expect(validVersion("0.15.1")); 182 + try testing.expect(!validVersion("../../etc/passwd")); 183 + try testing.expect(!validVersion("a/b")); 184 + try testing.expect(!validVersion("")); 185 + } 186 + 187 + test "containsIgnoreCase" { 188 + try testing.expect(containsIgnoreCase("Slices", "slice")); 189 + try testing.expect(containsIgnoreCase("Optionals", "OPT")); 190 + try testing.expect(!containsIgnoreCase("Errors", "slice")); 191 + try testing.expect(containsIgnoreCase("anything", "")); 192 + }
+50
web/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <title>zigman — the zig language reference in your terminal</title> 7 + <style> 8 + :root { color-scheme: light dark; } 9 + body { max-width: 42rem; margin: 4rem auto; padding: 0 1.25rem; 10 + font: 16px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; } 11 + h1 { font-size: 1.6rem; margin-bottom: .25rem; } 12 + p.tag { margin-top: 0; opacity: .75; } 13 + pre { padding: 1rem; border-radius: .5rem; overflow-x: auto; 14 + background: #8881; } 15 + a { color: inherit; } 16 + .targets { opacity: .7; font-size: .85rem; } 17 + </style> 18 + </head> 19 + <body> 20 + <h1>zigman</h1> 21 + <p class="tag">read the <a href="https://ziglang.org/documentation/master/">zig language 22 + reference</a> in your terminal — for humans without a browser tab, and for agents that 23 + can't render one.</p> 24 + 25 + <h2>install</h2> 26 + 27 + <h3>build from source</h3> 28 + <p class="targets">requires zig 0.16+. clone, build, and the binary lands in 29 + <code>zig-out/bin/zigman</code>.</p> 30 + <pre>git clone https://tangled.org/zzstoatzz.io/zigman 31 + cd zigman 32 + zig build -Doptimize=ReleaseSafe 33 + ./zig-out/bin/zigman comptime</pre> 34 + 35 + <h3>or grab a prebuilt binary</h3> 36 + <pre>curl -fsSL https://nate.tngl.io/zigman/install.sh | sh</pre> 37 + <p class="targets">prebuilt for macos &amp; linux (aarch64 / x86_64). single static binary, 38 + no runtime. installs to <code>$ZIGMAN_INSTALL</code> or <code>~/.local/bin</code>.</p> 39 + 40 + <h2>use</h2> 41 + <pre>zigman comptime # show a section 42 + zigman search slice # find sections by title/anchor 43 + zigman list # every section, indented by depth 44 + zigman -v 0.15.1 Optionals # a specific langref version</pre> 45 + 46 + <h2>source</h2> 47 + <p><a href="https://tangled.org/zzstoatzz.io/zigman">tangled.org/zzstoatzz.io/zigman</a> 48 + — the parser/renderer is also a reusable zig module. MIT licensed.</p> 49 + </body> 50 + </html>
+42
web/install.sh
··· 1 + #!/bin/sh 2 + # install zigman — read the zig language reference in your terminal. 3 + # curl -fsSL https://nate.tngl.io/zigman/install.sh | sh 4 + set -eu 5 + 6 + base="https://nate.tngl.io/zigman/release" 7 + 8 + os=$(uname -s) 9 + arch=$(uname -m) 10 + case "$os" in 11 + Darwin) os=macos ;; 12 + Linux) os=linux ;; 13 + *) echo "zigman: unsupported OS '$os' — build from source: https://tangled.org/zzstoatzz.io/zigman" >&2; exit 1 ;; 14 + esac 15 + case "$arch" in 16 + arm64 | aarch64) arch=aarch64 ;; 17 + x86_64 | amd64) arch=x86_64 ;; 18 + *) echo "zigman: unsupported arch '$arch'" >&2; exit 1 ;; 19 + esac 20 + 21 + target="${arch}-${os}" 22 + dest="${ZIGMAN_INSTALL:-$HOME/.local/bin}" 23 + url="$base/$target.tar.gz" 24 + 25 + tmp=$(mktemp -d) 26 + trap 'rm -rf "$tmp"' EXIT 27 + echo "zigman: downloading $target" 28 + if ! curl -fSL "$url" -o "$tmp/zigman.tar.gz"; then 29 + echo "zigman: no prebuilt binary for $target at $url" >&2 30 + echo "zigman: build from source instead: https://tangled.org/zzstoatzz.io/zigman" >&2 31 + exit 1 32 + fi 33 + tar -xzf "$tmp/zigman.tar.gz" -C "$tmp" 34 + mkdir -p "$dest" 35 + mv "$tmp/zigman" "$dest/zigman" 36 + chmod +x "$dest/zigman" 37 + 38 + echo "zigman: installed to $dest/zigman" 39 + case ":$PATH:" in 40 + *":$dest:"*) ;; 41 + *) echo "zigman: add $dest to your PATH to run 'zigman'" ;; 42 + esac
+50
www/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <title>zigman — the zig language reference in your terminal</title> 7 + <style> 8 + :root { color-scheme: light dark; } 9 + body { max-width: 42rem; margin: 4rem auto; padding: 0 1.25rem; 10 + font: 16px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; } 11 + h1 { font-size: 1.6rem; margin-bottom: .25rem; } 12 + p.tag { margin-top: 0; opacity: .75; } 13 + pre { padding: 1rem; border-radius: .5rem; overflow-x: auto; 14 + background: #8881; } 15 + a { color: inherit; } 16 + .targets { opacity: .7; font-size: .85rem; } 17 + </style> 18 + </head> 19 + <body> 20 + <h1>zigman</h1> 21 + <p class="tag">read the <a href="https://ziglang.org/documentation/master/">zig language 22 + reference</a> in your terminal — for humans without a browser tab, and for agents that 23 + can't render one.</p> 24 + 25 + <h2>install</h2> 26 + 27 + <h3>build from source</h3> 28 + <p class="targets">requires zig 0.16+. clone, build, and the binary lands in 29 + <code>zig-out/bin/zigman</code>.</p> 30 + <pre>git clone https://tangled.org/zzstoatzz.io/zigman 31 + cd zigman 32 + zig build -Doptimize=ReleaseSafe 33 + ./zig-out/bin/zigman comptime</pre> 34 + 35 + <h3>or grab a prebuilt binary</h3> 36 + <pre>curl -fsSL https://nate.tngl.io/zigman/install.sh | sh</pre> 37 + <p class="targets">prebuilt for macos &amp; linux (aarch64 / x86_64). single static binary, 38 + no runtime. installs to <code>$ZIGMAN_INSTALL</code> or <code>~/.local/bin</code>.</p> 39 + 40 + <h2>use</h2> 41 + <pre>zigman comptime # show a section 42 + zigman search slice # find sections by title/anchor 43 + zigman list # every section, indented by depth 44 + zigman -v 0.15.1 Optionals # a specific langref version</pre> 45 + 46 + <h2>source</h2> 47 + <p><a href="https://tangled.org/zzstoatzz.io/zigman">tangled.org/zzstoatzz.io/zigman</a> 48 + — the parser/renderer is also a reusable zig module. MIT licensed.</p> 49 + </body> 50 + </html>
+42
www/install.sh
··· 1 + #!/bin/sh 2 + # install zigman — read the zig language reference in your terminal. 3 + # curl -fsSL https://nate.tngl.io/zigman/install.sh | sh 4 + set -eu 5 + 6 + base="https://nate.tngl.io/zigman/release" 7 + 8 + os=$(uname -s) 9 + arch=$(uname -m) 10 + case "$os" in 11 + Darwin) os=macos ;; 12 + Linux) os=linux ;; 13 + *) echo "zigman: unsupported OS '$os' — build from source: https://tangled.org/zzstoatzz.io/zigman" >&2; exit 1 ;; 14 + esac 15 + case "$arch" in 16 + arm64 | aarch64) arch=aarch64 ;; 17 + x86_64 | amd64) arch=x86_64 ;; 18 + *) echo "zigman: unsupported arch '$arch'" >&2; exit 1 ;; 19 + esac 20 + 21 + target="${arch}-${os}" 22 + dest="${ZIGMAN_INSTALL:-$HOME/.local/bin}" 23 + url="$base/$target.tar.gz" 24 + 25 + tmp=$(mktemp -d) 26 + trap 'rm -rf "$tmp"' EXIT 27 + echo "zigman: downloading $target" 28 + if ! curl -fSL "$url" -o "$tmp/zigman.tar.gz"; then 29 + echo "zigman: no prebuilt binary for $target at $url" >&2 30 + echo "zigman: build from source instead: https://tangled.org/zzstoatzz.io/zigman" >&2 31 + exit 1 32 + fi 33 + tar -xzf "$tmp/zigman.tar.gz" -C "$tmp" 34 + mkdir -p "$dest" 35 + mv "$tmp/zigman" "$dest/zigman" 36 + chmod +x "$dest/zigman" 37 + 38 + echo "zigman: installed to $dest/zigman" 39 + case ":$PATH:" in 40 + *":$dest:"*) ;; 41 + *) echo "zigman: add $dest to your PATH to run 'zigman'" ;; 42 + esac
www/release/aarch64-linux.tar.gz

This is a binary file and will not be displayed.

www/release/aarch64-macos.tar.gz

This is a binary file and will not be displayed.

www/release/x86_64-linux.tar.gz

This is a binary file and will not be displayed.

www/release/x86_64-macos.tar.gz

This is a binary file and will not be displayed.