//! 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 const std = @import("std"); const Io = std.Io; const Writer = Io.Writer; const head = \\ \\ \\ \\ \\ \\zigman guide \\ \\ \\ \\ \\ \\
\\
\\ \\
\\ ; const foot = \\
\\
\\
\\ \\ \\ ; 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("

"); try inlineMd(ww, p.items); try ww.writeAll("

\n"); p.clearRetainingCapacity(); } if (list.*) { try ww.writeAll("\n"); list.* = false; } } }.call; while (lines.next()) |line| { // pass raw
/ 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("
");
            while (lines.next()) |code| {
                if (std.mem.startsWith(u8, code, "```")) break;
                try escape(w, code);
                try w.writeByte('\n');
            }
            try w.writeAll("
\n"); continue; } const heading = headingLevel(line); if (heading > 0) { try flush(w, ¶, &in_list); try w.print("", .{heading}); try inlineMd(w, std.mem.trimStart(u8, line[heading..], " ")); try w.print("\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("
    \n"); in_list = true; } try w.writeAll("
  • "); try inlineMd(w, line[2..]); try w.writeAll("
  • \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(""); try escape(w, text[i + 1 .. end]); try w.writeAll(""); i = end + 1; continue; } } else if (c == '[') { if (parseLink(text[i..])) |lnk| { try w.writeAll(""); try escape(w, lnk.text); try w.writeAll(""); 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
    / 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{ " 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), } }