Commit 7ca052a6fb

Manlio Perillo <manlio.perillo@gmail.com>
2023-02-25 11:14:43
docgen: improve the termColor function
Make the termColor function more robust, ensuring that the generated HTML classes are more consistent and that they are supported in the CSS. Add documentation about the ANSI codes generated by Zig, and remove the previous comment with the supported colors. Improve test coverage for the termColor function, and move the tests at the end of the file before the printShell tests. Rename the "term color" test to "term supported colors" and add an additional "term output from zig" test, using test data generated from the Zig compiler. Update the langref.html.in CSS to use the new names and remove incorrect or obsolete colors like .t0_1, .t37 and .t37_1. Fix support for the 1m color. Change font-weight to normal for the kbd element, to avoid both the command (like `zig build-exe`) and the first line of the error message having a bold font. Rename the "shell parsed" test to "printShell".
1 parent 1dd2604
Changed files (2)
doc/docgen.zig
@@ -793,28 +793,37 @@ fn writeEscaped(out: anytype, input: []const u8) !void {
     }
 }
 
-//#define VT_RED "\x1b[31;1m"
-//#define VT_GREEN "\x1b[32;1m"
-//#define VT_CYAN "\x1b[36;1m"
-//#define VT_WHITE "\x1b[37;1m"
-//#define VT_BOLD "\x1b[0;1m"
-//#define VT_RESET "\x1b[0m"
-
-test "term color" {
-    const input_bytes = "A\x1b[32;1mgreen\x1b[0mB";
-    const result = try termColor(std.testing.allocator, input_bytes);
-    defer std.testing.allocator.free(result);
-    try testing.expectEqualSlices(u8, "A<span class=\"t32_1\">green</span>B", result);
+// Returns true if number is in slice.
+fn in(slice: []const u8, number: u8) bool {
+    for (slice) |n| {
+        if (number == n) return true;
+    }
+    return false;
 }
 
 fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
+    // The SRG sequences generates by the Zig compiler are in the format:
+    //   ESC [ <foreground-color> ; <n> m
+    // or
+    //   ESC [ <n> m
+    //
+    // where
+    //   foreground-color is 31 (red), 32 (green), 36 (cyan)
+    //   n is 0 (reset), 1 (bold), 2 (dim)
+    //
+    //   Note that 37 (white) is currently not used by the compiler.
+    //
+    // See std.debug.TTY.Color.
+    const supported_sgr_colors = [_]u8{ 31, 32, 36 };
+    const supported_sgr_numbers = [_]u8{ 0, 1, 2 };
+
     var buf = std.ArrayList(u8).init(allocator);
     defer buf.deinit();
 
     var out = buf.writer();
-    var number_start_index: usize = undefined;
-    var first_number: usize = undefined;
-    var second_number: usize = undefined;
+    var sgr_param_start_index: usize = undefined;
+    var sgr_num: u8 = undefined;
+    var sgr_color: u8 = undefined;
     var i: usize = 0;
     var state: enum {
         start,
@@ -845,7 +854,7 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
             },
             .lbracket => switch (c) {
                 '0'...'9' => {
-                    number_start_index = i;
+                    sgr_param_start_index = i;
                     state = .number;
                 },
                 else => return error.UnsupportedEscape,
@@ -853,13 +862,12 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
             .number => switch (c) {
                 '0'...'9' => {},
                 else => {
-                    first_number = std.fmt.parseInt(usize, input[number_start_index..i], 10) catch unreachable;
-                    second_number = 0;
+                    sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
+                    sgr_color = 0;
                     state = .after_number;
                     i -= 1;
                 },
             },
-
             .after_number => switch (c) {
                 ';' => state = .arg,
                 'D' => state = .start,
@@ -874,7 +882,7 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
             },
             .arg => switch (c) {
                 '0'...'9' => {
-                    number_start_index = i;
+                    sgr_param_start_index = i;
                     state = .arg_number;
                 },
                 else => return error.UnsupportedEscape,
@@ -882,7 +890,15 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
             .arg_number => switch (c) {
                 '0'...'9' => {},
                 else => {
-                    second_number = std.fmt.parseInt(usize, input[number_start_index..i], 10) catch unreachable;
+                    // Keep the sequence consistent, foreground color first.
+                    // 32;1m is equivalent to 1;32m, but the latter will
+                    // generate an incorrect HTML class without notice.
+                    sgr_color = sgr_num;
+                    if (!in(&supported_sgr_colors, sgr_color)) return error.UnsupportedForegroundColor;
+
+                    sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
+                    if (!in(&supported_sgr_numbers, sgr_num)) return error.UnsupportedNumber;
+
                     state = .expect_end;
                     i -= 1;
                 },
@@ -893,10 +909,16 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
                     while (open_span_count != 0) : (open_span_count -= 1) {
                         try out.writeAll("</span>");
                     }
-                    if (first_number != 0 or second_number != 0) {
-                        try out.print("<span class=\"t{d}_{d}\">", .{ first_number, second_number });
-                        open_span_count += 1;
+                    if (sgr_num == 0) {
+                        if (sgr_color != 0) return error.UnsupportedColor;
+                        continue;
+                    }
+                    if (sgr_color != 0) {
+                        try out.print("<span class=\"sgr-{d}_{d}m\">", .{ sgr_color, sgr_num });
+                    } else {
+                        try out.print("<span class=\"sgr-{d}m\">", .{sgr_num});
                     }
+                    open_span_count += 1;
                 },
                 else => return error.UnsupportedEscape,
             },
@@ -1874,7 +1896,193 @@ fn dumpArgs(args: []const []const u8) void {
         print("\n", .{});
 }
 
-test "shell parsed" {
+test "term supported colors" {
+    const test_allocator = testing.allocator;
+
+    {
+        const input = "A\x1b[31;1mred\x1b[0mB";
+        const expect = "A<span class=\"sgr-31_1m\">red</span>B";
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        const input = "A\x1b[32;1mgreen\x1b[0mB";
+        const expect = "A<span class=\"sgr-32_1m\">green</span>B";
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        const input = "A\x1b[36;1mcyan\x1b[0mB";
+        const expect = "A<span class=\"sgr-36_1m\">cyan</span>B";
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        const input = "A\x1b[1mbold\x1b[0mB";
+        const expect = "A<span class=\"sgr-1m\">bold</span>B";
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        const input = "A\x1b[2mdim\x1b[0mB";
+        const expect = "A<span class=\"sgr-2m\">dim</span>B";
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+}
+
+test "term output from zig" {
+    // Use data generated by https://github.com/perillo/zig-tty-test-data,
+    // with zig version 0.11.0-dev.1898+36d47dd19.
+    const test_allocator = testing.allocator;
+
+    {
+        // 1.1-with-build-progress.out
+        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";
+        const expect = "";
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        // 2.1-with-reference-traces.out
+        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";
+        const expect =
+            \\<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
+            \\</span>    x += 1;
+            \\    <span class="sgr-32_1m">~~^~~~
+            \\</span><span class="sgr-2m">referenced by:
+            \\    main: src/2.1-with-reference-traces.zig:7:5
+            \\    callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
+            \\    remaining reference traces hidden; use '-freference-trace' to see all reference traces
+            \\
+            \\</span>
+        ;
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        // 2.2-without-reference-traces.out
+        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";
+        const expect =
+            \\<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
+            \\</span>                    else => @compileError("invalid type given to fixedBufferStream"),
+            \\                            <span class="sgr-32_1m">^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+            \\</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
+            \\</span>pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {
+            \\;                                                            <span class="sgr-32_1m">~~~~~^~~~~~~~~~~~~~~~~
+            \\</span>
+        ;
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        // 2.3-with-notes.out
+        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";
+        const expect =
+            \\<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'
+            \\</span>    bar(w);
+            \\        <span class="sgr-32_1m">^
+            \\</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'
+            \\</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
+            \\</span>const Wat = opaque {};
+            \\            <span class="sgr-32_1m">^~~~~~~~~
+            \\</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
+            \\</span>const Derp = opaque {};
+            \\             <span class="sgr-32_1m">^~~~~~~~~
+            \\</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
+            \\</span>extern fn bar(d: *Derp) void;
+            \\                 <span class="sgr-32_1m">^~~~~
+            \\</span><span class="sgr-2m">referenced by:
+            \\    main: src/2.3-with-notes.zig:10:5
+            \\    callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
+            \\    remaining reference traces hidden; use '-freference-trace' to see all reference traces
+            \\
+            \\</span>
+        ;
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        // 3.1-with-error-return-traces.out
+
+        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";
+        const expect =
+            \\error: Error
+            \\<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>
+            \\    return error.Error;
+            \\    <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\    try callee();
+            \\    <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\    try caller();
+            \\    <span class="sgr-32_1m">^</span>
+            \\
+        ;
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+
+    {
+        // 3.2-with-stack-trace.out
+        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";
+        const expect =
+            \\<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>
+            \\    while (it.next()) |return_address| {
+            \\                  <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\        writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {
+            \\                                                                               <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\    std.debug.dumpCurrentStackTrace(null);
+            \\                                   <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\    foo();
+            \\       <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\            root.main();
+            \\                     <span class="sgr-32_1m">^</span>
+            \\<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>
+            \\    @call(.never_inline, posixCallMainAndExit, .{});
+            \\    <span class="sgr-32_1m">^</span>
+            \\
+        ;
+
+        const result = try termColor(test_allocator, input);
+        defer test_allocator.free(result);
+        try testing.expectEqualSlices(u8, expect, result);
+    }
+}
+
+test "printShell" {
     const test_allocator = std.testing.allocator;
 
     {
doc/langref.html.in
@@ -71,19 +71,19 @@
           text-align: left;
           font-weight: normal;
       }
-      .t0_1, .t37, .t37_1 {
+      .sgr-1m {
         font-weight: bold;
       }
-      .t2_0 {
+      .sgr-2m {
         color: #575757;
       }
-      .t31_1 {
+      .sgr-31_1m {
         color: #b40000;
       }
-      .t32_1 {
+      .sgr-32_1m {
         color: green;
       }
-      .t36_1 {
+      .sgr-36_1m {
         color: #005C7A;
       }
       .file {
@@ -114,7 +114,7 @@
         line-height: normal;
       }
       kbd {
-        font-weight: bold;
+        font-weight: normal;
       }
       .table-wrapper {
         width: 100%;
@@ -232,16 +232,16 @@
         table, th, td {
             border-color: grey;
         }
-        .t2_0 {
+        .sgr-2m {
             color: grey;
         }
-        .t31_1 {
+        .sgr-31_1m {
             color: red;
         }
-        .t32_1 {
+        .sgr-32_1m {
             color: #00B800;
         }
-        .t36_1 {
+        .sgr-36_1m {
             color: #0086b3;
         }
         code {