Something went wrong. Try again.
zig langref cli nate.tngl.io/zigman
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215//! Tiny markdown -> HTML converter, used at build time to render docs into//! the project's static site (web served by tangled). Deliberately minimal: it//! supports exactly the constructs the README uses — headings, fenced code,//! unordered lists, paragraphs, inline code, and links. Not a general engine.//!//! usage: md2html <input.md> <output.html>
const std = @import("std");const Io = std.Io;const Writer = Io.Writer;
const head = \\<!doctype html> \\<html lang="en"> \\<head> \\<meta charset="utf-8"> \\<meta name="viewport" content="width=device-width, initial-scale=1"> \\<title>zigman guide</title> \\<meta name="description" content="Practical guide for using zigman in shell workflows and agent loops."> \\<link rel="stylesheet" href="site.css"> \\<script defer src="nav.js"></script> \\</head> \\<body> \\<main> \\ <section class="window" aria-labelledby="title"> \\ <nav class="bar" aria-label="Primary"> \\ <a data-nav-item data-hotkey="h" data-nav-target="home" href="index.html">home</a> \\ <a data-nav-item data-hotkey="p" href="prompt.md">setup</a> \\ <a data-nav-item data-hotkey="l" data-nav-target="llms" href="llms.txt">llms</a> \\ <a data-nav-item data-hotkey="s" href="https://tangled.org/zzstoatzz.io/zigman">source</a> \\ <a data-nav-item data-hotkey="r" href="https://ziglang.org/documentation/master/">langref</a> \\ </nav> \\ <article class="doc"> \\;
const foot = \\ </article> \\ </section> \\</main> \\</body> \\</html> \\;
pub fn main(init: std.process.Init) !void { const a = init.gpa; const io = init.io;
var it = init.minimal.args.iterate(); _ = it.skip(); const in_path = it.next() orelse return error.MissingInput; const out_path = it.next() orelse return error.MissingOutput;
const cwd = Io.Dir.cwd(); const md = try cwd.readFileAlloc(io, in_path, a, .unlimited); defer a.free(md);
var out: Writer.Allocating = .init(a); defer out.deinit(); const w = &out.writer;
try w.writeAll(head); try convert(a, w, md); try w.writeAll(foot);
try cwd.writeFile(io, .{ .sub_path = out_path, .data = out.written() });}
fn convert(a: std.mem.Allocator, w: *Writer, md: []const u8) !void { var lines = std.mem.splitScalar(u8, md, '\n'); var in_list = false; var para: std.ArrayList(u8) = .empty; defer para.deinit(a);
// close any open block (paragraph or list) before starting a new one const flush = struct { fn call(ww: *Writer, p: *std.ArrayList(u8), list: *bool) !void { if (p.items.len > 0) { try ww.writeAll("<p>"); try inlineMd(ww, p.items); try ww.writeAll("</p>\n"); p.clearRetainingCapacity(); } if (list.*) { try ww.writeAll("</ul>\n"); list.* = false; } } }.call;
while (lines.next()) |line| { // pass raw <details>/<summary> block tags through untouched if (rawBlock(std.mem.trim(u8, line, " \t"))) { try flush(w, ¶, &in_list); try w.writeAll(line); try w.writeByte('\n'); continue; }
if (std.mem.startsWith(u8, line, "```")) { try flush(w, ¶, &in_list); try w.writeAll("<pre><code>"); while (lines.next()) |code| { if (std.mem.startsWith(u8, code, "```")) break; try escape(w, code); try w.writeByte('\n'); } try w.writeAll("</code></pre>\n"); continue; }
const heading = headingLevel(line); if (heading > 0) { try flush(w, ¶, &in_list); try w.print("<h{d}>", .{heading}); try inlineMd(w, std.mem.trimStart(u8, line[heading..], " ")); try w.print("</h{d}>\n", .{heading}); continue; }
if (std.mem.startsWith(u8, line, "- ")) { if (para.items.len > 0) try flush(w, ¶, &in_list); if (!in_list) { try w.writeAll("<ul>\n"); in_list = true; } try w.writeAll("<li>"); try inlineMd(w, line[2..]); try w.writeAll("</li>\n"); continue; }
if (std.mem.trim(u8, line, " \t").len == 0) { try flush(w, ¶, &in_list); continue; }
// plain prose line: accumulate into the current paragraph, joining // wrapped lines with a space if (in_list) try flush(w, ¶, &in_list); if (para.items.len > 0) try para.append(a, ' '); try para.appendSlice(a, line); } try flush(w, ¶, &in_list);}
/// inline markdown: `code` spans and [text](url) links; everything else escaped.fn inlineMd(w: *Writer, text: []const u8) !void { var i: usize = 0; while (i < text.len) { const c = text[i]; if (c == '`') { if (std.mem.indexOfScalarPos(u8, text, i + 1, '`')) |end| { try w.writeAll("<code>"); try escape(w, text[i + 1 .. end]); try w.writeAll("</code>"); i = end + 1; continue; } } else if (c == '[') { if (parseLink(text[i..])) |lnk| { try w.writeAll("<a href=\""); try escape(w, lnk.url); try w.writeAll("\">"); try escape(w, lnk.text); try w.writeAll("</a>"); i += lnk.len; continue; } } try escapeByte(w, c); i += 1; }}
const Link = struct { text: []const u8, url: []const u8, len: usize };
fn parseLink(s: []const u8) ?Link { const close = std.mem.indexOfScalar(u8, s, ']') orelse return null; if (close + 1 >= s.len or s[close + 1] != '(') return null; const paren = std.mem.indexOfScalarPos(u8, s, close + 2, ')') orelse return null; return .{ .text = s[1..close], .url = s[close + 2 .. paren], .len = paren + 1 };}
/// lines that are a raw <details>/<summary> tag are emitted verbatim so the/// README's collapsible sections render in both the repo view and the page.fn rawBlock(line: []const u8) bool { for ([_][]const u8{ "<details", "</details", "<summary", "</summary" }) |tag| { if (std.mem.startsWith(u8, line, tag)) return true; } return false;}
fn headingLevel(line: []const u8) u8 { var n: u8 = 0; while (n < line.len and line[n] == '#') n += 1; if (n > 0 and n < line.len and line[n] == ' ') return n; return 0;}
fn escape(w: *Writer, s: []const u8) !void { for (s) |c| try escapeByte(w, c);}
fn escapeByte(w: *Writer, c: u8) !void { switch (c) { '&' => try w.writeAll("&"), '<' => try w.writeAll("<"), '>' => try w.writeAll(">"), '"' => try w.writeAll("""), else => try w.writeByte(c), }}