/// Build-time generator: extracts the set of backticked identifiers /// from each `docs/reference/config/*.md` reference page and emits them /// as a Zig source file. /// /// Exists because `@embedFile` cannot escape the package path, so /// `src/` code has no way to read `docs/`. The alternative - reading /// the markdown at test time via cwd - would make the doc-sync check /// dependent on where the test binary was launched from, and a check /// that can silently skip is a check that rots. /// /// Only the NAME SET is extracted, never the prose. That keeps the /// output free of string-escaping concerns and small (a few hundred /// identifiers), and it is exactly what the consumer needs: /// `srf_lint`'s doc-sync test asserts every field of each model appears /// in that model's reference page. /// /// An "identifier" here is a backtick-delimited token matching /// `[a-z][a-z0-9_]*` - the shape of every SRF field name. Prose words /// in backticks (`true`, `null`, `stock`) come along harmlessly; the /// test only ever asks whether a known field name is PRESENT, never /// whether an extracted name is a real field. const std = @import("std"); pub fn main(init: std.process.Init) !void { const allocator = init.arena.allocator(); const io = init.io; const args = try init.minimal.args.toSlice(allocator); if (args.len < 3) { var stderr_buf: [160]u8 = undefined; var stderr = std.Io.File.stderr().writer(io, &stderr_buf); try stderr.interface.writeAll("Usage: gen_config_docs ...\n"); try stderr.interface.flush(); std.process.exit(1); } const out_file = try std.Io.Dir.cwd().createFile(io, args[1], .{}); defer out_file.close(io); var out_buf: [4096]u8 = undefined; var file_writer = out_file.writer(io, &out_buf); const w = &file_writer.interface; try w.writeAll( \\// Auto-generated from docs/reference/config/*.md - do not edit. \\// Regenerate: zig build (runs build/gen_config_docs.zig) \\ \\/// One reference page's backticked identifiers. \\pub const Doc = struct { \\ /// Basename, e.g. "portfolio-srf.md". \\ file: []const u8, \\ names: []const []const u8, \\}; \\ \\pub const docs = [_]Doc{ \\ ); for (args[2..]) |path| { const body = try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(4 * 1024 * 1024)); const base = std.fs.path.basename(path); var names: std.ArrayList([]const u8) = .empty; try collectIdents(allocator, body, &names); try w.print(" .{{ .file = \"{s}\", .names = &.{{", .{base}); for (names.items, 0..) |n, i| { if (i % 6 == 0) try w.writeAll("\n "); try w.print("\"{s}\", ", .{n}); } try w.writeAll("\n } },\n"); } try w.writeAll( \\}; \\ \\/// The `Doc` for `file`, or null when that page was not built in. \\pub fn find(file: []const u8) ?Doc { \\ for (docs) |d| { \\ if (std.mem.eql(u8, d.file, file)) return d; \\ } \\ return null; \\} \\ \\const std = @import("std"); \\ ); try w.flush(); } /// Append every deduplicated backticked `[a-z][a-z0-9_]*` token in /// `body` to `out`. Slices borrow from `body`. fn collectIdents( allocator: std.mem.Allocator, body: []const u8, out: *std.ArrayList([]const u8), ) !void { var i: usize = 0; while (i < body.len) { if (body[i] != '`') { i += 1; continue; } // Skip fenced code blocks wholesale - a ```zig sample can // mention a field in a context the reference table does not, // and counting it would let a field be "documented" by an // example alone. if (std.mem.startsWith(u8, body[i..], "```")) { const rest = body[i + 3 ..]; const close = std.mem.indexOf(u8, rest, "```") orelse break; i += 3 + close + 3; continue; } const rest = body[i + 1 ..]; const close = std.mem.indexOfScalar(u8, rest, '`') orelse break; const raw = rest[0..close]; i += 1 + close + 1; // The docs name a field both bare (`symbol`, in reference // tables) and on-wire (`symbol::`, `shares:num:100`, in prose // and examples). Cut at the first separator so both spellings // count as documenting the field. const tok = raw[0 .. std.mem.indexOfScalar(u8, raw, ':') orelse raw.len]; if (!isIdent(tok)) continue; for (out.items) |seen| { if (std.mem.eql(u8, seen, tok)) break; } else { try out.append(allocator, tok); } } } fn isIdent(tok: []const u8) bool { if (tok.len == 0) return false; if (tok[0] < 'a' or tok[0] > 'z') return false; for (tok) |c| { const ok = (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_'; if (!ok) return false; } return true; }