Build step to convert markdown to man page (troff/groff)

Does anybody knows if there is a build step somewhere to convert a markdown document to a man page (groff/troff)?

I had a cursory look but coudn’t find anything.

Generic answer would be:

  • Run a system-installed program that does the conversion, or
  • Write a program that does the conversion and build/run that

See Running System Tools or Running the Project’s Tools

1 Like

No need install anything system wide, these days, you can use zig build system to fetch a binary directly.

I think pandoc can do markdown to manages conversion, so you can actually more or less directly re-use setup described in

build.zig.zon:

.{
  .name = "Build Zig Docs",
  .version = "0.0.0",
  .dependencies = .{
    .pandoc_macos_arm64 = .{
      .url = "https://github.com/jgm/pandoc/releases/download/3.4/pandoc-3.4-arm64-macOS.zip",
      .hash = "1220c2506a07845d667e7c127fd0811e4f5f7591e38ccc7fb4376450f3435048d87a",
      .lazy = true,
    },
    .pandoc_linux_amd64 = .{
      .url = "https://github.com/jgm/pandoc/releases/download/3.4/pandoc-3.4-linux-amd64.tar.gz",
      .hash = "1220139a44886509d8a61b44d8b8a79d03bad29ea95493dc97cd921d3f2eb208562c",
      .lazy = true,
    },
  },
  .paths = .{"."},
}

build.zig:

const std = @import("std");
const builtin = @import("builtin");
const os = builtin.os;
const cpu = builtin.cpu;
const assert = std.debug.assert;

pub fn build(b: *std.Build) !void {
    const pandoc_dependency = if (os.tag == .linux and cpu.arch == .x86_64)
        b.lazyDependency("pandoc_linux_amd64", .{}) orelse return
    else if (os.tag == .macos and cpu.arch == .aarch64)
        b.lazyDependency("pandoc_macos_arm64", .{}) orelse return
    else
        return error.UnsupportedHost;
    const pandoc = pandoc_dependency.path("bin/pandoc");

    const website = b.addWriteFiles();

    const markdown_files = b.run(&.{ "git", "ls-files", "content/*.md" });
    var lines = std.mem.tokenizeScalar(u8, markdown_files, '\n');
    while (lines.next()) |file_path| {
        const markdown = b.path(file_path);
        const html = markdown2html(b, pandoc, markdown);

        var html_path = file_path;
        html_path = cut_prefix(html_path, "content/").?;
        html_path = cut_suffix(html_path, ".md").?;
        html_path = b.fmt("{s}.html", .{html_path});

        _ = website.addCopyFile(html, html_path);
    }

    b.installDirectory(.{
        .source_dir = website.getDirectory(),
        .install_dir = .prefix,
        .install_subdir = ".",
    });
}

fn markdown2html(
    b: *std.Build,
    pandoc: std.Build.LazyPath,
    markdown: std.Build.LazyPath,
) std.Build.LazyPath {
    const pandoc_step = std.Build.Step.Run.create(b, "run pandoc");
    pandoc_step.addFileArg(pandoc);
    pandoc_step.addArgs(&.{ "--from=markdown", "--to=html5" });
    pandoc_step.addFileArg(markdown);
    return pandoc_step.captureStdOut();
}

fn cut_prefix(text: []const u8, prefix: []const u8) ?[]const u8 {
    if (std.mem.startsWith(u8, text, prefix)) return text[prefix.len..];
    return null;
}

fn cut_suffix(text: []const u8, suffix: []const u8) ?[]const u8 {
    if (std.mem.endsWith(u8, text, suffix)) return text[0 .. text.len - suffix.len];
    return null;
}
2 Likes

I ported the excellent scdoc to zig for almost this exact purpose. Scdoc syntax is not exactly the same as markdown, but it’s close and pretty tailored to manpages

2 Likes

More details for those interested: Introducing scdoc, a man page generator