master
   1const builtin = @import("builtin");
   2
   3const std = @import("std");
   4const Io = std.Io;
   5const Writer = std.Io.Writer;
   6const fatal = std.process.fatal;
   7const mem = std.mem;
   8const fs = std.fs;
   9const process = std.process;
  10const Allocator = std.mem.Allocator;
  11const testing = std.testing;
  12const getExternalExecutor = std.zig.system.getExternalExecutor;
  13
  14const max_doc_file_size = 10 * 1024 * 1024;
  15
  16const usage =
  17    \\Usage: doctest [options] -i input -o output
  18    \\
  19    \\   Compiles and possibly runs a code example, capturing output and rendering
  20    \\   it to HTML documentation.
  21    \\
  22    \\Options:
  23    \\   -h, --help             Print this help and exit
  24    \\   -i input               Source code file path
  25    \\   -o output              Where to write output HTML docs to
  26    \\   --zig zig              Path to the zig compiler
  27    \\   --zig-lib-dir dir      Override the zig compiler library path
  28    \\   --cache-root dir       Path to local .zig-cache/
  29    \\
  30;
  31
  32pub fn main() !void {
  33    var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
  34    defer arena_instance.deinit();
  35
  36    const arena = arena_instance.allocator();
  37
  38    var args_it = try process.argsWithAllocator(arena);
  39    if (!args_it.skip()) fatal("missing argv[0]", .{});
  40
  41    const gpa = arena;
  42
  43    var threaded: std.Io.Threaded = .init(gpa);
  44    defer threaded.deinit();
  45    const io = threaded.io();
  46
  47    var opt_input: ?[]const u8 = null;
  48    var opt_output: ?[]const u8 = null;
  49    var opt_zig: ?[]const u8 = null;
  50    var opt_zig_lib_dir: ?[]const u8 = null;
  51    var opt_cache_root: ?[]const u8 = null;
  52
  53    while (args_it.next()) |arg| {
  54        if (mem.startsWith(u8, arg, "-")) {
  55            if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
  56                try std.fs.File.stdout().writeAll(usage);
  57                process.exit(0);
  58            } else if (mem.eql(u8, arg, "-i")) {
  59                opt_input = args_it.next() orelse fatal("expected parameter after -i", .{});
  60            } else if (mem.eql(u8, arg, "-o")) {
  61                opt_output = args_it.next() orelse fatal("expected parameter after -o", .{});
  62            } else if (mem.eql(u8, arg, "--zig")) {
  63                opt_zig = args_it.next() orelse fatal("expected parameter after --zig", .{});
  64            } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
  65                opt_zig_lib_dir = args_it.next() orelse fatal("expected parameter after --zig-lib-dir", .{});
  66            } else if (mem.eql(u8, arg, "--cache-root")) {
  67                opt_cache_root = args_it.next() orelse fatal("expected parameter after --cache-root", .{});
  68            } else {
  69                fatal("unrecognized option: '{s}'", .{arg});
  70            }
  71        } else {
  72            fatal("unexpected positional argument: '{s}'", .{arg});
  73        }
  74    }
  75
  76    const input_path = opt_input orelse fatal("missing input file (-i)", .{});
  77    const output_path = opt_output orelse fatal("missing output file (-o)", .{});
  78    const zig_path = opt_zig orelse fatal("missing zig compiler path (--zig)", .{});
  79    const cache_root = opt_cache_root orelse fatal("missing cache root path (--cache-root)", .{});
  80
  81    const source_bytes = try fs.cwd().readFileAlloc(input_path, arena, .limited(std.math.maxInt(u32)));
  82    const code = try parseManifest(arena, source_bytes);
  83    const source = stripManifest(source_bytes);
  84
  85    const tmp_dir_path = try std.fmt.allocPrint(arena, "{s}/tmp/{x}", .{
  86        cache_root, std.crypto.random.int(u64),
  87    });
  88    fs.cwd().makePath(tmp_dir_path) catch |err|
  89        fatal("unable to create tmp dir '{s}': {s}", .{ tmp_dir_path, @errorName(err) });
  90    defer fs.cwd().deleteTree(tmp_dir_path) catch |err| std.log.err("unable to delete '{s}': {s}", .{
  91        tmp_dir_path, @errorName(err),
  92    });
  93
  94    var out_file = try fs.cwd().createFile(output_path, .{});
  95    defer out_file.close();
  96    var out_file_buffer: [4096]u8 = undefined;
  97    var out_file_writer = out_file.writer(&out_file_buffer);
  98
  99    const out = &out_file_writer.interface;
 100
 101    try printSourceBlock(arena, out, source, fs.path.basename(input_path));
 102    try printOutput(
 103        arena,
 104        io,
 105        out,
 106        code,
 107        tmp_dir_path,
 108        try std.fs.path.relative(arena, tmp_dir_path, zig_path),
 109        try std.fs.path.relative(arena, tmp_dir_path, input_path),
 110        if (opt_zig_lib_dir) |zig_lib_dir|
 111            try std.fs.path.relative(arena, tmp_dir_path, zig_lib_dir)
 112        else
 113            null,
 114    );
 115
 116    try out_file_writer.end();
 117}
 118
 119fn printOutput(
 120    arena: Allocator,
 121    io: Io,
 122    out: *Writer,
 123    code: Code,
 124    /// Relative to this process' cwd.
 125    tmp_dir_path: []const u8,
 126    /// Relative to `tmp_dir_path`.
 127    zig_exe: []const u8,
 128    /// Relative to `tmp_dir_path`.
 129    input_path: []const u8,
 130    /// Relative to `tmp_dir_path`.
 131    opt_zig_lib_dir: ?[]const u8,
 132) !void {
 133    var env_map = try process.getEnvMap(arena);
 134    try env_map.put("CLICOLOR_FORCE", "1");
 135
 136    const host = try std.zig.system.resolveTargetQuery(io, .{});
 137    const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch);
 138    const print = std.debug.print;
 139
 140    var shell_buffer: Writer.Allocating = .init(arena);
 141    defer shell_buffer.deinit();
 142    const shell_out = &shell_buffer.writer;
 143
 144    const code_name = std.fs.path.stem(input_path);
 145
 146    switch (code.id) {
 147        .exe => |expected_outcome| code_block: {
 148            var build_args = std.array_list.Managed([]const u8).init(arena);
 149            defer build_args.deinit();
 150            try build_args.appendSlice(&[_][]const u8{
 151                zig_exe,    "build-exe",
 152                "--name",   code_name,
 153                "--color",  "on",
 154                input_path,
 155            });
 156            if (opt_zig_lib_dir) |zig_lib_dir| {
 157                try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
 158            }
 159
 160            try shell_out.print("$ zig build-exe {s}.zig ", .{code_name});
 161
 162            switch (code.mode) {
 163                .Debug => {},
 164                else => {
 165                    try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
 166                    try shell_out.print("-O {s} ", .{@tagName(code.mode)});
 167                },
 168            }
 169            for (code.link_objects) |link_object| {
 170                const name_with_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ link_object, obj_ext });
 171                try build_args.append(name_with_ext);
 172                try shell_out.print("{s} ", .{name_with_ext});
 173            }
 174            if (code.link_libc) {
 175                try build_args.append("-lc");
 176                try shell_out.print("-lc ", .{});
 177            }
 178
 179            if (code.target_str) |triple| {
 180                try build_args.appendSlice(&[_][]const u8{ "-target", triple });
 181                try shell_out.print("-target {s} ", .{triple});
 182            }
 183            if (code.use_llvm) |use_llvm| {
 184                if (use_llvm) {
 185                    try build_args.append("-fllvm");
 186                    try shell_out.print("-fllvm", .{});
 187                } else {
 188                    try build_args.append("-fno-llvm");
 189                    try shell_out.print("-fno-llvm", .{});
 190                }
 191            }
 192            if (code.verbose_cimport) {
 193                try build_args.append("--verbose-cimport");
 194                try shell_out.print("--verbose-cimport ", .{});
 195            }
 196            for (code.additional_options) |option| {
 197                try build_args.append(option);
 198                try shell_out.print("{s} ", .{option});
 199            }
 200
 201            try shell_out.print("\n", .{});
 202
 203            if (expected_outcome == .build_fail) {
 204                const result = try process.Child.run(.{
 205                    .allocator = arena,
 206                    .argv = build_args.items,
 207                    .cwd = tmp_dir_path,
 208                    .env_map = &env_map,
 209                    .max_output_bytes = max_doc_file_size,
 210                });
 211                switch (result.term) {
 212                    .Exited => |exit_code| {
 213                        if (exit_code == 0) {
 214                            print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
 215                            dumpArgs(build_args.items);
 216                            fatal("example incorrectly compiled", .{});
 217                        }
 218                    },
 219                    else => {
 220                        print("{s}\nThe following command crashed:\n", .{result.stderr});
 221                        dumpArgs(build_args.items);
 222                        fatal("example compile crashed", .{});
 223                    },
 224                }
 225                const escaped_stderr = try escapeHtml(arena, result.stderr);
 226                const colored_stderr = try termColor(arena, escaped_stderr);
 227                try shell_out.writeAll(colored_stderr);
 228                break :code_block;
 229            }
 230            const exec_result = run(arena, &env_map, tmp_dir_path, build_args.items) catch
 231                fatal("example failed to compile", .{});
 232
 233            if (code.verbose_cimport) {
 234                const escaped_build_stderr = try escapeHtml(arena, exec_result.stderr);
 235                try shell_out.writeAll(escaped_build_stderr);
 236            }
 237
 238            if (code.target_str) |triple| {
 239                if (mem.startsWith(u8, triple, "wasm32") or
 240                    mem.startsWith(u8, triple, "riscv64-linux") or
 241                    (mem.startsWith(u8, triple, "x86_64-linux") and
 242                        builtin.os.tag != .linux or builtin.cpu.arch != .x86_64))
 243                {
 244                    // skip execution
 245                    break :code_block;
 246                }
 247            }
 248            const target_query = try std.Target.Query.parse(.{
 249                .arch_os_abi = code.target_str orelse "native",
 250            });
 251            const target = try std.zig.system.resolveTargetQuery(io, target_query);
 252
 253            const path_to_exe = try std.fmt.allocPrint(arena, "./{s}{s}", .{
 254                code_name, target.exeFileExt(),
 255            });
 256            const run_args = &[_][]const u8{path_to_exe};
 257
 258            var exited_with_signal = false;
 259
 260            const result = if (expected_outcome == .fail) blk: {
 261                const result = try process.Child.run(.{
 262                    .allocator = arena,
 263                    .argv = run_args,
 264                    .env_map = &env_map,
 265                    .cwd = tmp_dir_path,
 266                    .max_output_bytes = max_doc_file_size,
 267                });
 268                switch (result.term) {
 269                    .Exited => |exit_code| {
 270                        if (exit_code == 0) {
 271                            print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
 272                            dumpArgs(run_args);
 273                            fatal("example incorrectly compiled", .{});
 274                        }
 275                    },
 276                    .Signal => exited_with_signal = true,
 277                    else => {},
 278                }
 279                break :blk result;
 280            } else blk: {
 281                break :blk run(arena, &env_map, tmp_dir_path, run_args) catch
 282                    fatal("example crashed", .{});
 283            };
 284
 285            const escaped_stderr = try escapeHtml(arena, result.stderr);
 286            const escaped_stdout = try escapeHtml(arena, result.stdout);
 287
 288            const colored_stderr = try termColor(arena, escaped_stderr);
 289            const colored_stdout = try termColor(arena, escaped_stdout);
 290
 291            try shell_out.print("$ ./{s}\n{s}{s}", .{ code_name, colored_stdout, colored_stderr });
 292            if (exited_with_signal) {
 293                try shell_out.print("(process terminated by signal)", .{});
 294            }
 295            try shell_out.writeAll("\n");
 296        },
 297        .@"test" => {
 298            var test_args = std.array_list.Managed([]const u8).init(arena);
 299            defer test_args.deinit();
 300
 301            try test_args.appendSlice(&[_][]const u8{
 302                zig_exe, "test", input_path,
 303            });
 304            if (opt_zig_lib_dir) |zig_lib_dir| {
 305                try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
 306            }
 307            try shell_out.print("$ zig test {s}.zig ", .{code_name});
 308
 309            switch (code.mode) {
 310                .Debug => {},
 311                else => {
 312                    try test_args.appendSlice(&[_][]const u8{
 313                        "-O", @tagName(code.mode),
 314                    });
 315                    try shell_out.print("-O {s} ", .{@tagName(code.mode)});
 316                },
 317            }
 318            if (code.link_libc) {
 319                try test_args.append("-lc");
 320                try shell_out.print("-lc ", .{});
 321            }
 322            if (code.target_str) |triple| {
 323                try test_args.appendSlice(&[_][]const u8{ "-target", triple });
 324                try shell_out.print("-target {s} ", .{triple});
 325
 326                const target_query = try std.Target.Query.parse(.{
 327                    .arch_os_abi = triple,
 328                });
 329                const target = try std.zig.system.resolveTargetQuery(io, target_query);
 330                switch (getExternalExecutor(&host, &target, .{
 331                    .link_libc = code.link_libc,
 332                })) {
 333                    .native => {},
 334                    else => {
 335                        try test_args.appendSlice(&[_][]const u8{"--test-no-exec"});
 336                        try shell_out.writeAll("--test-no-exec");
 337                    },
 338                }
 339            }
 340            if (code.use_llvm) |use_llvm| {
 341                if (use_llvm) {
 342                    try test_args.append("-fllvm");
 343                    try shell_out.print("-fllvm", .{});
 344                } else {
 345                    try test_args.append("-fno-llvm");
 346                    try shell_out.print("-fno-llvm", .{});
 347                }
 348            }
 349
 350            const result = run(arena, &env_map, tmp_dir_path, test_args.items) catch
 351                fatal("test failed", .{});
 352            const escaped_stderr = try escapeHtml(arena, result.stderr);
 353            const escaped_stdout = try escapeHtml(arena, result.stdout);
 354            try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
 355        },
 356        .test_error => |error_match| {
 357            var test_args = std.array_list.Managed([]const u8).init(arena);
 358            defer test_args.deinit();
 359
 360            try test_args.appendSlice(&[_][]const u8{
 361                zig_exe,    "test",
 362                "--color",  "on",
 363                input_path,
 364            });
 365            if (opt_zig_lib_dir) |zig_lib_dir| {
 366                try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
 367            }
 368            try shell_out.print("$ zig test {s}.zig ", .{code_name});
 369
 370            switch (code.mode) {
 371                .Debug => {},
 372                else => {
 373                    try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
 374                    try shell_out.print("-O {s} ", .{@tagName(code.mode)});
 375                },
 376            }
 377            if (code.link_libc) {
 378                try test_args.append("-lc");
 379                try shell_out.print("-lc ", .{});
 380            }
 381            const result = try process.Child.run(.{
 382                .allocator = arena,
 383                .argv = test_args.items,
 384                .env_map = &env_map,
 385                .cwd = tmp_dir_path,
 386                .max_output_bytes = max_doc_file_size,
 387            });
 388            switch (result.term) {
 389                .Exited => |exit_code| {
 390                    if (exit_code == 0) {
 391                        print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
 392                        dumpArgs(test_args.items);
 393                        fatal("example incorrectly compiled", .{});
 394                    }
 395                },
 396                else => {
 397                    print("{s}\nThe following command crashed:\n", .{result.stderr});
 398                    dumpArgs(test_args.items);
 399                    fatal("example compile crashed", .{});
 400                },
 401            }
 402            if (mem.indexOf(u8, result.stderr, error_match) == null) {
 403                print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
 404                fatal("example did not have expected compile error", .{});
 405            }
 406            const escaped_stderr = try escapeHtml(arena, result.stderr);
 407            const colored_stderr = try termColor(arena, escaped_stderr);
 408            try shell_out.print("\n{s}\n", .{colored_stderr});
 409        },
 410        .test_safety => |error_match| {
 411            var test_args = std.array_list.Managed([]const u8).init(arena);
 412            defer test_args.deinit();
 413
 414            try test_args.appendSlice(&[_][]const u8{
 415                zig_exe,    "test",
 416                input_path,
 417            });
 418            if (opt_zig_lib_dir) |zig_lib_dir| {
 419                try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
 420            }
 421            var mode_arg: []const u8 = "";
 422            switch (code.mode) {
 423                .Debug => {},
 424                .ReleaseSafe => {
 425                    try test_args.append("-OReleaseSafe");
 426                    mode_arg = "-OReleaseSafe";
 427                },
 428                .ReleaseFast => {
 429                    try test_args.append("-OReleaseFast");
 430                    mode_arg = "-OReleaseFast";
 431                },
 432                .ReleaseSmall => {
 433                    try test_args.append("-OReleaseSmall");
 434                    mode_arg = "-OReleaseSmall";
 435                },
 436            }
 437
 438            const result = try process.Child.run(.{
 439                .allocator = arena,
 440                .argv = test_args.items,
 441                .env_map = &env_map,
 442                .cwd = tmp_dir_path,
 443                .max_output_bytes = max_doc_file_size,
 444            });
 445            switch (result.term) {
 446                .Exited => |exit_code| {
 447                    if (exit_code == 0) {
 448                        print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
 449                        dumpArgs(test_args.items);
 450                        fatal("example test incorrectly succeeded", .{});
 451                    }
 452                },
 453                else => {
 454                    print("{s}\nThe following command crashed:\n", .{result.stderr});
 455                    dumpArgs(test_args.items);
 456                    fatal("example compile crashed", .{});
 457                },
 458            }
 459            if (mem.indexOf(u8, result.stderr, error_match) == null) {
 460                print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
 461                fatal("example did not have expected runtime safety error message", .{});
 462            }
 463            const escaped_stderr = try escapeHtml(arena, result.stderr);
 464            const colored_stderr = try termColor(arena, escaped_stderr);
 465            try shell_out.print("$ zig test {s}.zig {s}\n{s}\n", .{
 466                code_name,
 467                mode_arg,
 468                colored_stderr,
 469            });
 470        },
 471        .obj => |maybe_error_match| {
 472            const name_plus_obj_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ code_name, obj_ext });
 473            var build_args = std.array_list.Managed([]const u8).init(arena);
 474            defer build_args.deinit();
 475
 476            try build_args.appendSlice(&[_][]const u8{
 477                zig_exe,    "build-obj",
 478                "--color",  "on",
 479                "--name",   code_name,
 480                input_path, try std.fmt.allocPrint(arena, "-femit-bin={s}", .{name_plus_obj_ext}),
 481            });
 482            if (opt_zig_lib_dir) |zig_lib_dir| {
 483                try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
 484            }
 485
 486            try shell_out.print("$ zig build-obj {s}.zig ", .{code_name});
 487
 488            switch (code.mode) {
 489                .Debug => {},
 490                else => {
 491                    try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
 492                    try shell_out.print("-O {s} ", .{@tagName(code.mode)});
 493                },
 494            }
 495
 496            if (code.target_str) |triple| {
 497                try build_args.appendSlice(&[_][]const u8{ "-target", triple });
 498                try shell_out.print("-target {s} ", .{triple});
 499            }
 500            if (code.use_llvm) |use_llvm| {
 501                if (use_llvm) {
 502                    try build_args.append("-fllvm");
 503                    try shell_out.print("-fllvm", .{});
 504                } else {
 505                    try build_args.append("-fno-llvm");
 506                    try shell_out.print("-fno-llvm", .{});
 507                }
 508            }
 509            for (code.additional_options) |option| {
 510                try build_args.append(option);
 511                try shell_out.print("{s} ", .{option});
 512            }
 513
 514            if (maybe_error_match) |error_match| {
 515                const result = try process.Child.run(.{
 516                    .allocator = arena,
 517                    .argv = build_args.items,
 518                    .env_map = &env_map,
 519                    .cwd = tmp_dir_path,
 520                    .max_output_bytes = max_doc_file_size,
 521                });
 522                switch (result.term) {
 523                    .Exited => |exit_code| {
 524                        if (exit_code == 0) {
 525                            print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
 526                            dumpArgs(build_args.items);
 527                            fatal("example build incorrectly succeeded", .{});
 528                        }
 529                    },
 530                    else => {
 531                        print("{s}\nThe following command crashed:\n", .{result.stderr});
 532                        dumpArgs(build_args.items);
 533                        fatal("example compile crashed", .{});
 534                    },
 535                }
 536                if (mem.indexOf(u8, result.stderr, error_match) == null) {
 537                    print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
 538                    fatal("example did not have expected compile error message", .{});
 539                }
 540                const escaped_stderr = try escapeHtml(arena, result.stderr);
 541                const colored_stderr = try termColor(arena, escaped_stderr);
 542                try shell_out.print("\n{s} ", .{colored_stderr});
 543            } else {
 544                _ = run(arena, &env_map, tmp_dir_path, build_args.items) catch fatal("example failed to compile", .{});
 545            }
 546            try shell_out.writeAll("\n");
 547        },
 548        .lib => {
 549            const bin_basename = try std.zig.binNameAlloc(arena, .{
 550                .root_name = code_name,
 551                .target = &builtin.target,
 552                .output_mode = .Lib,
 553            });
 554
 555            var test_args = std.array_list.Managed([]const u8).init(arena);
 556            defer test_args.deinit();
 557
 558            try test_args.appendSlice(&[_][]const u8{
 559                zig_exe,    "build-lib",
 560                input_path, try std.fmt.allocPrint(arena, "-femit-bin={s}", .{bin_basename}),
 561            });
 562            if (opt_zig_lib_dir) |zig_lib_dir| {
 563                try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
 564            }
 565            try shell_out.print("$ zig build-lib {s}.zig ", .{code_name});
 566
 567            switch (code.mode) {
 568                .Debug => {},
 569                else => {
 570                    try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
 571                    try shell_out.print("-O {s} ", .{@tagName(code.mode)});
 572                },
 573            }
 574            if (code.target_str) |triple| {
 575                try test_args.appendSlice(&[_][]const u8{ "-target", triple });
 576                try shell_out.print("-target {s} ", .{triple});
 577            }
 578            if (code.use_llvm) |use_llvm| {
 579                if (use_llvm) {
 580                    try test_args.append("-fllvm");
 581                    try shell_out.print("-fllvm", .{});
 582                } else {
 583                    try test_args.append("-fno-llvm");
 584                    try shell_out.print("-fno-llvm", .{});
 585                }
 586            }
 587            if (code.link_mode) |link_mode| {
 588                switch (link_mode) {
 589                    .static => {
 590                        try test_args.append("-static");
 591                        try shell_out.print("-static ", .{});
 592                    },
 593                    .dynamic => {
 594                        try test_args.append("-dynamic");
 595                        try shell_out.print("-dynamic ", .{});
 596                    },
 597                }
 598            }
 599            for (code.additional_options) |option| {
 600                try test_args.append(option);
 601                try shell_out.print("{s} ", .{option});
 602            }
 603            const result = run(arena, &env_map, tmp_dir_path, test_args.items) catch fatal("test failed", .{});
 604            const escaped_stderr = try escapeHtml(arena, result.stderr);
 605            const escaped_stdout = try escapeHtml(arena, result.stdout);
 606            try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
 607        },
 608    }
 609
 610    if (!code.just_check_syntax) {
 611        try printShell(out, shell_buffer.written(), false);
 612    }
 613}
 614
 615fn dumpArgs(args: []const []const u8) void {
 616    for (args) |arg|
 617        std.debug.print("{s} ", .{arg})
 618    else
 619        std.debug.print("\n", .{});
 620}
 621
 622fn printSourceBlock(arena: Allocator, out: *Writer, source_bytes: []const u8, name: []const u8) !void {
 623    try out.print("<figure><figcaption class=\"{s}-cap\"><cite class=\"file\">{s}</cite></figcaption><pre>", .{
 624        "zig", name,
 625    });
 626    try tokenizeAndPrint(arena, out, source_bytes);
 627    try out.writeAll("</pre></figure>");
 628}
 629
 630fn tokenizeAndPrint(arena: Allocator, out: *Writer, raw_src: []const u8) !void {
 631    const src_non_terminated = mem.trim(u8, raw_src, " \r\n");
 632    const src = try arena.dupeZ(u8, src_non_terminated);
 633
 634    try out.writeAll("<code>");
 635    var tokenizer = std.zig.Tokenizer.init(src);
 636    var index: usize = 0;
 637    var next_tok_is_fn = false;
 638    while (true) {
 639        const prev_tok_was_fn = next_tok_is_fn;
 640        next_tok_is_fn = false;
 641
 642        const token = tokenizer.next();
 643        if (mem.indexOf(u8, src[index..token.loc.start], "//")) |comment_start_off| {
 644            // render one comment
 645            const comment_start = index + comment_start_off;
 646            const comment_end_off = mem.indexOf(u8, src[comment_start..token.loc.start], "\n");
 647            const comment_end = if (comment_end_off) |o| comment_start + o else token.loc.start;
 648
 649            try writeEscapedLines(out, src[index..comment_start]);
 650            try out.writeAll("<span class=\"tok-comment\">");
 651            try writeEscaped(out, src[comment_start..comment_end]);
 652            try out.writeAll("</span>");
 653            index = comment_end;
 654            tokenizer.index = index;
 655            continue;
 656        }
 657
 658        try writeEscapedLines(out, src[index..token.loc.start]);
 659        switch (token.tag) {
 660            .eof => break,
 661
 662            .keyword_addrspace,
 663            .keyword_align,
 664            .keyword_and,
 665            .keyword_asm,
 666            .keyword_break,
 667            .keyword_catch,
 668            .keyword_comptime,
 669            .keyword_const,
 670            .keyword_continue,
 671            .keyword_defer,
 672            .keyword_else,
 673            .keyword_enum,
 674            .keyword_errdefer,
 675            .keyword_error,
 676            .keyword_export,
 677            .keyword_extern,
 678            .keyword_for,
 679            .keyword_if,
 680            .keyword_inline,
 681            .keyword_noalias,
 682            .keyword_noinline,
 683            .keyword_nosuspend,
 684            .keyword_opaque,
 685            .keyword_or,
 686            .keyword_orelse,
 687            .keyword_packed,
 688            .keyword_anyframe,
 689            .keyword_pub,
 690            .keyword_resume,
 691            .keyword_return,
 692            .keyword_linksection,
 693            .keyword_callconv,
 694            .keyword_struct,
 695            .keyword_suspend,
 696            .keyword_switch,
 697            .keyword_test,
 698            .keyword_threadlocal,
 699            .keyword_try,
 700            .keyword_union,
 701            .keyword_unreachable,
 702            .keyword_var,
 703            .keyword_volatile,
 704            .keyword_allowzero,
 705            .keyword_while,
 706            .keyword_anytype,
 707            => {
 708                try out.writeAll("<span class=\"tok-kw\">");
 709                try writeEscaped(out, src[token.loc.start..token.loc.end]);
 710                try out.writeAll("</span>");
 711            },
 712
 713            .keyword_fn => {
 714                try out.writeAll("<span class=\"tok-kw\">");
 715                try writeEscaped(out, src[token.loc.start..token.loc.end]);
 716                try out.writeAll("</span>");
 717                next_tok_is_fn = true;
 718            },
 719
 720            .string_literal,
 721            .multiline_string_literal_line,
 722            .char_literal,
 723            => {
 724                try out.writeAll("<span class=\"tok-str\">");
 725                try writeEscaped(out, src[token.loc.start..token.loc.end]);
 726                try out.writeAll("</span>");
 727            },
 728
 729            .builtin => {
 730                try out.writeAll("<span class=\"tok-builtin\">");
 731                try writeEscaped(out, src[token.loc.start..token.loc.end]);
 732                try out.writeAll("</span>");
 733            },
 734
 735            .doc_comment,
 736            .container_doc_comment,
 737            => {
 738                try out.writeAll("<span class=\"tok-comment\">");
 739                try writeEscaped(out, src[token.loc.start..token.loc.end]);
 740                try out.writeAll("</span>");
 741            },
 742
 743            .identifier => {
 744                const tok_bytes = src[token.loc.start..token.loc.end];
 745                if (mem.eql(u8, tok_bytes, "undefined") or
 746                    mem.eql(u8, tok_bytes, "null") or
 747                    mem.eql(u8, tok_bytes, "true") or
 748                    mem.eql(u8, tok_bytes, "false"))
 749                {
 750                    try out.writeAll("<span class=\"tok-null\">");
 751                    try writeEscaped(out, tok_bytes);
 752                    try out.writeAll("</span>");
 753                } else if (prev_tok_was_fn) {
 754                    try out.writeAll("<span class=\"tok-fn\">");
 755                    try writeEscaped(out, tok_bytes);
 756                    try out.writeAll("</span>");
 757                } else {
 758                    const is_int = blk: {
 759                        if (src[token.loc.start] != 'i' and src[token.loc.start] != 'u')
 760                            break :blk false;
 761                        var i = token.loc.start + 1;
 762                        if (i == token.loc.end)
 763                            break :blk false;
 764                        while (i != token.loc.end) : (i += 1) {
 765                            if (src[i] < '0' or src[i] > '9')
 766                                break :blk false;
 767                        }
 768                        break :blk true;
 769                    };
 770                    const isType = std.zig.isPrimitive;
 771                    if (is_int or isType(tok_bytes)) {
 772                        try out.writeAll("<span class=\"tok-type\">");
 773                        try writeEscaped(out, tok_bytes);
 774                        try out.writeAll("</span>");
 775                    } else {
 776                        try writeEscaped(out, tok_bytes);
 777                    }
 778                }
 779            },
 780
 781            .number_literal => {
 782                try out.writeAll("<span class=\"tok-number\">");
 783                try writeEscaped(out, src[token.loc.start..token.loc.end]);
 784                try out.writeAll("</span>");
 785            },
 786
 787            .bang,
 788            .pipe,
 789            .pipe_pipe,
 790            .pipe_equal,
 791            .equal,
 792            .equal_equal,
 793            .equal_angle_bracket_right,
 794            .bang_equal,
 795            .l_paren,
 796            .r_paren,
 797            .semicolon,
 798            .percent,
 799            .percent_equal,
 800            .l_brace,
 801            .r_brace,
 802            .l_bracket,
 803            .r_bracket,
 804            .period,
 805            .period_asterisk,
 806            .ellipsis2,
 807            .ellipsis3,
 808            .caret,
 809            .caret_equal,
 810            .plus,
 811            .plus_plus,
 812            .plus_equal,
 813            .plus_percent,
 814            .plus_percent_equal,
 815            .plus_pipe,
 816            .plus_pipe_equal,
 817            .minus,
 818            .minus_equal,
 819            .minus_percent,
 820            .minus_percent_equal,
 821            .minus_pipe,
 822            .minus_pipe_equal,
 823            .asterisk,
 824            .asterisk_equal,
 825            .asterisk_asterisk,
 826            .asterisk_percent,
 827            .asterisk_percent_equal,
 828            .asterisk_pipe,
 829            .asterisk_pipe_equal,
 830            .arrow,
 831            .colon,
 832            .slash,
 833            .slash_equal,
 834            .comma,
 835            .ampersand,
 836            .ampersand_equal,
 837            .question_mark,
 838            .angle_bracket_left,
 839            .angle_bracket_left_equal,
 840            .angle_bracket_angle_bracket_left,
 841            .angle_bracket_angle_bracket_left_equal,
 842            .angle_bracket_angle_bracket_left_pipe,
 843            .angle_bracket_angle_bracket_left_pipe_equal,
 844            .angle_bracket_right,
 845            .angle_bracket_right_equal,
 846            .angle_bracket_angle_bracket_right,
 847            .angle_bracket_angle_bracket_right_equal,
 848            .tilde,
 849            => try writeEscaped(out, src[token.loc.start..token.loc.end]),
 850
 851            .invalid, .invalid_periodasterisks => fatal("syntax error", .{}),
 852        }
 853        index = token.loc.end;
 854    }
 855    try out.writeAll("</code>");
 856}
 857
 858fn writeEscapedLines(out: *Writer, text: []const u8) !void {
 859    return writeEscaped(out, text);
 860}
 861
 862const Code = struct {
 863    id: Id,
 864    mode: std.builtin.OptimizeMode,
 865    link_objects: []const []const u8,
 866    target_str: ?[]const u8,
 867    link_libc: bool,
 868    link_mode: ?std.builtin.LinkMode,
 869    disable_cache: bool,
 870    verbose_cimport: bool,
 871    just_check_syntax: bool,
 872    additional_options: []const []const u8,
 873    use_llvm: ?bool,
 874
 875    const Id = union(enum) {
 876        @"test",
 877        test_error: []const u8,
 878        test_safety: []const u8,
 879        exe: ExpectedOutcome,
 880        obj: ?[]const u8,
 881        lib,
 882    };
 883
 884    const ExpectedOutcome = enum {
 885        succeed,
 886        fail,
 887        build_fail,
 888    };
 889};
 890
 891fn stripManifest(source_bytes: []const u8) []const u8 {
 892    const manifest_start = mem.lastIndexOf(u8, source_bytes, "\n\n// ") orelse
 893        fatal("missing manifest comment", .{});
 894    return source_bytes[0 .. manifest_start + 1];
 895}
 896
 897fn parseManifest(arena: Allocator, source_bytes: []const u8) !Code {
 898    const manifest_start = mem.lastIndexOf(u8, source_bytes, "\n\n// ") orelse
 899        fatal("missing manifest comment", .{});
 900    var it = mem.tokenizeScalar(u8, source_bytes[manifest_start..], '\n');
 901    const first_line = skipPrefix(it.next().?);
 902
 903    var just_check_syntax = false;
 904    const id: Code.Id = if (mem.eql(u8, first_line, "syntax")) blk: {
 905        just_check_syntax = true;
 906        break :blk .{ .obj = null };
 907    } else if (mem.eql(u8, first_line, "test"))
 908        .@"test"
 909    else if (mem.eql(u8, first_line, "lib"))
 910        .lib
 911    else if (mem.eql(u8, first_line, "obj"))
 912        .{ .obj = null }
 913    else if (mem.startsWith(u8, first_line, "test_error="))
 914        .{ .test_error = first_line["test_error=".len..] }
 915    else if (mem.startsWith(u8, first_line, "test_safety="))
 916        .{ .test_safety = first_line["test_safety=".len..] }
 917    else if (mem.startsWith(u8, first_line, "exe="))
 918        .{ .exe = std.meta.stringToEnum(Code.ExpectedOutcome, first_line["exe=".len..]) orelse
 919            fatal("bad exe expected outcome in line '{s}'", .{first_line}) }
 920    else if (mem.startsWith(u8, first_line, "obj="))
 921        .{ .obj = first_line["obj=".len..] }
 922    else
 923        fatal("unrecognized manifest id: '{s}'", .{first_line});
 924
 925    var mode: std.builtin.OptimizeMode = .Debug;
 926    var link_mode: ?std.builtin.LinkMode = null;
 927    var link_objects: std.ArrayList([]const u8) = .empty;
 928    var additional_options: std.ArrayList([]const u8) = .empty;
 929    var target_str: ?[]const u8 = null;
 930    var link_libc = false;
 931    var disable_cache = false;
 932    var verbose_cimport = false;
 933    var use_llvm: ?bool = null;
 934
 935    while (it.next()) |prefixed_line| {
 936        const line = skipPrefix(prefixed_line);
 937        if (mem.startsWith(u8, line, "optimize=")) {
 938            mode = std.meta.stringToEnum(std.builtin.OptimizeMode, line["optimize=".len..]) orelse
 939                fatal("bad optimization mode line: '{s}'", .{line});
 940        } else if (mem.startsWith(u8, line, "link_mode=")) {
 941            link_mode = std.meta.stringToEnum(std.builtin.LinkMode, line["link_mode=".len..]) orelse
 942                fatal("bad link mode line: '{s}'", .{line});
 943        } else if (mem.startsWith(u8, line, "link_object=")) {
 944            try link_objects.append(arena, line["link_object=".len..]);
 945        } else if (mem.startsWith(u8, line, "additional_option=")) {
 946            try additional_options.append(arena, line["additional_option=".len..]);
 947        } else if (mem.startsWith(u8, line, "target=")) {
 948            target_str = line["target=".len..];
 949        } else if (mem.eql(u8, line, "llvm=true")) {
 950            use_llvm = true;
 951        } else if (mem.eql(u8, line, "llvm=false")) {
 952            use_llvm = false;
 953        } else if (mem.eql(u8, line, "link_libc")) {
 954            link_libc = true;
 955        } else if (mem.eql(u8, line, "disable_cache")) {
 956            disable_cache = true;
 957        } else if (mem.eql(u8, line, "verbose_cimport")) {
 958            verbose_cimport = true;
 959        } else {
 960            fatal("unrecognized manifest line: {s}", .{line});
 961        }
 962    }
 963
 964    return .{
 965        .id = id,
 966        .mode = mode,
 967        .additional_options = try additional_options.toOwnedSlice(arena),
 968        .link_objects = try link_objects.toOwnedSlice(arena),
 969        .target_str = target_str,
 970        .link_libc = link_libc,
 971        .link_mode = link_mode,
 972        .disable_cache = disable_cache,
 973        .verbose_cimport = verbose_cimport,
 974        .just_check_syntax = just_check_syntax,
 975        .use_llvm = use_llvm,
 976    };
 977}
 978
 979fn skipPrefix(line: []const u8) []const u8 {
 980    if (!mem.startsWith(u8, line, "// ")) {
 981        fatal("line does not start with '// ': '{s}", .{line});
 982    }
 983    return line[3..];
 984}
 985
 986fn escapeHtml(gpa: Allocator, input: []const u8) ![]u8 {
 987    var allocating: Writer.Allocating = .init(gpa);
 988    defer allocating.deinit();
 989    try writeEscaped(&allocating.writer, input);
 990    return allocating.toOwnedSlice();
 991}
 992
 993fn writeEscaped(w: *Writer, input: []const u8) !void {
 994    for (input) |c| try switch (c) {
 995        '&' => w.writeAll("&amp;"),
 996        '<' => w.writeAll("&lt;"),
 997        '>' => w.writeAll("&gt;"),
 998        '"' => w.writeAll("&quot;"),
 999        else => w.writeByte(c),
1000    };
1001}
1002
1003fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
1004    // The SRG sequences generates by the Zig compiler are in the format:
1005    //   ESC [ <foreground-color> ; <n> m
1006    // or
1007    //   ESC [ <n> m
1008    //
1009    // where
1010    //   foreground-color is 31 (red), 32 (green), 36 (cyan)
1011    //   n is 0 (reset), 1 (bold), 2 (dim)
1012    //
1013    //   Note that 37 (white) is currently not used by the compiler.
1014    //
1015    // See std.debug.TTY.Color.
1016    const supported_sgr_colors = [_]u8{ 31, 32, 36 };
1017    const supported_sgr_numbers = [_]u8{ 0, 1, 2 };
1018
1019    var buf = std.array_list.Managed(u8).init(allocator);
1020    defer buf.deinit();
1021
1022    var sgr_param_start_index: usize = undefined;
1023    var sgr_num: u8 = undefined;
1024    var sgr_color: u8 = undefined;
1025    var i: usize = 0;
1026    var state: enum {
1027        start,
1028        escape,
1029        lbracket,
1030        number,
1031        after_number,
1032        arg,
1033        arg_number,
1034        expect_end,
1035    } = .start;
1036    var last_new_line: usize = 0;
1037    var open_span_count: usize = 0;
1038    while (i < input.len) : (i += 1) {
1039        const c = input[i];
1040        switch (state) {
1041            .start => switch (c) {
1042                '\x1b' => state = .escape,
1043                '\n' => {
1044                    try buf.append(c);
1045                    last_new_line = buf.items.len;
1046                },
1047                else => try buf.append(c),
1048            },
1049            .escape => switch (c) {
1050                '[' => state = .lbracket,
1051                else => return error.UnsupportedEscape,
1052            },
1053            .lbracket => switch (c) {
1054                '0'...'9' => {
1055                    sgr_param_start_index = i;
1056                    state = .number;
1057                },
1058                else => return error.UnsupportedEscape,
1059            },
1060            .number => switch (c) {
1061                '0'...'9' => {},
1062                else => {
1063                    sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
1064                    sgr_color = 0;
1065                    state = .after_number;
1066                    i -= 1;
1067                },
1068            },
1069            .after_number => switch (c) {
1070                ';' => state = .arg,
1071                'D' => state = .start,
1072                'K' => {
1073                    buf.items.len = last_new_line;
1074                    state = .start;
1075                },
1076                else => {
1077                    state = .expect_end;
1078                    i -= 1;
1079                },
1080            },
1081            .arg => switch (c) {
1082                '0'...'9' => {
1083                    sgr_param_start_index = i;
1084                    state = .arg_number;
1085                },
1086                else => return error.UnsupportedEscape,
1087            },
1088            .arg_number => switch (c) {
1089                '0'...'9' => {},
1090                else => {
1091                    // Keep the sequence consistent, foreground color first.
1092                    // 32;1m is equivalent to 1;32m, but the latter will
1093                    // generate an incorrect HTML class without notice.
1094                    sgr_color = sgr_num;
1095                    if (!in(&supported_sgr_colors, sgr_color)) return error.UnsupportedForegroundColor;
1096
1097                    sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
1098                    if (!in(&supported_sgr_numbers, sgr_num)) return error.UnsupportedNumber;
1099
1100                    state = .expect_end;
1101                    i -= 1;
1102                },
1103            },
1104            .expect_end => switch (c) {
1105                'm' => {
1106                    state = .start;
1107                    while (open_span_count != 0) : (open_span_count -= 1) {
1108                        try buf.appendSlice("</span>");
1109                    }
1110                    if (sgr_num == 0) {
1111                        if (sgr_color != 0) return error.UnsupportedColor;
1112                        continue;
1113                    }
1114                    if (sgr_color != 0) {
1115                        try buf.print("<span class=\"sgr-{d}_{d}m\">", .{ sgr_color, sgr_num });
1116                    } else {
1117                        try buf.print("<span class=\"sgr-{d}m\">", .{sgr_num});
1118                    }
1119                    open_span_count += 1;
1120                },
1121                else => return error.UnsupportedEscape,
1122            },
1123        }
1124    }
1125    return try buf.toOwnedSlice();
1126}
1127
1128// Returns true if number is in slice.
1129fn in(slice: []const u8, number: u8) bool {
1130    return mem.indexOfScalar(u8, slice, number) != null;
1131}
1132
1133fn run(
1134    allocator: Allocator,
1135    env_map: *process.EnvMap,
1136    cwd: []const u8,
1137    args: []const []const u8,
1138) !process.Child.RunResult {
1139    const result = try process.Child.run(.{
1140        .allocator = allocator,
1141        .argv = args,
1142        .env_map = env_map,
1143        .cwd = cwd,
1144        .max_output_bytes = max_doc_file_size,
1145    });
1146    switch (result.term) {
1147        .Exited => |exit_code| {
1148            if (exit_code != 0) {
1149                std.debug.print("{s}\nThe following command exited with code {}:\n", .{ result.stderr, exit_code });
1150                dumpArgs(args);
1151                return error.ChildExitError;
1152            }
1153        },
1154        else => {
1155            std.debug.print("{s}\nThe following command crashed:\n", .{result.stderr});
1156            dumpArgs(args);
1157            return error.ChildCrashed;
1158        },
1159    }
1160    return result;
1161}
1162
1163fn printShell(out: *Writer, shell_content: []const u8, escape: bool) !void {
1164    const trimmed_shell_content = mem.trim(u8, shell_content, " \r\n");
1165    try out.writeAll("<figure><figcaption class=\"shell-cap\">Shell</figcaption><pre><samp>");
1166    var cmd_cont: bool = false;
1167    var iter = std.mem.splitScalar(u8, trimmed_shell_content, '\n');
1168    while (iter.next()) |orig_line| {
1169        const line = mem.trimEnd(u8, orig_line, " \r");
1170        if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] != '\\') {
1171            try out.writeAll("$ <kbd>");
1172            const s = std.mem.trimStart(u8, line[1..], " ");
1173            if (escape) {
1174                try writeEscaped(out, s);
1175            } else {
1176                try out.writeAll(s);
1177            }
1178            try out.writeAll("</kbd>" ++ "\n");
1179        } else if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] == '\\') {
1180            try out.writeAll("$ <kbd>");
1181            const s = std.mem.trimStart(u8, line[1..], " ");
1182            if (escape) {
1183                try writeEscaped(out, s);
1184            } else {
1185                try out.writeAll(s);
1186            }
1187            try out.writeAll("\n");
1188            cmd_cont = true;
1189        } else if (line.len > 0 and line[line.len - 1] != '\\' and cmd_cont) {
1190            if (escape) {
1191                try writeEscaped(out, line);
1192            } else {
1193                try out.writeAll(line);
1194            }
1195            try out.writeAll("</kbd>" ++ "\n");
1196            cmd_cont = false;
1197        } else {
1198            if (escape) {
1199                try writeEscaped(out, line);
1200            } else {
1201                try out.writeAll(line);
1202            }
1203            try out.writeAll("\n");
1204        }
1205    }
1206
1207    try out.writeAll("</samp></pre></figure>");
1208}
1209
1210test "term supported colors" {
1211    const test_allocator = testing.allocator;
1212
1213    {
1214        const input = "A\x1b[31;1mred\x1b[0mB";
1215        const expect = "A<span class=\"sgr-31_1m\">red</span>B";
1216
1217        const result = try termColor(test_allocator, input);
1218        defer test_allocator.free(result);
1219        try testing.expectEqualSlices(u8, expect, result);
1220    }
1221
1222    {
1223        const input = "A\x1b[32;1mgreen\x1b[0mB";
1224        const expect = "A<span class=\"sgr-32_1m\">green</span>B";
1225
1226        const result = try termColor(test_allocator, input);
1227        defer test_allocator.free(result);
1228        try testing.expectEqualSlices(u8, expect, result);
1229    }
1230
1231    {
1232        const input = "A\x1b[36;1mcyan\x1b[0mB";
1233        const expect = "A<span class=\"sgr-36_1m\">cyan</span>B";
1234
1235        const result = try termColor(test_allocator, input);
1236        defer test_allocator.free(result);
1237        try testing.expectEqualSlices(u8, expect, result);
1238    }
1239
1240    {
1241        const input = "A\x1b[1mbold\x1b[0mB";
1242        const expect = "A<span class=\"sgr-1m\">bold</span>B";
1243
1244        const result = try termColor(test_allocator, input);
1245        defer test_allocator.free(result);
1246        try testing.expectEqualSlices(u8, expect, result);
1247    }
1248
1249    {
1250        const input = "A\x1b[2mdim\x1b[0mB";
1251        const expect = "A<span class=\"sgr-2m\">dim</span>B";
1252
1253        const result = try termColor(test_allocator, input);
1254        defer test_allocator.free(result);
1255        try testing.expectEqualSlices(u8, expect, result);
1256    }
1257}
1258
1259test "term output from zig" {
1260    // Use data generated by https://github.com/perillo/zig-tty-test-data,
1261    // with zig version 0.11.0-dev.1898+36d47dd19.
1262    const test_allocator = testing.allocator;
1263
1264    {
1265        // 1.1-with-build-progress.out
1266        const input = "Semantic Analysis [1324] \x1b[25D\x1b[0KLLVM Emit Object... \x1b[20D\x1b[0KLLVM Emit Object... \x1b[20D\x1b[0KLLD Link... \x1b[12D\x1b[0K";
1267        const expect = "";
1268
1269        const result = try termColor(test_allocator, input);
1270        defer test_allocator.free(result);
1271        try testing.expectEqualSlices(u8, expect, result);
1272    }
1273
1274    {
1275        // 2.1-with-reference-traces.out
1276        const input = "\x1b[1msrc/2.1-with-reference-traces.zig:3:7: \x1b[31;1merror: \x1b[0m\x1b[1mcannot assign to constant\n\x1b[0m    x += 1;\n    \x1b[32;1m~~^~~~\n\x1b[0m\x1b[0m\x1b[2mreferenced by:\n    main: src/2.1-with-reference-traces.zig:7:5\n    callMain: /usr/local/lib/zig/lib/std/start.zig:607:17\n    remaining reference traces hidden; use '-freference-trace' to see all reference traces\n\n\x1b[0m";
1277        const expect =
1278            \\<span class="sgr-1m">src/2.1-with-reference-traces.zig:3:7: </span><span class="sgr-31_1m">error: </span><span class="sgr-1m">cannot assign to constant
1279            \\</span>    x += 1;
1280            \\    <span class="sgr-32_1m">~~^~~~
1281            \\</span><span class="sgr-2m">referenced by:
1282            \\    main: src/2.1-with-reference-traces.zig:7:5
1283            \\    callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
1284            \\    remaining reference traces hidden; use '-freference-trace' to see all reference traces
1285            \\
1286            \\</span>
1287        ;
1288
1289        const result = try termColor(test_allocator, input);
1290        defer test_allocator.free(result);
1291        try testing.expectEqualSlices(u8, expect, result);
1292    }
1293
1294    {
1295        // 2.2-without-reference-traces.out
1296        const input = "\x1b[1m/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:128:29: \x1b[31;1merror: \x1b[0m\x1b[1minvalid type given to fixedBufferStream\n\x1b[0m                    else => @compileError(\"invalid type given to fixedBufferStream\"),\n                            \x1b[32;1m^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\x1b[0m\x1b[1m/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:116:66: \x1b[36;1mnote: \x1b[0m\x1b[1mcalled from here\n\x1b[0mpub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {\n;                                                            \x1b[32;1m~~~~~^~~~~~~~~~~~~~~~~\n\x1b[0m";
1297        const expect =
1298            \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:128:29: </span><span class="sgr-31_1m">error: </span><span class="sgr-1m">invalid type given to fixedBufferStream
1299            \\</span>                    else => @compileError("invalid type given to fixedBufferStream"),
1300            \\                            <span class="sgr-32_1m">^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1301            \\</span><span class="sgr-1m">/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:116:66: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">called from here
1302            \\</span>pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {
1303            \\;                                                            <span class="sgr-32_1m">~~~~~^~~~~~~~~~~~~~~~~
1304            \\</span>
1305        ;
1306
1307        const result = try termColor(test_allocator, input);
1308        defer test_allocator.free(result);
1309        try testing.expectEqualSlices(u8, expect, result);
1310    }
1311
1312    {
1313        // 2.3-with-notes.out
1314        const input = "\x1b[1msrc/2.3-with-notes.zig:6:9: \x1b[31;1merror: \x1b[0m\x1b[1mexpected type '*2.3-with-notes.Derp', found '*2.3-with-notes.Wat'\n\x1b[0m    bar(w);\n        \x1b[32;1m^\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:6:9: \x1b[36;1mnote: \x1b[0m\x1b[1mpointer type child '2.3-with-notes.Wat' cannot cast into pointer type child '2.3-with-notes.Derp'\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:2:13: \x1b[36;1mnote: \x1b[0m\x1b[1mopaque declared here\n\x1b[0mconst Wat = opaque {};\n            \x1b[32;1m^~~~~~~~~\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:1:14: \x1b[36;1mnote: \x1b[0m\x1b[1mopaque declared here\n\x1b[0mconst Derp = opaque {};\n             \x1b[32;1m^~~~~~~~~\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:4:18: \x1b[36;1mnote: \x1b[0m\x1b[1mparameter type declared here\n\x1b[0mextern fn bar(d: *Derp) void;\n                 \x1b[32;1m^~~~~\n\x1b[0m\x1b[0m\x1b[2mreferenced by:\n    main: src/2.3-with-notes.zig:10:5\n    callMain: /usr/local/lib/zig/lib/std/start.zig:607:17\n    remaining reference traces hidden; use '-freference-trace' to see all reference traces\n\n\x1b[0m";
1315        const expect =
1316            \\<span class="sgr-1m">src/2.3-with-notes.zig:6:9: </span><span class="sgr-31_1m">error: </span><span class="sgr-1m">expected type '*2.3-with-notes.Derp', found '*2.3-with-notes.Wat'
1317            \\</span>    bar(w);
1318            \\        <span class="sgr-32_1m">^
1319            \\</span><span class="sgr-1m">src/2.3-with-notes.zig:6:9: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">pointer type child '2.3-with-notes.Wat' cannot cast into pointer type child '2.3-with-notes.Derp'
1320            \\</span><span class="sgr-1m">src/2.3-with-notes.zig:2:13: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">opaque declared here
1321            \\</span>const Wat = opaque {};
1322            \\            <span class="sgr-32_1m">^~~~~~~~~
1323            \\</span><span class="sgr-1m">src/2.3-with-notes.zig:1:14: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">opaque declared here
1324            \\</span>const Derp = opaque {};
1325            \\             <span class="sgr-32_1m">^~~~~~~~~
1326            \\</span><span class="sgr-1m">src/2.3-with-notes.zig:4:18: </span><span class="sgr-36_1m">note: </span><span class="sgr-1m">parameter type declared here
1327            \\</span>extern fn bar(d: *Derp) void;
1328            \\                 <span class="sgr-32_1m">^~~~~
1329            \\</span><span class="sgr-2m">referenced by:
1330            \\    main: src/2.3-with-notes.zig:10:5
1331            \\    callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
1332            \\    remaining reference traces hidden; use '-freference-trace' to see all reference traces
1333            \\
1334            \\</span>
1335        ;
1336
1337        const result = try termColor(test_allocator, input);
1338        defer test_allocator.free(result);
1339        try testing.expectEqualSlices(u8, expect, result);
1340    }
1341
1342    {
1343        // 3.1-with-error-return-traces.out
1344
1345        const input = "error: Error\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:5:5\x1b[0m: \x1b[2m0x20b008 in callee (3.1-with-error-return-traces)\x1b[0m\n    return error.Error;\n    \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:9:5\x1b[0m: \x1b[2m0x20b113 in caller (3.1-with-error-return-traces)\x1b[0m\n    try callee();\n    \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:13:5\x1b[0m: \x1b[2m0x20b153 in main (3.1-with-error-return-traces)\x1b[0m\n    try caller();\n    \x1b[32;1m^\x1b[0m\n";
1346        const expect =
1347            \\error: Error
1348            \\<span class="sgr-1m">/home/zig/src/3.1-with-error-return-traces.zig:5:5</span>: <span class="sgr-2m">0x20b008 in callee (3.1-with-error-return-traces)</span>
1349            \\    return error.Error;
1350            \\    <span class="sgr-32_1m">^</span>
1351            \\<span class="sgr-1m">/home/zig/src/3.1-with-error-return-traces.zig:9:5</span>: <span class="sgr-2m">0x20b113 in caller (3.1-with-error-return-traces)</span>
1352            \\    try callee();
1353            \\    <span class="sgr-32_1m">^</span>
1354            \\<span class="sgr-1m">/home/zig/src/3.1-with-error-return-traces.zig:13:5</span>: <span class="sgr-2m">0x20b153 in main (3.1-with-error-return-traces)</span>
1355            \\    try caller();
1356            \\    <span class="sgr-32_1m">^</span>
1357            \\
1358        ;
1359
1360        const result = try termColor(test_allocator, input);
1361        defer test_allocator.free(result);
1362        try testing.expectEqualSlices(u8, expect, result);
1363    }
1364
1365    {
1366        // 3.2-with-stack-trace.out
1367        const input = "\x1b[1m/usr/local/lib/zig/lib/std/debug.zig:561:19\x1b[0m: \x1b[2m0x22a107 in writeCurrentStackTrace__anon_5898 (3.2-with-stack-trace)\x1b[0m\n    while (it.next()) |return_address| {\n                  \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/debug.zig:157:80\x1b[0m: \x1b[2m0x20bb23 in dumpCurrentStackTrace (3.2-with-stack-trace)\x1b[0m\n        writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {\n                                                                               \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.2-with-stack-trace.zig:5:36\x1b[0m: \x1b[2m0x20d3b2 in foo (3.2-with-stack-trace)\x1b[0m\n    std.debug.dumpCurrentStackTrace(null);\n                                   \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.2-with-stack-trace.zig:9:8\x1b[0m: \x1b[2m0x20b458 in main (3.2-with-stack-trace)\x1b[0m\n    foo();\n       \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/start.zig:607:22\x1b[0m: \x1b[2m0x20a965 in posixCallMainAndExit (3.2-with-stack-trace)\x1b[0m\n            root.main();\n                     \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/start.zig:376:5\x1b[0m: \x1b[2m0x20a411 in _start (3.2-with-stack-trace)\x1b[0m\n    @call(.never_inline, posixCallMainAndExit, .{});\n    \x1b[32;1m^\x1b[0m\n";
1368        const expect =
1369            \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/debug.zig:561:19</span>: <span class="sgr-2m">0x22a107 in writeCurrentStackTrace__anon_5898 (3.2-with-stack-trace)</span>
1370            \\    while (it.next()) |return_address| {
1371            \\                  <span class="sgr-32_1m">^</span>
1372            \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/debug.zig:157:80</span>: <span class="sgr-2m">0x20bb23 in dumpCurrentStackTrace (3.2-with-stack-trace)</span>
1373            \\        writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {
1374            \\                                                                               <span class="sgr-32_1m">^</span>
1375            \\<span class="sgr-1m">/home/zig/src/3.2-with-stack-trace.zig:5:36</span>: <span class="sgr-2m">0x20d3b2 in foo (3.2-with-stack-trace)</span>
1376            \\    std.debug.dumpCurrentStackTrace(null);
1377            \\                                   <span class="sgr-32_1m">^</span>
1378            \\<span class="sgr-1m">/home/zig/src/3.2-with-stack-trace.zig:9:8</span>: <span class="sgr-2m">0x20b458 in main (3.2-with-stack-trace)</span>
1379            \\    foo();
1380            \\       <span class="sgr-32_1m">^</span>
1381            \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/start.zig:607:22</span>: <span class="sgr-2m">0x20a965 in posixCallMainAndExit (3.2-with-stack-trace)</span>
1382            \\            root.main();
1383            \\                     <span class="sgr-32_1m">^</span>
1384            \\<span class="sgr-1m">/usr/local/lib/zig/lib/std/start.zig:376:5</span>: <span class="sgr-2m">0x20a411 in _start (3.2-with-stack-trace)</span>
1385            \\    @call(.never_inline, posixCallMainAndExit, .{});
1386            \\    <span class="sgr-32_1m">^</span>
1387            \\
1388        ;
1389
1390        const result = try termColor(test_allocator, input);
1391        defer test_allocator.free(result);
1392        try testing.expectEqualSlices(u8, expect, result);
1393    }
1394}
1395
1396test "printShell" {
1397    const test_allocator = std.testing.allocator;
1398
1399    {
1400        const shell_out =
1401            \\$ zig build test.zig
1402        ;
1403        const expected =
1404            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1405            \\</samp></pre></figure>
1406        ;
1407
1408        var buffer: Writer.Allocating = .init(test_allocator);
1409        defer buffer.deinit();
1410
1411        try printShell(&buffer.writer, shell_out, false);
1412        try testing.expectEqualSlices(u8, expected, buffer.written());
1413    }
1414    {
1415        const shell_out =
1416            \\$ zig build test.zig
1417            \\build output
1418        ;
1419        const expected =
1420            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1421            \\build output
1422            \\</samp></pre></figure>
1423        ;
1424
1425        var buffer: Writer.Allocating = .init(test_allocator);
1426        defer buffer.deinit();
1427
1428        try printShell(&buffer.writer, shell_out, false);
1429        try testing.expectEqualSlices(u8, expected, buffer.written());
1430    }
1431    {
1432        const shell_out = "$ zig build test.zig\r\nbuild output\r\n";
1433        const expected =
1434            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1435            \\build output
1436            \\</samp></pre></figure>
1437        ;
1438
1439        var buffer: Writer.Allocating = .init(test_allocator);
1440        defer buffer.deinit();
1441
1442        try printShell(&buffer.writer, shell_out, false);
1443        try testing.expectEqualSlices(u8, expected, buffer.written());
1444    }
1445    {
1446        const shell_out =
1447            \\$ zig build test.zig
1448            \\build output
1449            \\$ ./test
1450        ;
1451        const expected =
1452            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1453            \\build output
1454            \\$ <kbd>./test</kbd>
1455            \\</samp></pre></figure>
1456        ;
1457
1458        var buffer: Writer.Allocating = .init(test_allocator);
1459        defer buffer.deinit();
1460
1461        try printShell(&buffer.writer, shell_out, false);
1462        try testing.expectEqualSlices(u8, expected, buffer.written());
1463    }
1464    {
1465        const shell_out =
1466            \\$ zig build test.zig
1467            \\
1468            \\$ ./test
1469            \\output
1470        ;
1471        const expected =
1472            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1473            \\
1474            \\$ <kbd>./test</kbd>
1475            \\output
1476            \\</samp></pre></figure>
1477        ;
1478
1479        var buffer: Writer.Allocating = .init(test_allocator);
1480        defer buffer.deinit();
1481
1482        try printShell(&buffer.writer, shell_out, false);
1483        try testing.expectEqualSlices(u8, expected, buffer.written());
1484    }
1485    {
1486        const shell_out =
1487            \\$ zig build test.zig
1488            \\$ ./test
1489            \\output
1490        ;
1491        const expected =
1492            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1493            \\$ <kbd>./test</kbd>
1494            \\output
1495            \\</samp></pre></figure>
1496        ;
1497
1498        var buffer: Writer.Allocating = .init(test_allocator);
1499        defer buffer.deinit();
1500
1501        try printShell(&buffer.writer, shell_out, false);
1502        try testing.expectEqualSlices(u8, expected, buffer.written());
1503    }
1504    {
1505        const shell_out =
1506            \\$ zig build test.zig \
1507            \\ --build-option
1508            \\build output
1509            \\$ ./test
1510            \\output
1511        ;
1512        const expected =
1513            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
1514            \\ --build-option</kbd>
1515            \\build output
1516            \\$ <kbd>./test</kbd>
1517            \\output
1518            \\</samp></pre></figure>
1519        ;
1520
1521        var buffer: Writer.Allocating = .init(test_allocator);
1522        defer buffer.deinit();
1523
1524        try printShell(&buffer.writer, shell_out, false);
1525        try testing.expectEqualSlices(u8, expected, buffer.written());
1526    }
1527    {
1528        // intentional space after "--build-option1 \"
1529        const shell_out =
1530            \\$ zig build test.zig \
1531            \\ --build-option1 \ 
1532            \\ --build-option2
1533            \\$ ./test
1534        ;
1535        const expected =
1536            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
1537            \\ --build-option1 \
1538            \\ --build-option2</kbd>
1539            \\$ <kbd>./test</kbd>
1540            \\</samp></pre></figure>
1541        ;
1542
1543        var buffer: Writer.Allocating = .init(test_allocator);
1544        defer buffer.deinit();
1545
1546        try printShell(&buffer.writer, shell_out, false);
1547        try testing.expectEqualSlices(u8, expected, buffer.written());
1548    }
1549    {
1550        const shell_out =
1551            \\$ zig build test.zig \
1552            \\$ ./test
1553        ;
1554        const expected =
1555            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
1556            \\$ ./test</kbd>
1557            \\</samp></pre></figure>
1558        ;
1559
1560        var buffer: Writer.Allocating = .init(test_allocator);
1561        defer buffer.deinit();
1562
1563        try printShell(&buffer.writer, shell_out, false);
1564        try testing.expectEqualSlices(u8, expected, buffer.written());
1565    }
1566    {
1567        const shell_out =
1568            \\$ zig build test.zig
1569            \\$ ./test
1570            \\$1
1571        ;
1572        const expected =
1573            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
1574            \\$ <kbd>./test</kbd>
1575            \\$1
1576            \\</samp></pre></figure>
1577        ;
1578
1579        var buffer: Writer.Allocating = .init(test_allocator);
1580        defer buffer.deinit();
1581
1582        try printShell(&buffer.writer, shell_out, false);
1583        try testing.expectEqualSlices(u8, expected, buffer.written());
1584    }
1585    {
1586        const shell_out =
1587            \\$zig build test.zig
1588        ;
1589        const expected =
1590            \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$zig build test.zig
1591            \\</samp></pre></figure>
1592        ;
1593
1594        var buffer: Writer.Allocating = .init(test_allocator);
1595        defer buffer.deinit();
1596
1597        try printShell(&buffer.writer, shell_out, false);
1598        try testing.expectEqualSlices(u8, expected, buffer.written());
1599    }
1600}