authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-07-30 18:44:31-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-07-30 18:44:31-07:00
logacbb6418c3899eb79aea98e7f5d3173298716377
treeeaf6206e2038cbf7af17098162458b78cac91058
parent25a9487caa05c16f9f87cb6931340e1a1051a7f3

move docgen.zig to tools/


3 files changed, 2279 insertions(+), 2279 deletions(-)

build.zig+1-1
......@@ -36,7 +36,7 @@ pub fn build(b: *std.Build) !void {
3636
3737 const docgen_exe = b.addExecutable(.{
3838 .name = "docgen",
39 .root_source_file = .{ .path = "doc/docgen.zig" },
39 .root_source_file = .{ .path = "tools/docgen.zig" },
4040 .target = .{},
4141 .optimize = .Debug,
4242 });
doc/docgen.zig deleted-2278
......@@ -1,2278 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const io = std.io;
4const fs = std.fs;
5const process = std.process;
6const ChildProcess = std.ChildProcess;
7const Progress = std.Progress;
8const print = std.debug.print;
9const mem = std.mem;
10const testing = std.testing;
11const Allocator = std.mem.Allocator;
12
13const max_doc_file_size = 10 * 1024 * 1024;
14
15const exe_ext = @as(std.zig.CrossTarget, .{}).exeFileExt();
16const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch);
17const tmp_dir_name = "docgen_tmp";
18const test_out_path = tmp_dir_name ++ fs.path.sep_str ++ "test" ++ exe_ext;
19
20const usage =
21 \\Usage: docgen [--zig] [--skip-code-tests] input output"
22 \\
23 \\ Generates an HTML document from a docgen template.
24 \\
25 \\Options:
26 \\ -h, --help Print this help and exit
27 \\ --skip-code-tests Skip the doctests
28 \\
29;
30
31fn fatal(comptime format: []const u8, args: anytype) noreturn {
32 const stderr = io.getStdErr().writer();
33
34 stderr.print("error: " ++ format ++ "\n", args) catch {};
35 process.exit(1);
36}
37
38pub fn main() !void {
39 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
40 defer arena.deinit();
41
42 const allocator = arena.allocator();
43
44 var args_it = try process.argsWithAllocator(allocator);
45 if (!args_it.skip()) @panic("expected self arg");
46
47 var zig_exe: []const u8 = "zig";
48 var opt_zig_lib_dir: ?[]const u8 = null;
49 var do_code_tests = true;
50 var files = [_][]const u8{ "", "" };
51
52 var i: usize = 0;
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 const stdout = io.getStdOut().writer();
57 try stdout.writeAll(usage);
58 process.exit(0);
59 } else if (mem.eql(u8, arg, "--zig")) {
60 if (args_it.next()) |param| {
61 zig_exe = param;
62 } else {
63 fatal("expected parameter after --zig", .{});
64 }
65 } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
66 if (args_it.next()) |param| {
67 // Convert relative to absolute because this will be passed
68 // to a child process with a different cwd.
69 opt_zig_lib_dir = try fs.realpathAlloc(allocator, param);
70 } else {
71 fatal("expected parameter after --zig-lib-dir", .{});
72 }
73 } else if (mem.eql(u8, arg, "--skip-code-tests")) {
74 do_code_tests = false;
75 } else {
76 fatal("unrecognized option: '{s}'", .{arg});
77 }
78 } else {
79 if (i > 1) {
80 fatal("too many arguments", .{});
81 }
82 files[i] = arg;
83 i += 1;
84 }
85 }
86 if (i < 2) {
87 fatal("not enough arguments", .{});
88 }
89
90 var in_file = try fs.cwd().openFile(files[0], .{ .mode = .read_only });
91 defer in_file.close();
92
93 var out_file = try fs.cwd().createFile(files[1], .{});
94 defer out_file.close();
95
96 const input_file_bytes = try in_file.reader().readAllAlloc(allocator, max_doc_file_size);
97
98 var buffered_writer = io.bufferedWriter(out_file.writer());
99
100 var tokenizer = Tokenizer.init(files[0], input_file_bytes);
101 var toc = try genToc(allocator, &tokenizer);
102
103 try fs.cwd().makePath(tmp_dir_name);
104 defer fs.cwd().deleteTree(tmp_dir_name) catch {};
105
106 try genHtml(allocator, &tokenizer, &toc, buffered_writer.writer(), zig_exe, opt_zig_lib_dir, do_code_tests);
107 try buffered_writer.flush();
108}
109
110const Token = struct {
111 id: Id,
112 start: usize,
113 end: usize,
114
115 const Id = enum {
116 invalid,
117 content,
118 bracket_open,
119 tag_content,
120 separator,
121 bracket_close,
122 eof,
123 };
124};
125
126const Tokenizer = struct {
127 buffer: []const u8,
128 index: usize,
129 state: State,
130 source_file_name: []const u8,
131 code_node_count: usize,
132
133 const State = enum {
134 start,
135 l_bracket,
136 hash,
137 tag_name,
138 eof,
139 };
140
141 fn init(source_file_name: []const u8, buffer: []const u8) Tokenizer {
142 return Tokenizer{
143 .buffer = buffer,
144 .index = 0,
145 .state = .start,
146 .source_file_name = source_file_name,
147 .code_node_count = 0,
148 };
149 }
150
151 fn next(self: *Tokenizer) Token {
152 var result = Token{
153 .id = .eof,
154 .start = self.index,
155 .end = undefined,
156 };
157 while (self.index < self.buffer.len) : (self.index += 1) {
158 const c = self.buffer[self.index];
159 switch (self.state) {
160 .start => switch (c) {
161 '{' => {
162 self.state = .l_bracket;
163 },
164 else => {
165 result.id = .content;
166 },
167 },
168 .l_bracket => switch (c) {
169 '#' => {
170 if (result.id != .eof) {
171 self.index -= 1;
172 self.state = .start;
173 break;
174 } else {
175 result.id = .bracket_open;
176 self.index += 1;
177 self.state = .tag_name;
178 break;
179 }
180 },
181 else => {
182 result.id = .content;
183 self.state = .start;
184 },
185 },
186 .tag_name => switch (c) {
187 '|' => {
188 if (result.id != .eof) {
189 break;
190 } else {
191 result.id = .separator;
192 self.index += 1;
193 break;
194 }
195 },
196 '#' => {
197 self.state = .hash;
198 },
199 else => {
200 result.id = .tag_content;
201 },
202 },
203 .hash => switch (c) {
204 '}' => {
205 if (result.id != .eof) {
206 self.index -= 1;
207 self.state = .tag_name;
208 break;
209 } else {
210 result.id = .bracket_close;
211 self.index += 1;
212 self.state = .start;
213 break;
214 }
215 },
216 else => {
217 result.id = .tag_content;
218 self.state = .tag_name;
219 },
220 },
221 .eof => unreachable,
222 }
223 } else {
224 switch (self.state) {
225 .start, .l_bracket, .eof => {},
226 else => {
227 result.id = .invalid;
228 },
229 }
230 self.state = .eof;
231 }
232 result.end = self.index;
233 return result;
234 }
235
236 const Location = struct {
237 line: usize,
238 column: usize,
239 line_start: usize,
240 line_end: usize,
241 };
242
243 fn getTokenLocation(self: *Tokenizer, token: Token) Location {
244 var loc = Location{
245 .line = 0,
246 .column = 0,
247 .line_start = 0,
248 .line_end = 0,
249 };
250 for (self.buffer, 0..) |c, i| {
251 if (i == token.start) {
252 loc.line_end = i;
253 while (loc.line_end < self.buffer.len and self.buffer[loc.line_end] != '\n') : (loc.line_end += 1) {}
254 return loc;
255 }
256 if (c == '\n') {
257 loc.line += 1;
258 loc.column = 0;
259 loc.line_start = i + 1;
260 } else {
261 loc.column += 1;
262 }
263 }
264 return loc;
265 }
266};
267
268fn parseError(tokenizer: *Tokenizer, token: Token, comptime fmt: []const u8, args: anytype) anyerror {
269 const loc = tokenizer.getTokenLocation(token);
270 const args_prefix = .{ tokenizer.source_file_name, loc.line + 1, loc.column + 1 };
271 print("{s}:{d}:{d}: error: " ++ fmt ++ "\n", args_prefix ++ args);
272 if (loc.line_start <= loc.line_end) {
273 print("{s}\n", .{tokenizer.buffer[loc.line_start..loc.line_end]});
274 {
275 var i: usize = 0;
276 while (i < loc.column) : (i += 1) {
277 print(" ", .{});
278 }
279 }
280 {
281 const caret_count = @min(token.end, loc.line_end) - token.start;
282 var i: usize = 0;
283 while (i < caret_count) : (i += 1) {
284 print("~", .{});
285 }
286 }
287 print("\n", .{});
288 }
289 return error.ParseError;
290}
291
292fn assertToken(tokenizer: *Tokenizer, token: Token, id: Token.Id) !void {
293 if (token.id != id) {
294 return parseError(tokenizer, token, "expected {s}, found {s}", .{ @tagName(id), @tagName(token.id) });
295 }
296}
297
298fn eatToken(tokenizer: *Tokenizer, id: Token.Id) !Token {
299 const token = tokenizer.next();
300 try assertToken(tokenizer, token, id);
301 return token;
302}
303
304const HeaderOpen = struct {
305 name: []const u8,
306 url: []const u8,
307 n: usize,
308};
309
310const SeeAlsoItem = struct {
311 name: []const u8,
312 token: Token,
313};
314
315const ExpectedOutcome = enum {
316 succeed,
317 fail,
318 build_fail,
319};
320
321const Code = struct {
322 id: Id,
323 name: []const u8,
324 source_token: Token,
325 just_check_syntax: bool,
326 mode: std.builtin.Mode,
327 link_objects: []const []const u8,
328 target_str: ?[]const u8,
329 link_libc: bool,
330 link_mode: ?std.builtin.LinkMode,
331 disable_cache: bool,
332 verbose_cimport: bool,
333 additional_options: []const []const u8,
334
335 const Id = union(enum) {
336 @"test",
337 test_error: []const u8,
338 test_safety: []const u8,
339 exe: ExpectedOutcome,
340 obj: ?[]const u8,
341 lib,
342 };
343};
344
345const Link = struct {
346 url: []const u8,
347 name: []const u8,
348 token: Token,
349};
350
351const SyntaxBlock = struct {
352 source_type: SourceType,
353 name: []const u8,
354 source_token: Token,
355
356 const SourceType = enum {
357 zig,
358 c,
359 peg,
360 javascript,
361 };
362};
363
364const Node = union(enum) {
365 Content: []const u8,
366 Nav,
367 Builtin: Token,
368 HeaderOpen: HeaderOpen,
369 SeeAlso: []const SeeAlsoItem,
370 Code: Code,
371 Link: Link,
372 InlineSyntax: Token,
373 Shell: Token,
374 SyntaxBlock: SyntaxBlock,
375};
376
377const Toc = struct {
378 nodes: []Node,
379 toc: []u8,
380 urls: std.StringHashMap(Token),
381};
382
383const Action = enum {
384 open,
385 close,
386};
387
388fn genToc(allocator: Allocator, tokenizer: *Tokenizer) !Toc {
389 var urls = std.StringHashMap(Token).init(allocator);
390 errdefer urls.deinit();
391
392 var header_stack_size: usize = 0;
393 var last_action: Action = .open;
394 var last_columns: ?u8 = null;
395
396 var toc_buf = std.ArrayList(u8).init(allocator);
397 defer toc_buf.deinit();
398
399 var toc = toc_buf.writer();
400
401 var nodes = std.ArrayList(Node).init(allocator);
402 defer nodes.deinit();
403
404 try toc.writeByte('\n');
405
406 while (true) {
407 const token = tokenizer.next();
408 switch (token.id) {
409 .eof => {
410 if (header_stack_size != 0) {
411 return parseError(tokenizer, token, "unbalanced headers", .{});
412 }
413 try toc.writeAll(" </ul>\n");
414 break;
415 },
416 .content => {
417 try nodes.append(Node{ .Content = tokenizer.buffer[token.start..token.end] });
418 },
419 .bracket_open => {
420 const tag_token = try eatToken(tokenizer, .tag_content);
421 const tag_name = tokenizer.buffer[tag_token.start..tag_token.end];
422
423 if (mem.eql(u8, tag_name, "nav")) {
424 _ = try eatToken(tokenizer, .bracket_close);
425
426 try nodes.append(Node.Nav);
427 } else if (mem.eql(u8, tag_name, "builtin")) {
428 _ = try eatToken(tokenizer, .bracket_close);
429 try nodes.append(Node{ .Builtin = tag_token });
430 } else if (mem.eql(u8, tag_name, "header_open")) {
431 _ = try eatToken(tokenizer, .separator);
432 const content_token = try eatToken(tokenizer, .tag_content);
433 const content = tokenizer.buffer[content_token.start..content_token.end];
434 var columns: ?u8 = null;
435 while (true) {
436 const bracket_tok = tokenizer.next();
437 switch (bracket_tok.id) {
438 .bracket_close => break,
439 .separator => continue,
440 .tag_content => {
441 const param = tokenizer.buffer[bracket_tok.start..bracket_tok.end];
442 if (mem.eql(u8, param, "2col")) {
443 columns = 2;
444 } else {
445 return parseError(
446 tokenizer,
447 bracket_tok,
448 "unrecognized header_open param: {s}",
449 .{param},
450 );
451 }
452 },
453 else => return parseError(tokenizer, bracket_tok, "invalid header_open token", .{}),
454 }
455 }
456
457 header_stack_size += 1;
458
459 const urlized = try urlize(allocator, content);
460 try nodes.append(Node{
461 .HeaderOpen = HeaderOpen{
462 .name = content,
463 .url = urlized,
464 .n = header_stack_size + 1, // highest-level section headers start at h2
465 },
466 });
467 if (try urls.fetchPut(urlized, tag_token)) |kv| {
468 parseError(tokenizer, tag_token, "duplicate header url: #{s}", .{urlized}) catch {};
469 parseError(tokenizer, kv.value, "other tag here", .{}) catch {};
470 return error.ParseError;
471 }
472 if (last_action == .open) {
473 try toc.writeByte('\n');
474 try toc.writeByteNTimes(' ', header_stack_size * 4);
475 if (last_columns) |n| {
476 try toc.print("<ul style=\"columns: {}\">\n", .{n});
477 } else {
478 try toc.writeAll("<ul>\n");
479 }
480 } else {
481 last_action = .open;
482 }
483 last_columns = columns;
484 try toc.writeByteNTimes(' ', 4 + header_stack_size * 4);
485 try toc.print("<li><a id=\"toc-{s}\" href=\"#{s}\">{s}</a>", .{ urlized, urlized, content });
486 } else if (mem.eql(u8, tag_name, "header_close")) {
487 if (header_stack_size == 0) {
488 return parseError(tokenizer, tag_token, "unbalanced close header", .{});
489 }
490 header_stack_size -= 1;
491 _ = try eatToken(tokenizer, .bracket_close);
492
493 if (last_action == .close) {
494 try toc.writeByteNTimes(' ', 8 + header_stack_size * 4);
495 try toc.writeAll("</ul></li>\n");
496 } else {
497 try toc.writeAll("</li>\n");
498 last_action = .close;
499 }
500 } else if (mem.eql(u8, tag_name, "see_also")) {
501 var list = std.ArrayList(SeeAlsoItem).init(allocator);
502 errdefer list.deinit();
503
504 while (true) {
505 const see_also_tok = tokenizer.next();
506 switch (see_also_tok.id) {
507 .tag_content => {
508 const content = tokenizer.buffer[see_also_tok.start..see_also_tok.end];
509 try list.append(SeeAlsoItem{
510 .name = content,
511 .token = see_also_tok,
512 });
513 },
514 .separator => {},
515 .bracket_close => {
516 try nodes.append(Node{ .SeeAlso = try list.toOwnedSlice() });
517 break;
518 },
519 else => return parseError(tokenizer, see_also_tok, "invalid see_also token", .{}),
520 }
521 }
522 } else if (mem.eql(u8, tag_name, "link")) {
523 _ = try eatToken(tokenizer, .separator);
524 const name_tok = try eatToken(tokenizer, .tag_content);
525 const name = tokenizer.buffer[name_tok.start..name_tok.end];
526
527 const url_name = blk: {
528 const tok = tokenizer.next();
529 switch (tok.id) {
530 .bracket_close => break :blk name,
531 .separator => {
532 const explicit_text = try eatToken(tokenizer, .tag_content);
533 _ = try eatToken(tokenizer, .bracket_close);
534 break :blk tokenizer.buffer[explicit_text.start..explicit_text.end];
535 },
536 else => return parseError(tokenizer, tok, "invalid link token", .{}),
537 }
538 };
539
540 try nodes.append(Node{
541 .Link = Link{
542 .url = try urlize(allocator, url_name),
543 .name = name,
544 .token = name_tok,
545 },
546 });
547 } else if (mem.eql(u8, tag_name, "code_begin")) {
548 _ = try eatToken(tokenizer, .separator);
549 const code_kind_tok = try eatToken(tokenizer, .tag_content);
550 _ = try eatToken(tokenizer, .separator);
551 const name_tok = try eatToken(tokenizer, .tag_content);
552 const name = tokenizer.buffer[name_tok.start..name_tok.end];
553 var error_str: []const u8 = "";
554 const maybe_sep = tokenizer.next();
555 switch (maybe_sep.id) {
556 .separator => {
557 const error_tok = try eatToken(tokenizer, .tag_content);
558 error_str = tokenizer.buffer[error_tok.start..error_tok.end];
559 _ = try eatToken(tokenizer, .bracket_close);
560 },
561 .bracket_close => {},
562 else => return parseError(tokenizer, token, "invalid token", .{}),
563 }
564 const code_kind_str = tokenizer.buffer[code_kind_tok.start..code_kind_tok.end];
565 var code_kind_id: Code.Id = undefined;
566 var just_check_syntax = false;
567 if (mem.eql(u8, code_kind_str, "exe")) {
568 code_kind_id = Code.Id{ .exe = .succeed };
569 } else if (mem.eql(u8, code_kind_str, "exe_err")) {
570 code_kind_id = Code.Id{ .exe = .fail };
571 } else if (mem.eql(u8, code_kind_str, "exe_build_err")) {
572 code_kind_id = Code.Id{ .exe = .build_fail };
573 } else if (mem.eql(u8, code_kind_str, "test")) {
574 code_kind_id = .@"test";
575 } else if (mem.eql(u8, code_kind_str, "test_err")) {
576 code_kind_id = Code.Id{ .test_error = error_str };
577 } else if (mem.eql(u8, code_kind_str, "test_safety")) {
578 code_kind_id = Code.Id{ .test_safety = error_str };
579 } else if (mem.eql(u8, code_kind_str, "obj")) {
580 code_kind_id = Code.Id{ .obj = null };
581 } else if (mem.eql(u8, code_kind_str, "obj_err")) {
582 code_kind_id = Code.Id{ .obj = error_str };
583 } else if (mem.eql(u8, code_kind_str, "lib")) {
584 code_kind_id = Code.Id.lib;
585 } else if (mem.eql(u8, code_kind_str, "syntax")) {
586 code_kind_id = Code.Id{ .obj = null };
587 just_check_syntax = true;
588 } else {
589 return parseError(tokenizer, code_kind_tok, "unrecognized code kind: {s}", .{code_kind_str});
590 }
591
592 var mode: std.builtin.Mode = .Debug;
593 var link_objects = std.ArrayList([]const u8).init(allocator);
594 defer link_objects.deinit();
595 var target_str: ?[]const u8 = null;
596 var link_libc = false;
597 var link_mode: ?std.builtin.LinkMode = null;
598 var disable_cache = false;
599 var verbose_cimport = false;
600 var additional_options = std.ArrayList([]const u8).init(allocator);
601 defer additional_options.deinit();
602
603 const source_token = while (true) {
604 const content_tok = try eatToken(tokenizer, .content);
605 _ = try eatToken(tokenizer, .bracket_open);
606 const end_code_tag = try eatToken(tokenizer, .tag_content);
607 const end_tag_name = tokenizer.buffer[end_code_tag.start..end_code_tag.end];
608 if (mem.eql(u8, end_tag_name, "code_release_fast")) {
609 mode = .ReleaseFast;
610 } else if (mem.eql(u8, end_tag_name, "code_release_safe")) {
611 mode = .ReleaseSafe;
612 } else if (mem.eql(u8, end_tag_name, "code_disable_cache")) {
613 disable_cache = true;
614 } else if (mem.eql(u8, end_tag_name, "code_verbose_cimport")) {
615 verbose_cimport = true;
616 } else if (mem.eql(u8, end_tag_name, "code_link_object")) {
617 _ = try eatToken(tokenizer, .separator);
618 const obj_tok = try eatToken(tokenizer, .tag_content);
619 try link_objects.append(tokenizer.buffer[obj_tok.start..obj_tok.end]);
620 } else if (mem.eql(u8, end_tag_name, "target_windows")) {
621 target_str = "x86_64-windows";
622 } else if (mem.eql(u8, end_tag_name, "target_linux_x86_64")) {
623 target_str = "x86_64-linux";
624 } else if (mem.eql(u8, end_tag_name, "target_linux_riscv64")) {
625 target_str = "riscv64-linux";
626 } else if (mem.eql(u8, end_tag_name, "target_wasm")) {
627 target_str = "wasm32-freestanding";
628 } else if (mem.eql(u8, end_tag_name, "target_wasi")) {
629 target_str = "wasm32-wasi";
630 } else if (mem.eql(u8, end_tag_name, "link_libc")) {
631 link_libc = true;
632 } else if (mem.eql(u8, end_tag_name, "link_mode_dynamic")) {
633 link_mode = .Dynamic;
634 } else if (mem.eql(u8, end_tag_name, "additonal_option")) {
635 _ = try eatToken(tokenizer, .separator);
636 const option = try eatToken(tokenizer, .tag_content);
637 try additional_options.append(tokenizer.buffer[option.start..option.end]);
638 } else if (mem.eql(u8, end_tag_name, "code_end")) {
639 _ = try eatToken(tokenizer, .bracket_close);
640 break content_tok;
641 } else {
642 return parseError(
643 tokenizer,
644 end_code_tag,
645 "invalid token inside code_begin: {s}",
646 .{end_tag_name},
647 );
648 }
649 _ = try eatToken(tokenizer, .bracket_close);
650 } else unreachable; // TODO issue #707
651 try nodes.append(Node{
652 .Code = Code{
653 .id = code_kind_id,
654 .name = name,
655 .source_token = source_token,
656 .just_check_syntax = just_check_syntax,
657 .mode = mode,
658 .link_objects = try link_objects.toOwnedSlice(),
659 .target_str = target_str,
660 .link_libc = link_libc,
661 .link_mode = link_mode,
662 .disable_cache = disable_cache,
663 .verbose_cimport = verbose_cimport,
664 .additional_options = try additional_options.toOwnedSlice(),
665 },
666 });
667 tokenizer.code_node_count += 1;
668 } else if (mem.eql(u8, tag_name, "syntax")) {
669 _ = try eatToken(tokenizer, .bracket_close);
670 const content_tok = try eatToken(tokenizer, .content);
671 _ = try eatToken(tokenizer, .bracket_open);
672 const end_syntax_tag = try eatToken(tokenizer, .tag_content);
673 const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end];
674 if (!mem.eql(u8, end_tag_name, "endsyntax")) {
675 return parseError(
676 tokenizer,
677 end_syntax_tag,
678 "invalid token inside syntax: {s}",
679 .{end_tag_name},
680 );
681 }
682 _ = try eatToken(tokenizer, .bracket_close);
683 try nodes.append(Node{ .InlineSyntax = content_tok });
684 } else if (mem.eql(u8, tag_name, "shell_samp")) {
685 _ = try eatToken(tokenizer, .bracket_close);
686 const content_tok = try eatToken(tokenizer, .content);
687 _ = try eatToken(tokenizer, .bracket_open);
688 const end_syntax_tag = try eatToken(tokenizer, .tag_content);
689 const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end];
690 if (!mem.eql(u8, end_tag_name, "end_shell_samp")) {
691 return parseError(
692 tokenizer,
693 end_syntax_tag,
694 "invalid token inside syntax: {s}",
695 .{end_tag_name},
696 );
697 }
698 _ = try eatToken(tokenizer, .bracket_close);
699 try nodes.append(Node{ .Shell = content_tok });
700 } else if (mem.eql(u8, tag_name, "syntax_block")) {
701 _ = try eatToken(tokenizer, .separator);
702 const source_type_tok = try eatToken(tokenizer, .tag_content);
703 var name: []const u8 = "sample_code";
704 const maybe_sep = tokenizer.next();
705 switch (maybe_sep.id) {
706 .separator => {
707 const name_tok = try eatToken(tokenizer, .tag_content);
708 name = tokenizer.buffer[name_tok.start..name_tok.end];
709 _ = try eatToken(tokenizer, .bracket_close);
710 },
711 .bracket_close => {},
712 else => return parseError(tokenizer, token, "invalid token", .{}),
713 }
714 const source_type_str = tokenizer.buffer[source_type_tok.start..source_type_tok.end];
715 var source_type: SyntaxBlock.SourceType = undefined;
716 if (mem.eql(u8, source_type_str, "zig")) {
717 source_type = SyntaxBlock.SourceType.zig;
718 } else if (mem.eql(u8, source_type_str, "c")) {
719 source_type = SyntaxBlock.SourceType.c;
720 } else if (mem.eql(u8, source_type_str, "peg")) {
721 source_type = SyntaxBlock.SourceType.peg;
722 } else if (mem.eql(u8, source_type_str, "javascript")) {
723 source_type = SyntaxBlock.SourceType.javascript;
724 } else {
725 return parseError(tokenizer, source_type_tok, "unrecognized code kind: {s}", .{source_type_str});
726 }
727 const source_token = while (true) {
728 const content_tok = try eatToken(tokenizer, .content);
729 _ = try eatToken(tokenizer, .bracket_open);
730 const end_code_tag = try eatToken(tokenizer, .tag_content);
731 const end_tag_name = tokenizer.buffer[end_code_tag.start..end_code_tag.end];
732 if (mem.eql(u8, end_tag_name, "end_syntax_block")) {
733 _ = try eatToken(tokenizer, .bracket_close);
734 break content_tok;
735 } else {
736 return parseError(
737 tokenizer,
738 end_code_tag,
739 "invalid token inside code_begin: {s}",
740 .{end_tag_name},
741 );
742 }
743 _ = try eatToken(tokenizer, .bracket_close);
744 };
745 try nodes.append(Node{ .SyntaxBlock = SyntaxBlock{ .source_type = source_type, .name = name, .source_token = source_token } });
746 } else {
747 return parseError(tokenizer, tag_token, "unrecognized tag name: {s}", .{tag_name});
748 }
749 },
750 else => return parseError(tokenizer, token, "invalid token", .{}),
751 }
752 }
753
754 return Toc{
755 .nodes = try nodes.toOwnedSlice(),
756 .toc = try toc_buf.toOwnedSlice(),
757 .urls = urls,
758 };
759}
760
761fn urlize(allocator: Allocator, input: []const u8) ![]u8 {
762 var buf = std.ArrayList(u8).init(allocator);
763 defer buf.deinit();
764
765 const out = buf.writer();
766 for (input) |c| {
767 switch (c) {
768 'a'...'z', 'A'...'Z', '_', '-', '0'...'9' => {
769 try out.writeByte(c);
770 },
771 ' ' => {
772 try out.writeByte('-');
773 },
774 else => {},
775 }
776 }
777 return try buf.toOwnedSlice();
778}
779
780fn escapeHtml(allocator: Allocator, input: []const u8) ![]u8 {
781 var buf = std.ArrayList(u8).init(allocator);
782 defer buf.deinit();
783
784 const out = buf.writer();
785 try writeEscaped(out, input);
786 return try buf.toOwnedSlice();
787}
788
789fn writeEscaped(out: anytype, input: []const u8) !void {
790 for (input) |c| {
791 try switch (c) {
792 '&' => out.writeAll("&amp;"),
793 '<' => out.writeAll("&lt;"),
794 '>' => out.writeAll("&gt;"),
795 '"' => out.writeAll("&quot;"),
796 else => out.writeByte(c),
797 };
798 }
799}
800
801// Returns true if number is in slice.
802fn in(slice: []const u8, number: u8) bool {
803 for (slice) |n| {
804 if (number == n) return true;
805 }
806 return false;
807}
808
809fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
810 // The SRG sequences generates by the Zig compiler are in the format:
811 // ESC [ <foreground-color> ; <n> m
812 // or
813 // ESC [ <n> m
814 //
815 // where
816 // foreground-color is 31 (red), 32 (green), 36 (cyan)
817 // n is 0 (reset), 1 (bold), 2 (dim)
818 //
819 // Note that 37 (white) is currently not used by the compiler.
820 //
821 // See std.debug.TTY.Color.
822 const supported_sgr_colors = [_]u8{ 31, 32, 36 };
823 const supported_sgr_numbers = [_]u8{ 0, 1, 2 };
824
825 var buf = std.ArrayList(u8).init(allocator);
826 defer buf.deinit();
827
828 var out = buf.writer();
829 var sgr_param_start_index: usize = undefined;
830 var sgr_num: u8 = undefined;
831 var sgr_color: u8 = undefined;
832 var i: usize = 0;
833 var state: enum {
834 start,
835 escape,
836 lbracket,
837 number,
838 after_number,
839 arg,
840 arg_number,
841 expect_end,
842 } = .start;
843 var last_new_line: usize = 0;
844 var open_span_count: usize = 0;
845 while (i < input.len) : (i += 1) {
846 const c = input[i];
847 switch (state) {
848 .start => switch (c) {
849 '\x1b' => state = .escape,
850 '\n' => {
851 try out.writeByte(c);
852 last_new_line = buf.items.len;
853 },
854 else => try out.writeByte(c),
855 },
856 .escape => switch (c) {
857 '[' => state = .lbracket,
858 else => return error.UnsupportedEscape,
859 },
860 .lbracket => switch (c) {
861 '0'...'9' => {
862 sgr_param_start_index = i;
863 state = .number;
864 },
865 else => return error.UnsupportedEscape,
866 },
867 .number => switch (c) {
868 '0'...'9' => {},
869 else => {
870 sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
871 sgr_color = 0;
872 state = .after_number;
873 i -= 1;
874 },
875 },
876 .after_number => switch (c) {
877 ';' => state = .arg,
878 'D' => state = .start,
879 'K' => {
880 buf.items.len = last_new_line;
881 state = .start;
882 },
883 else => {
884 state = .expect_end;
885 i -= 1;
886 },
887 },
888 .arg => switch (c) {
889 '0'...'9' => {
890 sgr_param_start_index = i;
891 state = .arg_number;
892 },
893 else => return error.UnsupportedEscape,
894 },
895 .arg_number => switch (c) {
896 '0'...'9' => {},
897 else => {
898 // Keep the sequence consistent, foreground color first.
899 // 32;1m is equivalent to 1;32m, but the latter will
900 // generate an incorrect HTML class without notice.
901 sgr_color = sgr_num;
902 if (!in(&supported_sgr_colors, sgr_color)) return error.UnsupportedForegroundColor;
903
904 sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
905 if (!in(&supported_sgr_numbers, sgr_num)) return error.UnsupportedNumber;
906
907 state = .expect_end;
908 i -= 1;
909 },
910 },
911 .expect_end => switch (c) {
912 'm' => {
913 state = .start;
914 while (open_span_count != 0) : (open_span_count -= 1) {
915 try out.writeAll("</span>");
916 }
917 if (sgr_num == 0) {
918 if (sgr_color != 0) return error.UnsupportedColor;
919 continue;
920 }
921 if (sgr_color != 0) {
922 try out.print("<span class=\"sgr-{d}_{d}m\">", .{ sgr_color, sgr_num });
923 } else {
924 try out.print("<span class=\"sgr-{d}m\">", .{sgr_num});
925 }
926 open_span_count += 1;
927 },
928 else => return error.UnsupportedEscape,
929 },
930 }
931 }
932 return try buf.toOwnedSlice();
933}
934
935const builtin_types = [_][]const u8{
936 "f16", "f32", "f64", "f80", "f128",
937 "c_longdouble", "c_short", "c_ushort", "c_int", "c_uint",
938 "c_long", "c_ulong", "c_longlong", "c_ulonglong", "c_char",
939 "anyopaque", "void", "bool", "isize", "usize",
940 "noreturn", "type", "anyerror", "comptime_int", "comptime_float",
941};
942
943fn isType(name: []const u8) bool {
944 for (builtin_types) |t| {
945 if (mem.eql(u8, t, name))
946 return true;
947 }
948 return false;
949}
950
951const start_line = "<span class=\"line\">";
952const end_line = "</span>";
953
954fn writeEscapedLines(out: anytype, text: []const u8) !void {
955 for (text) |char| {
956 if (char == '\n') {
957 try out.writeAll(end_line);
958 try out.writeAll("\n");
959 try out.writeAll(start_line);
960 } else {
961 try writeEscaped(out, &[_]u8{char});
962 }
963 }
964}
965
966fn tokenizeAndPrintRaw(
967 allocator: Allocator,
968 docgen_tokenizer: *Tokenizer,
969 out: anytype,
970 source_token: Token,
971 raw_src: []const u8,
972) !void {
973 const src_non_terminated = mem.trim(u8, raw_src, " \n");
974 const src = try allocator.dupeZ(u8, src_non_terminated);
975
976 try out.writeAll("<code>" ++ start_line);
977 var tokenizer = std.zig.Tokenizer.init(src);
978 var index: usize = 0;
979 var next_tok_is_fn = false;
980 while (true) {
981 const prev_tok_was_fn = next_tok_is_fn;
982 next_tok_is_fn = false;
983
984 const token = tokenizer.next();
985 if (mem.indexOf(u8, src[index..token.loc.start], "//")) |comment_start_off| {
986 // render one comment
987 const comment_start = index + comment_start_off;
988 const comment_end_off = mem.indexOf(u8, src[comment_start..token.loc.start], "\n");
989 const comment_end = if (comment_end_off) |o| comment_start + o else token.loc.start;
990
991 try writeEscapedLines(out, src[index..comment_start]);
992 try out.writeAll("<span class=\"tok-comment\">");
993 try writeEscaped(out, src[comment_start..comment_end]);
994 try out.writeAll("</span>");
995 index = comment_end;
996 tokenizer.index = index;
997 continue;
998 }
999
1000 try writeEscapedLines(out, src[index..token.loc.start]);
1001 switch (token.tag) {
1002 .eof => break,
1003
1004 .keyword_addrspace,
1005 .keyword_align,
1006 .keyword_and,
1007 .keyword_asm,
1008 .keyword_async,
1009 .keyword_await,
1010 .keyword_break,
1011 .keyword_catch,
1012 .keyword_comptime,
1013 .keyword_const,
1014 .keyword_continue,
1015 .keyword_defer,
1016 .keyword_else,
1017 .keyword_enum,
1018 .keyword_errdefer,
1019 .keyword_error,
1020 .keyword_export,
1021 .keyword_extern,
1022 .keyword_for,
1023 .keyword_if,
1024 .keyword_inline,
1025 .keyword_noalias,
1026 .keyword_noinline,
1027 .keyword_nosuspend,
1028 .keyword_opaque,
1029 .keyword_or,
1030 .keyword_orelse,
1031 .keyword_packed,
1032 .keyword_anyframe,
1033 .keyword_pub,
1034 .keyword_resume,
1035 .keyword_return,
1036 .keyword_linksection,
1037 .keyword_callconv,
1038 .keyword_struct,
1039 .keyword_suspend,
1040 .keyword_switch,
1041 .keyword_test,
1042 .keyword_threadlocal,
1043 .keyword_try,
1044 .keyword_union,
1045 .keyword_unreachable,
1046 .keyword_usingnamespace,
1047 .keyword_var,
1048 .keyword_volatile,
1049 .keyword_allowzero,
1050 .keyword_while,
1051 .keyword_anytype,
1052 => {
1053 try out.writeAll("<span class=\"tok-kw\">");
1054 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1055 try out.writeAll("</span>");
1056 },
1057
1058 .keyword_fn => {
1059 try out.writeAll("<span class=\"tok-kw\">");
1060 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1061 try out.writeAll("</span>");
1062 next_tok_is_fn = true;
1063 },
1064
1065 .string_literal,
1066 .char_literal,
1067 => {
1068 try out.writeAll("<span class=\"tok-str\">");
1069 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1070 try out.writeAll("</span>");
1071 },
1072
1073 .multiline_string_literal_line => {
1074 if (src[token.loc.end - 1] == '\n') {
1075 try out.writeAll("<span class=\"tok-str\">");
1076 try writeEscaped(out, src[token.loc.start .. token.loc.end - 1]);
1077 try out.writeAll("</span>" ++ end_line ++ "\n" ++ start_line);
1078 } else {
1079 try out.writeAll("<span class=\"tok-str\">");
1080 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1081 try out.writeAll("</span>");
1082 }
1083 },
1084
1085 .builtin => {
1086 try out.writeAll("<span class=\"tok-builtin\">");
1087 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1088 try out.writeAll("</span>");
1089 },
1090
1091 .doc_comment,
1092 .container_doc_comment,
1093 => {
1094 try out.writeAll("<span class=\"tok-comment\">");
1095 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1096 try out.writeAll("</span>");
1097 },
1098
1099 .identifier => {
1100 const tok_bytes = src[token.loc.start..token.loc.end];
1101 if (mem.eql(u8, tok_bytes, "undefined") or
1102 mem.eql(u8, tok_bytes, "null") or
1103 mem.eql(u8, tok_bytes, "true") or
1104 mem.eql(u8, tok_bytes, "false"))
1105 {
1106 try out.writeAll("<span class=\"tok-null\">");
1107 try writeEscaped(out, tok_bytes);
1108 try out.writeAll("</span>");
1109 } else if (prev_tok_was_fn) {
1110 try out.writeAll("<span class=\"tok-fn\">");
1111 try writeEscaped(out, tok_bytes);
1112 try out.writeAll("</span>");
1113 } else {
1114 const is_int = blk: {
1115 if (src[token.loc.start] != 'i' and src[token.loc.start] != 'u')
1116 break :blk false;
1117 var i = token.loc.start + 1;
1118 if (i == token.loc.end)
1119 break :blk false;
1120 while (i != token.loc.end) : (i += 1) {
1121 if (src[i] < '0' or src[i] > '9')
1122 break :blk false;
1123 }
1124 break :blk true;
1125 };
1126 if (is_int or isType(tok_bytes)) {
1127 try out.writeAll("<span class=\"tok-type\">");
1128 try writeEscaped(out, tok_bytes);
1129 try out.writeAll("</span>");
1130 } else {
1131 try writeEscaped(out, tok_bytes);
1132 }
1133 }
1134 },
1135
1136 .number_literal => {
1137 try out.writeAll("<span class=\"tok-number\">");
1138 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1139 try out.writeAll("</span>");
1140 },
1141
1142 .bang,
1143 .pipe,
1144 .pipe_pipe,
1145 .pipe_equal,
1146 .equal,
1147 .equal_equal,
1148 .equal_angle_bracket_right,
1149 .bang_equal,
1150 .l_paren,
1151 .r_paren,
1152 .semicolon,
1153 .percent,
1154 .percent_equal,
1155 .l_brace,
1156 .r_brace,
1157 .l_bracket,
1158 .r_bracket,
1159 .period,
1160 .period_asterisk,
1161 .ellipsis2,
1162 .ellipsis3,
1163 .caret,
1164 .caret_equal,
1165 .plus,
1166 .plus_plus,
1167 .plus_equal,
1168 .plus_percent,
1169 .plus_percent_equal,
1170 .plus_pipe,
1171 .plus_pipe_equal,
1172 .minus,
1173 .minus_equal,
1174 .minus_percent,
1175 .minus_percent_equal,
1176 .minus_pipe,
1177 .minus_pipe_equal,
1178 .asterisk,
1179 .asterisk_equal,
1180 .asterisk_asterisk,
1181 .asterisk_percent,
1182 .asterisk_percent_equal,
1183 .asterisk_pipe,
1184 .asterisk_pipe_equal,
1185 .arrow,
1186 .colon,
1187 .slash,
1188 .slash_equal,
1189 .comma,
1190 .ampersand,
1191 .ampersand_equal,
1192 .question_mark,
1193 .angle_bracket_left,
1194 .angle_bracket_left_equal,
1195 .angle_bracket_angle_bracket_left,
1196 .angle_bracket_angle_bracket_left_equal,
1197 .angle_bracket_angle_bracket_left_pipe,
1198 .angle_bracket_angle_bracket_left_pipe_equal,
1199 .angle_bracket_right,
1200 .angle_bracket_right_equal,
1201 .angle_bracket_angle_bracket_right,
1202 .angle_bracket_angle_bracket_right_equal,
1203 .tilde,
1204 => try writeEscaped(out, src[token.loc.start..token.loc.end]),
1205
1206 .invalid, .invalid_periodasterisks => return parseError(
1207 docgen_tokenizer,
1208 source_token,
1209 "syntax error",
1210 .{},
1211 ),
1212 }
1213 index = token.loc.end;
1214 }
1215 try out.writeAll(end_line ++ "</code>");
1216}
1217
1218fn tokenizeAndPrint(
1219 allocator: Allocator,
1220 docgen_tokenizer: *Tokenizer,
1221 out: anytype,
1222 source_token: Token,
1223) !void {
1224 const raw_src = docgen_tokenizer.buffer[source_token.start..source_token.end];
1225 return tokenizeAndPrintRaw(allocator, docgen_tokenizer, out, source_token, raw_src);
1226}
1227
1228fn printSourceBlock(allocator: Allocator, docgen_tokenizer: *Tokenizer, out: anytype, syntax_block: SyntaxBlock) !void {
1229 const source_type = @tagName(syntax_block.source_type);
1230
1231 try out.print("<figure><figcaption class=\"{s}-cap\"><cite class=\"file\">{s}</cite></figcaption><pre>", .{ source_type, syntax_block.name });
1232 switch (syntax_block.source_type) {
1233 .zig => try tokenizeAndPrint(allocator, docgen_tokenizer, out, syntax_block.source_token),
1234 else => {
1235 const raw_source = docgen_tokenizer.buffer[syntax_block.source_token.start..syntax_block.source_token.end];
1236 const trimmed_raw_source = mem.trim(u8, raw_source, " \n");
1237
1238 try out.writeAll("<code>" ++ start_line);
1239 try writeEscapedLines(out, trimmed_raw_source);
1240 try out.writeAll(end_line ++ "</code>");
1241 },
1242 }
1243 try out.writeAll("</pre></figure>");
1244}
1245
1246fn printShell(out: anytype, shell_content: []const u8, escape: bool) !void {
1247 const trimmed_shell_content = mem.trim(u8, shell_content, " \n");
1248 try out.writeAll("<figure><figcaption class=\"shell-cap\">Shell</figcaption><pre><samp>");
1249 var cmd_cont: bool = false;
1250 var iter = std.mem.splitScalar(u8, trimmed_shell_content, '\n');
1251 while (iter.next()) |orig_line| {
1252 const line = mem.trimRight(u8, orig_line, " ");
1253 if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] != '\\') {
1254 try out.writeAll("$ <kbd>");
1255 const s = std.mem.trimLeft(u8, line[1..], " ");
1256 if (escape) {
1257 try writeEscaped(out, s);
1258 } else {
1259 try out.writeAll(s);
1260 }
1261 try out.writeAll("</kbd>" ++ "\n");
1262 } else if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] == '\\') {
1263 try out.writeAll("$ <kbd>");
1264 const s = std.mem.trimLeft(u8, line[1..], " ");
1265 if (escape) {
1266 try writeEscaped(out, s);
1267 } else {
1268 try out.writeAll(s);
1269 }
1270 try out.writeAll("\n");
1271 cmd_cont = true;
1272 } else if (line.len > 0 and line[line.len - 1] != '\\' and cmd_cont) {
1273 if (escape) {
1274 try writeEscaped(out, line);
1275 } else {
1276 try out.writeAll(line);
1277 }
1278 try out.writeAll("</kbd>" ++ "\n");
1279 cmd_cont = false;
1280 } else {
1281 if (escape) {
1282 try writeEscaped(out, line);
1283 } else {
1284 try out.writeAll(line);
1285 }
1286 try out.writeAll("\n");
1287 }
1288 }
1289
1290 try out.writeAll("</samp></pre></figure>");
1291}
1292
1293// Override this to skip to later tests
1294const debug_start_line = 0;
1295
1296fn genHtml(
1297 allocator: Allocator,
1298 tokenizer: *Tokenizer,
1299 toc: *Toc,
1300 out: anytype,
1301 zig_exe: []const u8,
1302 opt_zig_lib_dir: ?[]const u8,
1303 do_code_tests: bool,
1304) !void {
1305 var progress = Progress{ .dont_print_on_dumb = true };
1306 const root_node = progress.start("Generating docgen examples", toc.nodes.len);
1307 defer root_node.end();
1308
1309 var env_map = try process.getEnvMap(allocator);
1310 try env_map.put("YES_COLOR", "1");
1311
1312 const host = try std.zig.system.NativeTargetInfo.detect(.{});
1313 const builtin_code = try getBuiltinCode(allocator, &env_map, zig_exe, opt_zig_lib_dir);
1314
1315 for (toc.nodes) |node| {
1316 defer root_node.completeOne();
1317 switch (node) {
1318 .Content => |data| {
1319 try out.writeAll(data);
1320 },
1321 .Link => |info| {
1322 if (!toc.urls.contains(info.url)) {
1323 return parseError(tokenizer, info.token, "url not found: {s}", .{info.url});
1324 }
1325 try out.print("<a href=\"#{s}\">{s}</a>", .{ info.url, info.name });
1326 },
1327 .Nav => {
1328 try out.writeAll(toc.toc);
1329 },
1330 .Builtin => |tok| {
1331 try out.writeAll("<figure><figcaption class=\"zig-cap\"><cite>@import(\"builtin\")</cite></figcaption><pre>");
1332 try tokenizeAndPrintRaw(allocator, tokenizer, out, tok, builtin_code);
1333 try out.writeAll("</pre></figure>");
1334 },
1335 .HeaderOpen => |info| {
1336 try out.print(
1337 "<h{d} id=\"{s}\"><a href=\"#toc-{s}\">{s}</a> <a class=\"hdr\" href=\"#{s}\">§</a></h{d}>\n",
1338 .{ info.n, info.url, info.url, info.name, info.url, info.n },
1339 );
1340 },
1341 .SeeAlso => |items| {
1342 try out.writeAll("<p>See also:</p><ul>\n");
1343 for (items) |item| {
1344 const url = try urlize(allocator, item.name);
1345 if (!toc.urls.contains(url)) {
1346 return parseError(tokenizer, item.token, "url not found: {s}", .{url});
1347 }
1348 try out.print("<li><a href=\"#{s}\">{s}</a></li>\n", .{ url, item.name });
1349 }
1350 try out.writeAll("</ul>\n");
1351 },
1352 .InlineSyntax => |content_tok| {
1353 try tokenizeAndPrint(allocator, tokenizer, out, content_tok);
1354 },
1355 .Shell => |content_tok| {
1356 const raw_shell_content = tokenizer.buffer[content_tok.start..content_tok.end];
1357 try printShell(out, raw_shell_content, true);
1358 },
1359 .SyntaxBlock => |syntax_block| {
1360 try printSourceBlock(allocator, tokenizer, out, syntax_block);
1361 },
1362 .Code => |code| {
1363 const name_plus_ext = try std.fmt.allocPrint(allocator, "{s}.zig", .{code.name});
1364 const syntax_block = SyntaxBlock{
1365 .source_type = .zig,
1366 .name = name_plus_ext,
1367 .source_token = code.source_token,
1368 };
1369
1370 try printSourceBlock(allocator, tokenizer, out, syntax_block);
1371
1372 if (!do_code_tests) {
1373 continue;
1374 }
1375
1376 if (debug_start_line > 0) {
1377 const loc = tokenizer.getTokenLocation(code.source_token);
1378 if (debug_start_line > loc.line) {
1379 continue;
1380 }
1381 }
1382
1383 const raw_source = tokenizer.buffer[code.source_token.start..code.source_token.end];
1384 const trimmed_raw_source = mem.trim(u8, raw_source, " \n");
1385 const tmp_source_file_name = try fs.path.join(
1386 allocator,
1387 &[_][]const u8{ tmp_dir_name, name_plus_ext },
1388 );
1389 try fs.cwd().writeFile(tmp_source_file_name, trimmed_raw_source);
1390
1391 var shell_buffer = std.ArrayList(u8).init(allocator);
1392 defer shell_buffer.deinit();
1393 var shell_out = shell_buffer.writer();
1394
1395 switch (code.id) {
1396 .exe => |expected_outcome| code_block: {
1397 var build_args = std.ArrayList([]const u8).init(allocator);
1398 defer build_args.deinit();
1399 try build_args.appendSlice(&[_][]const u8{
1400 zig_exe, "build-exe",
1401 "--name", code.name,
1402 "--color", "on",
1403 name_plus_ext,
1404 });
1405 if (opt_zig_lib_dir) |zig_lib_dir| {
1406 try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1407 }
1408
1409 try shell_out.print("$ zig build-exe {s} ", .{name_plus_ext});
1410
1411 switch (code.mode) {
1412 .Debug => {},
1413 else => {
1414 try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1415 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1416 },
1417 }
1418 for (code.link_objects) |link_object| {
1419 const name_with_ext = try std.fmt.allocPrint(allocator, "{s}{s}", .{ link_object, obj_ext });
1420 try build_args.append(name_with_ext);
1421 try shell_out.print("{s} ", .{name_with_ext});
1422 }
1423 if (code.link_libc) {
1424 try build_args.append("-lc");
1425 try shell_out.print("-lc ", .{});
1426 }
1427 const target = try std.zig.CrossTarget.parse(.{
1428 .arch_os_abi = code.target_str orelse "native",
1429 });
1430 if (code.target_str) |triple| {
1431 try build_args.appendSlice(&[_][]const u8{ "-target", triple });
1432 try shell_out.print("-target {s} ", .{triple});
1433 }
1434 if (code.verbose_cimport) {
1435 try build_args.append("--verbose-cimport");
1436 try shell_out.print("--verbose-cimport ", .{});
1437 }
1438 for (code.additional_options) |option| {
1439 try build_args.append(option);
1440 try shell_out.print("{s} ", .{option});
1441 }
1442
1443 try shell_out.print("\n", .{});
1444
1445 if (expected_outcome == .build_fail) {
1446 const result = try ChildProcess.exec(.{
1447 .allocator = allocator,
1448 .argv = build_args.items,
1449 .cwd = tmp_dir_name,
1450 .env_map = &env_map,
1451 .max_output_bytes = max_doc_file_size,
1452 });
1453 switch (result.term) {
1454 .Exited => |exit_code| {
1455 if (exit_code == 0) {
1456 progress.log("", .{});
1457 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1458 dumpArgs(build_args.items);
1459 return parseError(tokenizer, code.source_token, "example incorrectly compiled", .{});
1460 }
1461 },
1462 else => {
1463 progress.log("", .{});
1464 print("{s}\nThe following command crashed:\n", .{result.stderr});
1465 dumpArgs(build_args.items);
1466 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1467 },
1468 }
1469 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1470 const colored_stderr = try termColor(allocator, escaped_stderr);
1471 try shell_out.writeAll(colored_stderr);
1472 break :code_block;
1473 }
1474 const exec_result = exec(allocator, &env_map, tmp_dir_name, build_args.items) catch
1475 return parseError(tokenizer, code.source_token, "example failed to compile", .{});
1476
1477 if (code.verbose_cimport) {
1478 const escaped_build_stderr = try escapeHtml(allocator, exec_result.stderr);
1479 try shell_out.writeAll(escaped_build_stderr);
1480 }
1481
1482 if (code.target_str) |triple| {
1483 if (mem.startsWith(u8, triple, "wasm32") or
1484 mem.startsWith(u8, triple, "riscv64-linux") or
1485 (mem.startsWith(u8, triple, "x86_64-linux") and
1486 builtin.os.tag != .linux or builtin.cpu.arch != .x86_64))
1487 {
1488 // skip execution
1489 break :code_block;
1490 }
1491 }
1492
1493 const path_to_exe = try std.fmt.allocPrint(allocator, "./{s}{s}", .{
1494 code.name,
1495 target.exeFileExt(),
1496 });
1497 const run_args = &[_][]const u8{path_to_exe};
1498
1499 var exited_with_signal = false;
1500
1501 const result = if (expected_outcome == .fail) blk: {
1502 const result = try ChildProcess.exec(.{
1503 .allocator = allocator,
1504 .argv = run_args,
1505 .env_map = &env_map,
1506 .cwd = tmp_dir_name,
1507 .max_output_bytes = max_doc_file_size,
1508 });
1509 switch (result.term) {
1510 .Exited => |exit_code| {
1511 if (exit_code == 0) {
1512 progress.log("", .{});
1513 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1514 dumpArgs(run_args);
1515 return parseError(tokenizer, code.source_token, "example incorrectly compiled", .{});
1516 }
1517 },
1518 .Signal => exited_with_signal = true,
1519 else => {},
1520 }
1521 break :blk result;
1522 } else blk: {
1523 break :blk exec(allocator, &env_map, tmp_dir_name, run_args) catch return parseError(tokenizer, code.source_token, "example crashed", .{});
1524 };
1525
1526 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1527 const escaped_stdout = try escapeHtml(allocator, result.stdout);
1528
1529 const colored_stderr = try termColor(allocator, escaped_stderr);
1530 const colored_stdout = try termColor(allocator, escaped_stdout);
1531
1532 try shell_out.print("$ ./{s}\n{s}{s}", .{ code.name, colored_stdout, colored_stderr });
1533 if (exited_with_signal) {
1534 try shell_out.print("(process terminated by signal)", .{});
1535 }
1536 try shell_out.writeAll("\n");
1537 },
1538 .@"test" => {
1539 var test_args = std.ArrayList([]const u8).init(allocator);
1540 defer test_args.deinit();
1541
1542 try test_args.appendSlice(&[_][]const u8{
1543 zig_exe, "test",
1544 tmp_source_file_name,
1545 });
1546 if (opt_zig_lib_dir) |zig_lib_dir| {
1547 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1548 }
1549 try shell_out.print("$ zig test {s}.zig ", .{code.name});
1550
1551 switch (code.mode) {
1552 .Debug => {},
1553 else => {
1554 try test_args.appendSlice(&[_][]const u8{
1555 "-O", @tagName(code.mode),
1556 });
1557 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1558 },
1559 }
1560 if (code.link_libc) {
1561 try test_args.append("-lc");
1562 try shell_out.print("-lc ", .{});
1563 }
1564 if (code.target_str) |triple| {
1565 try test_args.appendSlice(&[_][]const u8{ "-target", triple });
1566 try shell_out.print("-target {s} ", .{triple});
1567
1568 const cross_target = try std.zig.CrossTarget.parse(.{
1569 .arch_os_abi = triple,
1570 });
1571 const target_info = try std.zig.system.NativeTargetInfo.detect(
1572 cross_target,
1573 );
1574 switch (host.getExternalExecutor(target_info, .{
1575 .link_libc = code.link_libc,
1576 })) {
1577 .native => {},
1578 else => {
1579 try test_args.appendSlice(&[_][]const u8{"--test-no-exec"});
1580 try shell_out.writeAll("--test-no-exec");
1581 },
1582 }
1583 }
1584 const result = exec(allocator, &env_map, null, test_args.items) catch
1585 return parseError(tokenizer, code.source_token, "test failed", .{});
1586 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1587 const escaped_stdout = try escapeHtml(allocator, result.stdout);
1588 try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
1589 },
1590 .test_error => |error_match| {
1591 var test_args = std.ArrayList([]const u8).init(allocator);
1592 defer test_args.deinit();
1593
1594 try test_args.appendSlice(&[_][]const u8{
1595 zig_exe, "test",
1596 "--color", "on",
1597 tmp_source_file_name,
1598 });
1599 if (opt_zig_lib_dir) |zig_lib_dir| {
1600 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1601 }
1602 try shell_out.print("$ zig test {s}.zig ", .{code.name});
1603
1604 switch (code.mode) {
1605 .Debug => {},
1606 else => {
1607 try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1608 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1609 },
1610 }
1611 if (code.link_libc) {
1612 try test_args.append("-lc");
1613 try shell_out.print("-lc ", .{});
1614 }
1615 const result = try ChildProcess.exec(.{
1616 .allocator = allocator,
1617 .argv = test_args.items,
1618 .env_map = &env_map,
1619 .max_output_bytes = max_doc_file_size,
1620 });
1621 switch (result.term) {
1622 .Exited => |exit_code| {
1623 if (exit_code == 0) {
1624 progress.log("", .{});
1625 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1626 dumpArgs(test_args.items);
1627 return parseError(tokenizer, code.source_token, "example incorrectly compiled", .{});
1628 }
1629 },
1630 else => {
1631 progress.log("", .{});
1632 print("{s}\nThe following command crashed:\n", .{result.stderr});
1633 dumpArgs(test_args.items);
1634 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1635 },
1636 }
1637 if (mem.indexOf(u8, result.stderr, error_match) == null) {
1638 progress.log("", .{});
1639 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
1640 return parseError(tokenizer, code.source_token, "example did not have expected compile error", .{});
1641 }
1642 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1643 const colored_stderr = try termColor(allocator, escaped_stderr);
1644 try shell_out.print("\n{s}\n", .{colored_stderr});
1645 },
1646 .test_safety => |error_match| {
1647 var test_args = std.ArrayList([]const u8).init(allocator);
1648 defer test_args.deinit();
1649
1650 try test_args.appendSlice(&[_][]const u8{
1651 zig_exe, "test",
1652 tmp_source_file_name,
1653 });
1654 if (opt_zig_lib_dir) |zig_lib_dir| {
1655 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1656 }
1657 var mode_arg: []const u8 = "";
1658 switch (code.mode) {
1659 .Debug => {},
1660 .ReleaseSafe => {
1661 try test_args.append("-OReleaseSafe");
1662 mode_arg = "-OReleaseSafe";
1663 },
1664 .ReleaseFast => {
1665 try test_args.append("-OReleaseFast");
1666 mode_arg = "-OReleaseFast";
1667 },
1668 .ReleaseSmall => {
1669 try test_args.append("-OReleaseSmall");
1670 mode_arg = "-OReleaseSmall";
1671 },
1672 }
1673
1674 const result = try ChildProcess.exec(.{
1675 .allocator = allocator,
1676 .argv = test_args.items,
1677 .env_map = &env_map,
1678 .max_output_bytes = max_doc_file_size,
1679 });
1680 switch (result.term) {
1681 .Exited => |exit_code| {
1682 if (exit_code == 0) {
1683 progress.log("", .{});
1684 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1685 dumpArgs(test_args.items);
1686 return parseError(tokenizer, code.source_token, "example test incorrectly succeeded", .{});
1687 }
1688 },
1689 else => {
1690 progress.log("", .{});
1691 print("{s}\nThe following command crashed:\n", .{result.stderr});
1692 dumpArgs(test_args.items);
1693 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1694 },
1695 }
1696 if (mem.indexOf(u8, result.stderr, error_match) == null) {
1697 progress.log("", .{});
1698 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
1699 return parseError(tokenizer, code.source_token, "example did not have expected runtime safety error message", .{});
1700 }
1701 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1702 const colored_stderr = try termColor(allocator, escaped_stderr);
1703 try shell_out.print("$ zig test {s}.zig {s}\n{s}\n", .{
1704 code.name,
1705 mode_arg,
1706 colored_stderr,
1707 });
1708 },
1709 .obj => |maybe_error_match| {
1710 const name_plus_obj_ext = try std.fmt.allocPrint(allocator, "{s}{s}", .{ code.name, obj_ext });
1711 var build_args = std.ArrayList([]const u8).init(allocator);
1712 defer build_args.deinit();
1713
1714 try build_args.appendSlice(&[_][]const u8{
1715 zig_exe, "build-obj",
1716 "--color", "on",
1717 "--name", code.name,
1718 tmp_source_file_name,
1719 try std.fmt.allocPrint(allocator, "-femit-bin={s}{c}{s}", .{
1720 tmp_dir_name, fs.path.sep, name_plus_obj_ext,
1721 }),
1722 });
1723 if (opt_zig_lib_dir) |zig_lib_dir| {
1724 try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1725 }
1726
1727 try shell_out.print("$ zig build-obj {s}.zig ", .{code.name});
1728
1729 switch (code.mode) {
1730 .Debug => {},
1731 else => {
1732 try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1733 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1734 },
1735 }
1736
1737 if (code.target_str) |triple| {
1738 try build_args.appendSlice(&[_][]const u8{ "-target", triple });
1739 try shell_out.print("-target {s} ", .{triple});
1740 }
1741 for (code.additional_options) |option| {
1742 try build_args.append(option);
1743 try shell_out.print("{s} ", .{option});
1744 }
1745
1746 if (maybe_error_match) |error_match| {
1747 const result = try ChildProcess.exec(.{
1748 .allocator = allocator,
1749 .argv = build_args.items,
1750 .env_map = &env_map,
1751 .max_output_bytes = max_doc_file_size,
1752 });
1753 switch (result.term) {
1754 .Exited => |exit_code| {
1755 if (exit_code == 0) {
1756 progress.log("", .{});
1757 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1758 dumpArgs(build_args.items);
1759 return parseError(tokenizer, code.source_token, "example build incorrectly succeeded", .{});
1760 }
1761 },
1762 else => {
1763 progress.log("", .{});
1764 print("{s}\nThe following command crashed:\n", .{result.stderr});
1765 dumpArgs(build_args.items);
1766 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1767 },
1768 }
1769 if (mem.indexOf(u8, result.stderr, error_match) == null) {
1770 progress.log("", .{});
1771 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
1772 return parseError(tokenizer, code.source_token, "example did not have expected compile error message", .{});
1773 }
1774 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1775 const colored_stderr = try termColor(allocator, escaped_stderr);
1776 try shell_out.print("\n{s} ", .{colored_stderr});
1777 } else {
1778 _ = exec(allocator, &env_map, null, build_args.items) catch return parseError(tokenizer, code.source_token, "example failed to compile", .{});
1779 }
1780 try shell_out.writeAll("\n");
1781 },
1782 .lib => {
1783 const bin_basename = try std.zig.binNameAlloc(allocator, .{
1784 .root_name = code.name,
1785 .target = builtin.target,
1786 .output_mode = .Lib,
1787 });
1788
1789 var test_args = std.ArrayList([]const u8).init(allocator);
1790 defer test_args.deinit();
1791
1792 try test_args.appendSlice(&[_][]const u8{
1793 zig_exe, "build-lib",
1794 tmp_source_file_name,
1795 try std.fmt.allocPrint(allocator, "-femit-bin={s}{s}{s}", .{
1796 tmp_dir_name, fs.path.sep_str, bin_basename,
1797 }),
1798 });
1799 if (opt_zig_lib_dir) |zig_lib_dir| {
1800 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1801 }
1802 try shell_out.print("$ zig build-lib {s}.zig ", .{code.name});
1803
1804 switch (code.mode) {
1805 .Debug => {},
1806 else => {
1807 try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1808 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1809 },
1810 }
1811 if (code.target_str) |triple| {
1812 try test_args.appendSlice(&[_][]const u8{ "-target", triple });
1813 try shell_out.print("-target {s} ", .{triple});
1814 }
1815 if (code.link_mode) |link_mode| {
1816 switch (link_mode) {
1817 .Static => {
1818 try test_args.append("-static");
1819 try shell_out.print("-static ", .{});
1820 },
1821 .Dynamic => {
1822 try test_args.append("-dynamic");
1823 try shell_out.print("-dynamic ", .{});
1824 },
1825 }
1826 }
1827 for (code.additional_options) |option| {
1828 try test_args.append(option);
1829 try shell_out.print("{s} ", .{option});
1830 }
1831 const result = exec(allocator, &env_map, null, test_args.items) catch return parseError(tokenizer, code.source_token, "test failed", .{});
1832 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1833 const escaped_stdout = try escapeHtml(allocator, result.stdout);
1834 try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
1835 },
1836 }
1837
1838 if (!code.just_check_syntax) {
1839 try printShell(out, shell_buffer.items, false);
1840 }
1841 },
1842 }
1843 }
1844}
1845
1846fn exec(
1847 allocator: Allocator,
1848 env_map: *process.EnvMap,
1849 cwd: ?[]const u8,
1850 args: []const []const u8,
1851) !ChildProcess.ExecResult {
1852 const result = try ChildProcess.exec(.{
1853 .allocator = allocator,
1854 .argv = args,
1855 .env_map = env_map,
1856 .cwd = cwd,
1857 .max_output_bytes = max_doc_file_size,
1858 });
1859 switch (result.term) {
1860 .Exited => |exit_code| {
1861 if (exit_code != 0) {
1862 print("{s}\nThe following command exited with code {}:\n", .{ result.stderr, exit_code });
1863 dumpArgs(args);
1864 return error.ChildExitError;
1865 }
1866 },
1867 else => {
1868 print("{s}\nThe following command crashed:\n", .{result.stderr});
1869 dumpArgs(args);
1870 return error.ChildCrashed;
1871 },
1872 }
1873 return result;
1874}
1875
1876fn getBuiltinCode(
1877 allocator: Allocator,
1878 env_map: *process.EnvMap,
1879 zig_exe: []const u8,
1880 opt_zig_lib_dir: ?[]const u8,
1881) ![]const u8 {
1882 if (opt_zig_lib_dir) |zig_lib_dir| {
1883 const result = try exec(allocator, env_map, null, &.{
1884 zig_exe, "build-obj", "--show-builtin", "--zig-lib-dir", zig_lib_dir,
1885 });
1886 return result.stdout;
1887 } else {
1888 const result = try exec(allocator, env_map, null, &.{
1889 zig_exe, "build-obj", "--show-builtin",
1890 });
1891 return result.stdout;
1892 }
1893}
1894
1895fn dumpArgs(args: []const []const u8) void {
1896 for (args) |arg|
1897 print("{s} ", .{arg})
1898 else
1899 print("\n", .{});
1900}
1901
1902test "term supported colors" {
1903 const test_allocator = testing.allocator;
1904
1905 {
1906 const input = "A\x1b[31;1mred\x1b[0mB";
1907 const expect = "A<span class=\"sgr-31_1m\">red</span>B";
1908
1909 const result = try termColor(test_allocator, input);
1910 defer test_allocator.free(result);
1911 try testing.expectEqualSlices(u8, expect, result);
1912 }
1913
1914 {
1915 const input = "A\x1b[32;1mgreen\x1b[0mB";
1916 const expect = "A<span class=\"sgr-32_1m\">green</span>B";
1917
1918 const result = try termColor(test_allocator, input);
1919 defer test_allocator.free(result);
1920 try testing.expectEqualSlices(u8, expect, result);
1921 }
1922
1923 {
1924 const input = "A\x1b[36;1mcyan\x1b[0mB";
1925 const expect = "A<span class=\"sgr-36_1m\">cyan</span>B";
1926
1927 const result = try termColor(test_allocator, input);
1928 defer test_allocator.free(result);
1929 try testing.expectEqualSlices(u8, expect, result);
1930 }
1931
1932 {
1933 const input = "A\x1b[1mbold\x1b[0mB";
1934 const expect = "A<span class=\"sgr-1m\">bold</span>B";
1935
1936 const result = try termColor(test_allocator, input);
1937 defer test_allocator.free(result);
1938 try testing.expectEqualSlices(u8, expect, result);
1939 }
1940
1941 {
1942 const input = "A\x1b[2mdim\x1b[0mB";
1943 const expect = "A<span class=\"sgr-2m\">dim</span>B";
1944
1945 const result = try termColor(test_allocator, input);
1946 defer test_allocator.free(result);
1947 try testing.expectEqualSlices(u8, expect, result);
1948 }
1949}
1950
1951test "term output from zig" {
1952 // Use data generated by https://github.com/perillo/zig-tty-test-data,
1953 // with zig version 0.11.0-dev.1898+36d47dd19.
1954 const test_allocator = testing.allocator;
1955
1956 {
1957 // 1.1-with-build-progress.out
1958 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";
1959 const expect = "";
1960
1961 const result = try termColor(test_allocator, input);
1962 defer test_allocator.free(result);
1963 try testing.expectEqualSlices(u8, expect, result);
1964 }
1965
1966 {
1967 // 2.1-with-reference-traces.out
1968 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";
1969 const expect =
1970 \\<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
1971 \\</span> x += 1;
1972 \\ <span class="sgr-32_1m">~~^~~~
1973 \\</span><span class="sgr-2m">referenced by:
1974 \\ main: src/2.1-with-reference-traces.zig:7:5
1975 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
1976 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces
1977 \\
1978 \\</span>
1979 ;
1980
1981 const result = try termColor(test_allocator, input);
1982 defer test_allocator.free(result);
1983 try testing.expectEqualSlices(u8, expect, result);
1984 }
1985
1986 {
1987 // 2.2-without-reference-traces.out
1988 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";
1989 const expect =
1990 \\<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
1991 \\</span> else => @compileError("invalid type given to fixedBufferStream"),
1992 \\ <span class="sgr-32_1m">^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1993 \\</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
1994 \\</span>pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {
1995 \\; <span class="sgr-32_1m">~~~~~^~~~~~~~~~~~~~~~~
1996 \\</span>
1997 ;
1998
1999 const result = try termColor(test_allocator, input);
2000 defer test_allocator.free(result);
2001 try testing.expectEqualSlices(u8, expect, result);
2002 }
2003
2004 {
2005 // 2.3-with-notes.out
2006 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";
2007 const expect =
2008 \\<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'
2009 \\</span> bar(w);
2010 \\ <span class="sgr-32_1m">^
2011 \\</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'
2012 \\</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
2013 \\</span>const Wat = opaque {};
2014 \\ <span class="sgr-32_1m">^~~~~~~~~
2015 \\</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
2016 \\</span>const Derp = opaque {};
2017 \\ <span class="sgr-32_1m">^~~~~~~~~
2018 \\</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
2019 \\</span>extern fn bar(d: *Derp) void;
2020 \\ <span class="sgr-32_1m">^~~~~
2021 \\</span><span class="sgr-2m">referenced by:
2022 \\ main: src/2.3-with-notes.zig:10:5
2023 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
2024 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces
2025 \\
2026 \\</span>
2027 ;
2028
2029 const result = try termColor(test_allocator, input);
2030 defer test_allocator.free(result);
2031 try testing.expectEqualSlices(u8, expect, result);
2032 }
2033
2034 {
2035 // 3.1-with-error-return-traces.out
2036
2037 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";
2038 const expect =
2039 \\error: Error
2040 \\<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>
2041 \\ return error.Error;
2042 \\ <span class="sgr-32_1m">^</span>
2043 \\<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>
2044 \\ try callee();
2045 \\ <span class="sgr-32_1m">^</span>
2046 \\<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>
2047 \\ try caller();
2048 \\ <span class="sgr-32_1m">^</span>
2049 \\
2050 ;
2051
2052 const result = try termColor(test_allocator, input);
2053 defer test_allocator.free(result);
2054 try testing.expectEqualSlices(u8, expect, result);
2055 }
2056
2057 {
2058 // 3.2-with-stack-trace.out
2059 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";
2060 const expect =
2061 \\<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>
2062 \\ while (it.next()) |return_address| {
2063 \\ <span class="sgr-32_1m">^</span>
2064 \\<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>
2065 \\ writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {
2066 \\ <span class="sgr-32_1m">^</span>
2067 \\<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>
2068 \\ std.debug.dumpCurrentStackTrace(null);
2069 \\ <span class="sgr-32_1m">^</span>
2070 \\<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>
2071 \\ foo();
2072 \\ <span class="sgr-32_1m">^</span>
2073 \\<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>
2074 \\ root.main();
2075 \\ <span class="sgr-32_1m">^</span>
2076 \\<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>
2077 \\ @call(.never_inline, posixCallMainAndExit, .{});
2078 \\ <span class="sgr-32_1m">^</span>
2079 \\
2080 ;
2081
2082 const result = try termColor(test_allocator, input);
2083 defer test_allocator.free(result);
2084 try testing.expectEqualSlices(u8, expect, result);
2085 }
2086}
2087
2088test "printShell" {
2089 const test_allocator = std.testing.allocator;
2090
2091 {
2092 const shell_out =
2093 \\$ zig build test.zig
2094 ;
2095 const expected =
2096 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2097 \\</samp></pre></figure>
2098 ;
2099
2100 var buffer = std.ArrayList(u8).init(test_allocator);
2101 defer buffer.deinit();
2102
2103 try printShell(buffer.writer(), shell_out, false);
2104 try testing.expectEqualSlices(u8, expected, buffer.items);
2105 }
2106 {
2107 const shell_out =
2108 \\$ zig build test.zig
2109 \\build output
2110 ;
2111 const expected =
2112 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2113 \\build output
2114 \\</samp></pre></figure>
2115 ;
2116
2117 var buffer = std.ArrayList(u8).init(test_allocator);
2118 defer buffer.deinit();
2119
2120 try printShell(buffer.writer(), shell_out, false);
2121 try testing.expectEqualSlices(u8, expected, buffer.items);
2122 }
2123 {
2124 const shell_out =
2125 \\$ zig build test.zig
2126 \\build output
2127 \\$ ./test
2128 ;
2129 const expected =
2130 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2131 \\build output
2132 \\$ <kbd>./test</kbd>
2133 \\</samp></pre></figure>
2134 ;
2135
2136 var buffer = std.ArrayList(u8).init(test_allocator);
2137 defer buffer.deinit();
2138
2139 try printShell(buffer.writer(), shell_out, false);
2140 try testing.expectEqualSlices(u8, expected, buffer.items);
2141 }
2142 {
2143 const shell_out =
2144 \\$ zig build test.zig
2145 \\
2146 \\$ ./test
2147 \\output
2148 ;
2149 const expected =
2150 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2151 \\
2152 \\$ <kbd>./test</kbd>
2153 \\output
2154 \\</samp></pre></figure>
2155 ;
2156
2157 var buffer = std.ArrayList(u8).init(test_allocator);
2158 defer buffer.deinit();
2159
2160 try printShell(buffer.writer(), shell_out, false);
2161 try testing.expectEqualSlices(u8, expected, buffer.items);
2162 }
2163 {
2164 const shell_out =
2165 \\$ zig build test.zig
2166 \\$ ./test
2167 \\output
2168 ;
2169 const expected =
2170 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2171 \\$ <kbd>./test</kbd>
2172 \\output
2173 \\</samp></pre></figure>
2174 ;
2175
2176 var buffer = std.ArrayList(u8).init(test_allocator);
2177 defer buffer.deinit();
2178
2179 try printShell(buffer.writer(), shell_out, false);
2180 try testing.expectEqualSlices(u8, expected, buffer.items);
2181 }
2182 {
2183 const shell_out =
2184 \\$ zig build test.zig \
2185 \\ --build-option
2186 \\build output
2187 \\$ ./test
2188 \\output
2189 ;
2190 const expected =
2191 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
2192 \\ --build-option</kbd>
2193 \\build output
2194 \\$ <kbd>./test</kbd>
2195 \\output
2196 \\</samp></pre></figure>
2197 ;
2198
2199 var buffer = std.ArrayList(u8).init(test_allocator);
2200 defer buffer.deinit();
2201
2202 try printShell(buffer.writer(), shell_out, false);
2203 try testing.expectEqualSlices(u8, expected, buffer.items);
2204 }
2205 {
2206 // intentional space after "--build-option1 \"
2207 const shell_out =
2208 \\$ zig build test.zig \
2209 \\ --build-option1 \
2210 \\ --build-option2
2211 \\$ ./test
2212 ;
2213 const expected =
2214 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
2215 \\ --build-option1 \
2216 \\ --build-option2</kbd>
2217 \\$ <kbd>./test</kbd>
2218 \\</samp></pre></figure>
2219 ;
2220
2221 var buffer = std.ArrayList(u8).init(test_allocator);
2222 defer buffer.deinit();
2223
2224 try printShell(buffer.writer(), shell_out, false);
2225 try testing.expectEqualSlices(u8, expected, buffer.items);
2226 }
2227 {
2228 const shell_out =
2229 \\$ zig build test.zig \
2230 \\$ ./test
2231 ;
2232 const expected =
2233 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
2234 \\$ ./test</kbd>
2235 \\</samp></pre></figure>
2236 ;
2237
2238 var buffer = std.ArrayList(u8).init(test_allocator);
2239 defer buffer.deinit();
2240
2241 try printShell(buffer.writer(), shell_out, false);
2242 try testing.expectEqualSlices(u8, expected, buffer.items);
2243 }
2244 {
2245 const shell_out =
2246 \\$ zig build test.zig
2247 \\$ ./test
2248 \\$1
2249 ;
2250 const expected =
2251 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2252 \\$ <kbd>./test</kbd>
2253 \\$1
2254 \\</samp></pre></figure>
2255 ;
2256
2257 var buffer = std.ArrayList(u8).init(test_allocator);
2258 defer buffer.deinit();
2259
2260 try printShell(buffer.writer(), shell_out, false);
2261 try testing.expectEqualSlices(u8, expected, buffer.items);
2262 }
2263 {
2264 const shell_out =
2265 \\$zig build test.zig
2266 ;
2267 const expected =
2268 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$zig build test.zig
2269 \\</samp></pre></figure>
2270 ;
2271
2272 var buffer = std.ArrayList(u8).init(test_allocator);
2273 defer buffer.deinit();
2274
2275 try printShell(buffer.writer(), shell_out, false);
2276 try testing.expectEqualSlices(u8, expected, buffer.items);
2277 }
2278}
tools/docgen.zig created+2278
......@@ -0,0 +1,2278 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const io = std.io;
4const fs = std.fs;
5const process = std.process;
6const ChildProcess = std.ChildProcess;
7const Progress = std.Progress;
8const print = std.debug.print;
9const mem = std.mem;
10const testing = std.testing;
11const Allocator = std.mem.Allocator;
12
13const max_doc_file_size = 10 * 1024 * 1024;
14
15const exe_ext = @as(std.zig.CrossTarget, .{}).exeFileExt();
16const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch);
17const tmp_dir_name = "docgen_tmp";
18const test_out_path = tmp_dir_name ++ fs.path.sep_str ++ "test" ++ exe_ext;
19
20const usage =
21 \\Usage: docgen [--zig] [--skip-code-tests] input output"
22 \\
23 \\ Generates an HTML document from a docgen template.
24 \\
25 \\Options:
26 \\ -h, --help Print this help and exit
27 \\ --skip-code-tests Skip the doctests
28 \\
29;
30
31fn fatal(comptime format: []const u8, args: anytype) noreturn {
32 const stderr = io.getStdErr().writer();
33
34 stderr.print("error: " ++ format ++ "\n", args) catch {};
35 process.exit(1);
36}
37
38pub fn main() !void {
39 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
40 defer arena.deinit();
41
42 const allocator = arena.allocator();
43
44 var args_it = try process.argsWithAllocator(allocator);
45 if (!args_it.skip()) @panic("expected self arg");
46
47 var zig_exe: []const u8 = "zig";
48 var opt_zig_lib_dir: ?[]const u8 = null;
49 var do_code_tests = true;
50 var files = [_][]const u8{ "", "" };
51
52 var i: usize = 0;
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 const stdout = io.getStdOut().writer();
57 try stdout.writeAll(usage);
58 process.exit(0);
59 } else if (mem.eql(u8, arg, "--zig")) {
60 if (args_it.next()) |param| {
61 zig_exe = param;
62 } else {
63 fatal("expected parameter after --zig", .{});
64 }
65 } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
66 if (args_it.next()) |param| {
67 // Convert relative to absolute because this will be passed
68 // to a child process with a different cwd.
69 opt_zig_lib_dir = try fs.realpathAlloc(allocator, param);
70 } else {
71 fatal("expected parameter after --zig-lib-dir", .{});
72 }
73 } else if (mem.eql(u8, arg, "--skip-code-tests")) {
74 do_code_tests = false;
75 } else {
76 fatal("unrecognized option: '{s}'", .{arg});
77 }
78 } else {
79 if (i > 1) {
80 fatal("too many arguments", .{});
81 }
82 files[i] = arg;
83 i += 1;
84 }
85 }
86 if (i < 2) {
87 fatal("not enough arguments", .{});
88 }
89
90 var in_file = try fs.cwd().openFile(files[0], .{ .mode = .read_only });
91 defer in_file.close();
92
93 var out_file = try fs.cwd().createFile(files[1], .{});
94 defer out_file.close();
95
96 const input_file_bytes = try in_file.reader().readAllAlloc(allocator, max_doc_file_size);
97
98 var buffered_writer = io.bufferedWriter(out_file.writer());
99
100 var tokenizer = Tokenizer.init(files[0], input_file_bytes);
101 var toc = try genToc(allocator, &tokenizer);
102
103 try fs.cwd().makePath(tmp_dir_name);
104 defer fs.cwd().deleteTree(tmp_dir_name) catch {};
105
106 try genHtml(allocator, &tokenizer, &toc, buffered_writer.writer(), zig_exe, opt_zig_lib_dir, do_code_tests);
107 try buffered_writer.flush();
108}
109
110const Token = struct {
111 id: Id,
112 start: usize,
113 end: usize,
114
115 const Id = enum {
116 invalid,
117 content,
118 bracket_open,
119 tag_content,
120 separator,
121 bracket_close,
122 eof,
123 };
124};
125
126const Tokenizer = struct {
127 buffer: []const u8,
128 index: usize,
129 state: State,
130 source_file_name: []const u8,
131 code_node_count: usize,
132
133 const State = enum {
134 start,
135 l_bracket,
136 hash,
137 tag_name,
138 eof,
139 };
140
141 fn init(source_file_name: []const u8, buffer: []const u8) Tokenizer {
142 return Tokenizer{
143 .buffer = buffer,
144 .index = 0,
145 .state = .start,
146 .source_file_name = source_file_name,
147 .code_node_count = 0,
148 };
149 }
150
151 fn next(self: *Tokenizer) Token {
152 var result = Token{
153 .id = .eof,
154 .start = self.index,
155 .end = undefined,
156 };
157 while (self.index < self.buffer.len) : (self.index += 1) {
158 const c = self.buffer[self.index];
159 switch (self.state) {
160 .start => switch (c) {
161 '{' => {
162 self.state = .l_bracket;
163 },
164 else => {
165 result.id = .content;
166 },
167 },
168 .l_bracket => switch (c) {
169 '#' => {
170 if (result.id != .eof) {
171 self.index -= 1;
172 self.state = .start;
173 break;
174 } else {
175 result.id = .bracket_open;
176 self.index += 1;
177 self.state = .tag_name;
178 break;
179 }
180 },
181 else => {
182 result.id = .content;
183 self.state = .start;
184 },
185 },
186 .tag_name => switch (c) {
187 '|' => {
188 if (result.id != .eof) {
189 break;
190 } else {
191 result.id = .separator;
192 self.index += 1;
193 break;
194 }
195 },
196 '#' => {
197 self.state = .hash;
198 },
199 else => {
200 result.id = .tag_content;
201 },
202 },
203 .hash => switch (c) {
204 '}' => {
205 if (result.id != .eof) {
206 self.index -= 1;
207 self.state = .tag_name;
208 break;
209 } else {
210 result.id = .bracket_close;
211 self.index += 1;
212 self.state = .start;
213 break;
214 }
215 },
216 else => {
217 result.id = .tag_content;
218 self.state = .tag_name;
219 },
220 },
221 .eof => unreachable,
222 }
223 } else {
224 switch (self.state) {
225 .start, .l_bracket, .eof => {},
226 else => {
227 result.id = .invalid;
228 },
229 }
230 self.state = .eof;
231 }
232 result.end = self.index;
233 return result;
234 }
235
236 const Location = struct {
237 line: usize,
238 column: usize,
239 line_start: usize,
240 line_end: usize,
241 };
242
243 fn getTokenLocation(self: *Tokenizer, token: Token) Location {
244 var loc = Location{
245 .line = 0,
246 .column = 0,
247 .line_start = 0,
248 .line_end = 0,
249 };
250 for (self.buffer, 0..) |c, i| {
251 if (i == token.start) {
252 loc.line_end = i;
253 while (loc.line_end < self.buffer.len and self.buffer[loc.line_end] != '\n') : (loc.line_end += 1) {}
254 return loc;
255 }
256 if (c == '\n') {
257 loc.line += 1;
258 loc.column = 0;
259 loc.line_start = i + 1;
260 } else {
261 loc.column += 1;
262 }
263 }
264 return loc;
265 }
266};
267
268fn parseError(tokenizer: *Tokenizer, token: Token, comptime fmt: []const u8, args: anytype) anyerror {
269 const loc = tokenizer.getTokenLocation(token);
270 const args_prefix = .{ tokenizer.source_file_name, loc.line + 1, loc.column + 1 };
271 print("{s}:{d}:{d}: error: " ++ fmt ++ "\n", args_prefix ++ args);
272 if (loc.line_start <= loc.line_end) {
273 print("{s}\n", .{tokenizer.buffer[loc.line_start..loc.line_end]});
274 {
275 var i: usize = 0;
276 while (i < loc.column) : (i += 1) {
277 print(" ", .{});
278 }
279 }
280 {
281 const caret_count = @min(token.end, loc.line_end) - token.start;
282 var i: usize = 0;
283 while (i < caret_count) : (i += 1) {
284 print("~", .{});
285 }
286 }
287 print("\n", .{});
288 }
289 return error.ParseError;
290}
291
292fn assertToken(tokenizer: *Tokenizer, token: Token, id: Token.Id) !void {
293 if (token.id != id) {
294 return parseError(tokenizer, token, "expected {s}, found {s}", .{ @tagName(id), @tagName(token.id) });
295 }
296}
297
298fn eatToken(tokenizer: *Tokenizer, id: Token.Id) !Token {
299 const token = tokenizer.next();
300 try assertToken(tokenizer, token, id);
301 return token;
302}
303
304const HeaderOpen = struct {
305 name: []const u8,
306 url: []const u8,
307 n: usize,
308};
309
310const SeeAlsoItem = struct {
311 name: []const u8,
312 token: Token,
313};
314
315const ExpectedOutcome = enum {
316 succeed,
317 fail,
318 build_fail,
319};
320
321const Code = struct {
322 id: Id,
323 name: []const u8,
324 source_token: Token,
325 just_check_syntax: bool,
326 mode: std.builtin.Mode,
327 link_objects: []const []const u8,
328 target_str: ?[]const u8,
329 link_libc: bool,
330 link_mode: ?std.builtin.LinkMode,
331 disable_cache: bool,
332 verbose_cimport: bool,
333 additional_options: []const []const u8,
334
335 const Id = union(enum) {
336 @"test",
337 test_error: []const u8,
338 test_safety: []const u8,
339 exe: ExpectedOutcome,
340 obj: ?[]const u8,
341 lib,
342 };
343};
344
345const Link = struct {
346 url: []const u8,
347 name: []const u8,
348 token: Token,
349};
350
351const SyntaxBlock = struct {
352 source_type: SourceType,
353 name: []const u8,
354 source_token: Token,
355
356 const SourceType = enum {
357 zig,
358 c,
359 peg,
360 javascript,
361 };
362};
363
364const Node = union(enum) {
365 Content: []const u8,
366 Nav,
367 Builtin: Token,
368 HeaderOpen: HeaderOpen,
369 SeeAlso: []const SeeAlsoItem,
370 Code: Code,
371 Link: Link,
372 InlineSyntax: Token,
373 Shell: Token,
374 SyntaxBlock: SyntaxBlock,
375};
376
377const Toc = struct {
378 nodes: []Node,
379 toc: []u8,
380 urls: std.StringHashMap(Token),
381};
382
383const Action = enum {
384 open,
385 close,
386};
387
388fn genToc(allocator: Allocator, tokenizer: *Tokenizer) !Toc {
389 var urls = std.StringHashMap(Token).init(allocator);
390 errdefer urls.deinit();
391
392 var header_stack_size: usize = 0;
393 var last_action: Action = .open;
394 var last_columns: ?u8 = null;
395
396 var toc_buf = std.ArrayList(u8).init(allocator);
397 defer toc_buf.deinit();
398
399 var toc = toc_buf.writer();
400
401 var nodes = std.ArrayList(Node).init(allocator);
402 defer nodes.deinit();
403
404 try toc.writeByte('\n');
405
406 while (true) {
407 const token = tokenizer.next();
408 switch (token.id) {
409 .eof => {
410 if (header_stack_size != 0) {
411 return parseError(tokenizer, token, "unbalanced headers", .{});
412 }
413 try toc.writeAll(" </ul>\n");
414 break;
415 },
416 .content => {
417 try nodes.append(Node{ .Content = tokenizer.buffer[token.start..token.end] });
418 },
419 .bracket_open => {
420 const tag_token = try eatToken(tokenizer, .tag_content);
421 const tag_name = tokenizer.buffer[tag_token.start..tag_token.end];
422
423 if (mem.eql(u8, tag_name, "nav")) {
424 _ = try eatToken(tokenizer, .bracket_close);
425
426 try nodes.append(Node.Nav);
427 } else if (mem.eql(u8, tag_name, "builtin")) {
428 _ = try eatToken(tokenizer, .bracket_close);
429 try nodes.append(Node{ .Builtin = tag_token });
430 } else if (mem.eql(u8, tag_name, "header_open")) {
431 _ = try eatToken(tokenizer, .separator);
432 const content_token = try eatToken(tokenizer, .tag_content);
433 const content = tokenizer.buffer[content_token.start..content_token.end];
434 var columns: ?u8 = null;
435 while (true) {
436 const bracket_tok = tokenizer.next();
437 switch (bracket_tok.id) {
438 .bracket_close => break,
439 .separator => continue,
440 .tag_content => {
441 const param = tokenizer.buffer[bracket_tok.start..bracket_tok.end];
442 if (mem.eql(u8, param, "2col")) {
443 columns = 2;
444 } else {
445 return parseError(
446 tokenizer,
447 bracket_tok,
448 "unrecognized header_open param: {s}",
449 .{param},
450 );
451 }
452 },
453 else => return parseError(tokenizer, bracket_tok, "invalid header_open token", .{}),
454 }
455 }
456
457 header_stack_size += 1;
458
459 const urlized = try urlize(allocator, content);
460 try nodes.append(Node{
461 .HeaderOpen = HeaderOpen{
462 .name = content,
463 .url = urlized,
464 .n = header_stack_size + 1, // highest-level section headers start at h2
465 },
466 });
467 if (try urls.fetchPut(urlized, tag_token)) |kv| {
468 parseError(tokenizer, tag_token, "duplicate header url: #{s}", .{urlized}) catch {};
469 parseError(tokenizer, kv.value, "other tag here", .{}) catch {};
470 return error.ParseError;
471 }
472 if (last_action == .open) {
473 try toc.writeByte('\n');
474 try toc.writeByteNTimes(' ', header_stack_size * 4);
475 if (last_columns) |n| {
476 try toc.print("<ul style=\"columns: {}\">\n", .{n});
477 } else {
478 try toc.writeAll("<ul>\n");
479 }
480 } else {
481 last_action = .open;
482 }
483 last_columns = columns;
484 try toc.writeByteNTimes(' ', 4 + header_stack_size * 4);
485 try toc.print("<li><a id=\"toc-{s}\" href=\"#{s}\">{s}</a>", .{ urlized, urlized, content });
486 } else if (mem.eql(u8, tag_name, "header_close")) {
487 if (header_stack_size == 0) {
488 return parseError(tokenizer, tag_token, "unbalanced close header", .{});
489 }
490 header_stack_size -= 1;
491 _ = try eatToken(tokenizer, .bracket_close);
492
493 if (last_action == .close) {
494 try toc.writeByteNTimes(' ', 8 + header_stack_size * 4);
495 try toc.writeAll("</ul></li>\n");
496 } else {
497 try toc.writeAll("</li>\n");
498 last_action = .close;
499 }
500 } else if (mem.eql(u8, tag_name, "see_also")) {
501 var list = std.ArrayList(SeeAlsoItem).init(allocator);
502 errdefer list.deinit();
503
504 while (true) {
505 const see_also_tok = tokenizer.next();
506 switch (see_also_tok.id) {
507 .tag_content => {
508 const content = tokenizer.buffer[see_also_tok.start..see_also_tok.end];
509 try list.append(SeeAlsoItem{
510 .name = content,
511 .token = see_also_tok,
512 });
513 },
514 .separator => {},
515 .bracket_close => {
516 try nodes.append(Node{ .SeeAlso = try list.toOwnedSlice() });
517 break;
518 },
519 else => return parseError(tokenizer, see_also_tok, "invalid see_also token", .{}),
520 }
521 }
522 } else if (mem.eql(u8, tag_name, "link")) {
523 _ = try eatToken(tokenizer, .separator);
524 const name_tok = try eatToken(tokenizer, .tag_content);
525 const name = tokenizer.buffer[name_tok.start..name_tok.end];
526
527 const url_name = blk: {
528 const tok = tokenizer.next();
529 switch (tok.id) {
530 .bracket_close => break :blk name,
531 .separator => {
532 const explicit_text = try eatToken(tokenizer, .tag_content);
533 _ = try eatToken(tokenizer, .bracket_close);
534 break :blk tokenizer.buffer[explicit_text.start..explicit_text.end];
535 },
536 else => return parseError(tokenizer, tok, "invalid link token", .{}),
537 }
538 };
539
540 try nodes.append(Node{
541 .Link = Link{
542 .url = try urlize(allocator, url_name),
543 .name = name,
544 .token = name_tok,
545 },
546 });
547 } else if (mem.eql(u8, tag_name, "code_begin")) {
548 _ = try eatToken(tokenizer, .separator);
549 const code_kind_tok = try eatToken(tokenizer, .tag_content);
550 _ = try eatToken(tokenizer, .separator);
551 const name_tok = try eatToken(tokenizer, .tag_content);
552 const name = tokenizer.buffer[name_tok.start..name_tok.end];
553 var error_str: []const u8 = "";
554 const maybe_sep = tokenizer.next();
555 switch (maybe_sep.id) {
556 .separator => {
557 const error_tok = try eatToken(tokenizer, .tag_content);
558 error_str = tokenizer.buffer[error_tok.start..error_tok.end];
559 _ = try eatToken(tokenizer, .bracket_close);
560 },
561 .bracket_close => {},
562 else => return parseError(tokenizer, token, "invalid token", .{}),
563 }
564 const code_kind_str = tokenizer.buffer[code_kind_tok.start..code_kind_tok.end];
565 var code_kind_id: Code.Id = undefined;
566 var just_check_syntax = false;
567 if (mem.eql(u8, code_kind_str, "exe")) {
568 code_kind_id = Code.Id{ .exe = .succeed };
569 } else if (mem.eql(u8, code_kind_str, "exe_err")) {
570 code_kind_id = Code.Id{ .exe = .fail };
571 } else if (mem.eql(u8, code_kind_str, "exe_build_err")) {
572 code_kind_id = Code.Id{ .exe = .build_fail };
573 } else if (mem.eql(u8, code_kind_str, "test")) {
574 code_kind_id = .@"test";
575 } else if (mem.eql(u8, code_kind_str, "test_err")) {
576 code_kind_id = Code.Id{ .test_error = error_str };
577 } else if (mem.eql(u8, code_kind_str, "test_safety")) {
578 code_kind_id = Code.Id{ .test_safety = error_str };
579 } else if (mem.eql(u8, code_kind_str, "obj")) {
580 code_kind_id = Code.Id{ .obj = null };
581 } else if (mem.eql(u8, code_kind_str, "obj_err")) {
582 code_kind_id = Code.Id{ .obj = error_str };
583 } else if (mem.eql(u8, code_kind_str, "lib")) {
584 code_kind_id = Code.Id.lib;
585 } else if (mem.eql(u8, code_kind_str, "syntax")) {
586 code_kind_id = Code.Id{ .obj = null };
587 just_check_syntax = true;
588 } else {
589 return parseError(tokenizer, code_kind_tok, "unrecognized code kind: {s}", .{code_kind_str});
590 }
591
592 var mode: std.builtin.Mode = .Debug;
593 var link_objects = std.ArrayList([]const u8).init(allocator);
594 defer link_objects.deinit();
595 var target_str: ?[]const u8 = null;
596 var link_libc = false;
597 var link_mode: ?std.builtin.LinkMode = null;
598 var disable_cache = false;
599 var verbose_cimport = false;
600 var additional_options = std.ArrayList([]const u8).init(allocator);
601 defer additional_options.deinit();
602
603 const source_token = while (true) {
604 const content_tok = try eatToken(tokenizer, .content);
605 _ = try eatToken(tokenizer, .bracket_open);
606 const end_code_tag = try eatToken(tokenizer, .tag_content);
607 const end_tag_name = tokenizer.buffer[end_code_tag.start..end_code_tag.end];
608 if (mem.eql(u8, end_tag_name, "code_release_fast")) {
609 mode = .ReleaseFast;
610 } else if (mem.eql(u8, end_tag_name, "code_release_safe")) {
611 mode = .ReleaseSafe;
612 } else if (mem.eql(u8, end_tag_name, "code_disable_cache")) {
613 disable_cache = true;
614 } else if (mem.eql(u8, end_tag_name, "code_verbose_cimport")) {
615 verbose_cimport = true;
616 } else if (mem.eql(u8, end_tag_name, "code_link_object")) {
617 _ = try eatToken(tokenizer, .separator);
618 const obj_tok = try eatToken(tokenizer, .tag_content);
619 try link_objects.append(tokenizer.buffer[obj_tok.start..obj_tok.end]);
620 } else if (mem.eql(u8, end_tag_name, "target_windows")) {
621 target_str = "x86_64-windows";
622 } else if (mem.eql(u8, end_tag_name, "target_linux_x86_64")) {
623 target_str = "x86_64-linux";
624 } else if (mem.eql(u8, end_tag_name, "target_linux_riscv64")) {
625 target_str = "riscv64-linux";
626 } else if (mem.eql(u8, end_tag_name, "target_wasm")) {
627 target_str = "wasm32-freestanding";
628 } else if (mem.eql(u8, end_tag_name, "target_wasi")) {
629 target_str = "wasm32-wasi";
630 } else if (mem.eql(u8, end_tag_name, "link_libc")) {
631 link_libc = true;
632 } else if (mem.eql(u8, end_tag_name, "link_mode_dynamic")) {
633 link_mode = .Dynamic;
634 } else if (mem.eql(u8, end_tag_name, "additonal_option")) {
635 _ = try eatToken(tokenizer, .separator);
636 const option = try eatToken(tokenizer, .tag_content);
637 try additional_options.append(tokenizer.buffer[option.start..option.end]);
638 } else if (mem.eql(u8, end_tag_name, "code_end")) {
639 _ = try eatToken(tokenizer, .bracket_close);
640 break content_tok;
641 } else {
642 return parseError(
643 tokenizer,
644 end_code_tag,
645 "invalid token inside code_begin: {s}",
646 .{end_tag_name},
647 );
648 }
649 _ = try eatToken(tokenizer, .bracket_close);
650 } else unreachable; // TODO issue #707
651 try nodes.append(Node{
652 .Code = Code{
653 .id = code_kind_id,
654 .name = name,
655 .source_token = source_token,
656 .just_check_syntax = just_check_syntax,
657 .mode = mode,
658 .link_objects = try link_objects.toOwnedSlice(),
659 .target_str = target_str,
660 .link_libc = link_libc,
661 .link_mode = link_mode,
662 .disable_cache = disable_cache,
663 .verbose_cimport = verbose_cimport,
664 .additional_options = try additional_options.toOwnedSlice(),
665 },
666 });
667 tokenizer.code_node_count += 1;
668 } else if (mem.eql(u8, tag_name, "syntax")) {
669 _ = try eatToken(tokenizer, .bracket_close);
670 const content_tok = try eatToken(tokenizer, .content);
671 _ = try eatToken(tokenizer, .bracket_open);
672 const end_syntax_tag = try eatToken(tokenizer, .tag_content);
673 const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end];
674 if (!mem.eql(u8, end_tag_name, "endsyntax")) {
675 return parseError(
676 tokenizer,
677 end_syntax_tag,
678 "invalid token inside syntax: {s}",
679 .{end_tag_name},
680 );
681 }
682 _ = try eatToken(tokenizer, .bracket_close);
683 try nodes.append(Node{ .InlineSyntax = content_tok });
684 } else if (mem.eql(u8, tag_name, "shell_samp")) {
685 _ = try eatToken(tokenizer, .bracket_close);
686 const content_tok = try eatToken(tokenizer, .content);
687 _ = try eatToken(tokenizer, .bracket_open);
688 const end_syntax_tag = try eatToken(tokenizer, .tag_content);
689 const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end];
690 if (!mem.eql(u8, end_tag_name, "end_shell_samp")) {
691 return parseError(
692 tokenizer,
693 end_syntax_tag,
694 "invalid token inside syntax: {s}",
695 .{end_tag_name},
696 );
697 }
698 _ = try eatToken(tokenizer, .bracket_close);
699 try nodes.append(Node{ .Shell = content_tok });
700 } else if (mem.eql(u8, tag_name, "syntax_block")) {
701 _ = try eatToken(tokenizer, .separator);
702 const source_type_tok = try eatToken(tokenizer, .tag_content);
703 var name: []const u8 = "sample_code";
704 const maybe_sep = tokenizer.next();
705 switch (maybe_sep.id) {
706 .separator => {
707 const name_tok = try eatToken(tokenizer, .tag_content);
708 name = tokenizer.buffer[name_tok.start..name_tok.end];
709 _ = try eatToken(tokenizer, .bracket_close);
710 },
711 .bracket_close => {},
712 else => return parseError(tokenizer, token, "invalid token", .{}),
713 }
714 const source_type_str = tokenizer.buffer[source_type_tok.start..source_type_tok.end];
715 var source_type: SyntaxBlock.SourceType = undefined;
716 if (mem.eql(u8, source_type_str, "zig")) {
717 source_type = SyntaxBlock.SourceType.zig;
718 } else if (mem.eql(u8, source_type_str, "c")) {
719 source_type = SyntaxBlock.SourceType.c;
720 } else if (mem.eql(u8, source_type_str, "peg")) {
721 source_type = SyntaxBlock.SourceType.peg;
722 } else if (mem.eql(u8, source_type_str, "javascript")) {
723 source_type = SyntaxBlock.SourceType.javascript;
724 } else {
725 return parseError(tokenizer, source_type_tok, "unrecognized code kind: {s}", .{source_type_str});
726 }
727 const source_token = while (true) {
728 const content_tok = try eatToken(tokenizer, .content);
729 _ = try eatToken(tokenizer, .bracket_open);
730 const end_code_tag = try eatToken(tokenizer, .tag_content);
731 const end_tag_name = tokenizer.buffer[end_code_tag.start..end_code_tag.end];
732 if (mem.eql(u8, end_tag_name, "end_syntax_block")) {
733 _ = try eatToken(tokenizer, .bracket_close);
734 break content_tok;
735 } else {
736 return parseError(
737 tokenizer,
738 end_code_tag,
739 "invalid token inside code_begin: {s}",
740 .{end_tag_name},
741 );
742 }
743 _ = try eatToken(tokenizer, .bracket_close);
744 };
745 try nodes.append(Node{ .SyntaxBlock = SyntaxBlock{ .source_type = source_type, .name = name, .source_token = source_token } });
746 } else {
747 return parseError(tokenizer, tag_token, "unrecognized tag name: {s}", .{tag_name});
748 }
749 },
750 else => return parseError(tokenizer, token, "invalid token", .{}),
751 }
752 }
753
754 return Toc{
755 .nodes = try nodes.toOwnedSlice(),
756 .toc = try toc_buf.toOwnedSlice(),
757 .urls = urls,
758 };
759}
760
761fn urlize(allocator: Allocator, input: []const u8) ![]u8 {
762 var buf = std.ArrayList(u8).init(allocator);
763 defer buf.deinit();
764
765 const out = buf.writer();
766 for (input) |c| {
767 switch (c) {
768 'a'...'z', 'A'...'Z', '_', '-', '0'...'9' => {
769 try out.writeByte(c);
770 },
771 ' ' => {
772 try out.writeByte('-');
773 },
774 else => {},
775 }
776 }
777 return try buf.toOwnedSlice();
778}
779
780fn escapeHtml(allocator: Allocator, input: []const u8) ![]u8 {
781 var buf = std.ArrayList(u8).init(allocator);
782 defer buf.deinit();
783
784 const out = buf.writer();
785 try writeEscaped(out, input);
786 return try buf.toOwnedSlice();
787}
788
789fn writeEscaped(out: anytype, input: []const u8) !void {
790 for (input) |c| {
791 try switch (c) {
792 '&' => out.writeAll("&amp;"),
793 '<' => out.writeAll("&lt;"),
794 '>' => out.writeAll("&gt;"),
795 '"' => out.writeAll("&quot;"),
796 else => out.writeByte(c),
797 };
798 }
799}
800
801// Returns true if number is in slice.
802fn in(slice: []const u8, number: u8) bool {
803 for (slice) |n| {
804 if (number == n) return true;
805 }
806 return false;
807}
808
809fn termColor(allocator: Allocator, input: []const u8) ![]u8 {
810 // The SRG sequences generates by the Zig compiler are in the format:
811 // ESC [ <foreground-color> ; <n> m
812 // or
813 // ESC [ <n> m
814 //
815 // where
816 // foreground-color is 31 (red), 32 (green), 36 (cyan)
817 // n is 0 (reset), 1 (bold), 2 (dim)
818 //
819 // Note that 37 (white) is currently not used by the compiler.
820 //
821 // See std.debug.TTY.Color.
822 const supported_sgr_colors = [_]u8{ 31, 32, 36 };
823 const supported_sgr_numbers = [_]u8{ 0, 1, 2 };
824
825 var buf = std.ArrayList(u8).init(allocator);
826 defer buf.deinit();
827
828 var out = buf.writer();
829 var sgr_param_start_index: usize = undefined;
830 var sgr_num: u8 = undefined;
831 var sgr_color: u8 = undefined;
832 var i: usize = 0;
833 var state: enum {
834 start,
835 escape,
836 lbracket,
837 number,
838 after_number,
839 arg,
840 arg_number,
841 expect_end,
842 } = .start;
843 var last_new_line: usize = 0;
844 var open_span_count: usize = 0;
845 while (i < input.len) : (i += 1) {
846 const c = input[i];
847 switch (state) {
848 .start => switch (c) {
849 '\x1b' => state = .escape,
850 '\n' => {
851 try out.writeByte(c);
852 last_new_line = buf.items.len;
853 },
854 else => try out.writeByte(c),
855 },
856 .escape => switch (c) {
857 '[' => state = .lbracket,
858 else => return error.UnsupportedEscape,
859 },
860 .lbracket => switch (c) {
861 '0'...'9' => {
862 sgr_param_start_index = i;
863 state = .number;
864 },
865 else => return error.UnsupportedEscape,
866 },
867 .number => switch (c) {
868 '0'...'9' => {},
869 else => {
870 sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
871 sgr_color = 0;
872 state = .after_number;
873 i -= 1;
874 },
875 },
876 .after_number => switch (c) {
877 ';' => state = .arg,
878 'D' => state = .start,
879 'K' => {
880 buf.items.len = last_new_line;
881 state = .start;
882 },
883 else => {
884 state = .expect_end;
885 i -= 1;
886 },
887 },
888 .arg => switch (c) {
889 '0'...'9' => {
890 sgr_param_start_index = i;
891 state = .arg_number;
892 },
893 else => return error.UnsupportedEscape,
894 },
895 .arg_number => switch (c) {
896 '0'...'9' => {},
897 else => {
898 // Keep the sequence consistent, foreground color first.
899 // 32;1m is equivalent to 1;32m, but the latter will
900 // generate an incorrect HTML class without notice.
901 sgr_color = sgr_num;
902 if (!in(&supported_sgr_colors, sgr_color)) return error.UnsupportedForegroundColor;
903
904 sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10);
905 if (!in(&supported_sgr_numbers, sgr_num)) return error.UnsupportedNumber;
906
907 state = .expect_end;
908 i -= 1;
909 },
910 },
911 .expect_end => switch (c) {
912 'm' => {
913 state = .start;
914 while (open_span_count != 0) : (open_span_count -= 1) {
915 try out.writeAll("</span>");
916 }
917 if (sgr_num == 0) {
918 if (sgr_color != 0) return error.UnsupportedColor;
919 continue;
920 }
921 if (sgr_color != 0) {
922 try out.print("<span class=\"sgr-{d}_{d}m\">", .{ sgr_color, sgr_num });
923 } else {
924 try out.print("<span class=\"sgr-{d}m\">", .{sgr_num});
925 }
926 open_span_count += 1;
927 },
928 else => return error.UnsupportedEscape,
929 },
930 }
931 }
932 return try buf.toOwnedSlice();
933}
934
935const builtin_types = [_][]const u8{
936 "f16", "f32", "f64", "f80", "f128",
937 "c_longdouble", "c_short", "c_ushort", "c_int", "c_uint",
938 "c_long", "c_ulong", "c_longlong", "c_ulonglong", "c_char",
939 "anyopaque", "void", "bool", "isize", "usize",
940 "noreturn", "type", "anyerror", "comptime_int", "comptime_float",
941};
942
943fn isType(name: []const u8) bool {
944 for (builtin_types) |t| {
945 if (mem.eql(u8, t, name))
946 return true;
947 }
948 return false;
949}
950
951const start_line = "<span class=\"line\">";
952const end_line = "</span>";
953
954fn writeEscapedLines(out: anytype, text: []const u8) !void {
955 for (text) |char| {
956 if (char == '\n') {
957 try out.writeAll(end_line);
958 try out.writeAll("\n");
959 try out.writeAll(start_line);
960 } else {
961 try writeEscaped(out, &[_]u8{char});
962 }
963 }
964}
965
966fn tokenizeAndPrintRaw(
967 allocator: Allocator,
968 docgen_tokenizer: *Tokenizer,
969 out: anytype,
970 source_token: Token,
971 raw_src: []const u8,
972) !void {
973 const src_non_terminated = mem.trim(u8, raw_src, " \n");
974 const src = try allocator.dupeZ(u8, src_non_terminated);
975
976 try out.writeAll("<code>" ++ start_line);
977 var tokenizer = std.zig.Tokenizer.init(src);
978 var index: usize = 0;
979 var next_tok_is_fn = false;
980 while (true) {
981 const prev_tok_was_fn = next_tok_is_fn;
982 next_tok_is_fn = false;
983
984 const token = tokenizer.next();
985 if (mem.indexOf(u8, src[index..token.loc.start], "//")) |comment_start_off| {
986 // render one comment
987 const comment_start = index + comment_start_off;
988 const comment_end_off = mem.indexOf(u8, src[comment_start..token.loc.start], "\n");
989 const comment_end = if (comment_end_off) |o| comment_start + o else token.loc.start;
990
991 try writeEscapedLines(out, src[index..comment_start]);
992 try out.writeAll("<span class=\"tok-comment\">");
993 try writeEscaped(out, src[comment_start..comment_end]);
994 try out.writeAll("</span>");
995 index = comment_end;
996 tokenizer.index = index;
997 continue;
998 }
999
1000 try writeEscapedLines(out, src[index..token.loc.start]);
1001 switch (token.tag) {
1002 .eof => break,
1003
1004 .keyword_addrspace,
1005 .keyword_align,
1006 .keyword_and,
1007 .keyword_asm,
1008 .keyword_async,
1009 .keyword_await,
1010 .keyword_break,
1011 .keyword_catch,
1012 .keyword_comptime,
1013 .keyword_const,
1014 .keyword_continue,
1015 .keyword_defer,
1016 .keyword_else,
1017 .keyword_enum,
1018 .keyword_errdefer,
1019 .keyword_error,
1020 .keyword_export,
1021 .keyword_extern,
1022 .keyword_for,
1023 .keyword_if,
1024 .keyword_inline,
1025 .keyword_noalias,
1026 .keyword_noinline,
1027 .keyword_nosuspend,
1028 .keyword_opaque,
1029 .keyword_or,
1030 .keyword_orelse,
1031 .keyword_packed,
1032 .keyword_anyframe,
1033 .keyword_pub,
1034 .keyword_resume,
1035 .keyword_return,
1036 .keyword_linksection,
1037 .keyword_callconv,
1038 .keyword_struct,
1039 .keyword_suspend,
1040 .keyword_switch,
1041 .keyword_test,
1042 .keyword_threadlocal,
1043 .keyword_try,
1044 .keyword_union,
1045 .keyword_unreachable,
1046 .keyword_usingnamespace,
1047 .keyword_var,
1048 .keyword_volatile,
1049 .keyword_allowzero,
1050 .keyword_while,
1051 .keyword_anytype,
1052 => {
1053 try out.writeAll("<span class=\"tok-kw\">");
1054 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1055 try out.writeAll("</span>");
1056 },
1057
1058 .keyword_fn => {
1059 try out.writeAll("<span class=\"tok-kw\">");
1060 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1061 try out.writeAll("</span>");
1062 next_tok_is_fn = true;
1063 },
1064
1065 .string_literal,
1066 .char_literal,
1067 => {
1068 try out.writeAll("<span class=\"tok-str\">");
1069 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1070 try out.writeAll("</span>");
1071 },
1072
1073 .multiline_string_literal_line => {
1074 if (src[token.loc.end - 1] == '\n') {
1075 try out.writeAll("<span class=\"tok-str\">");
1076 try writeEscaped(out, src[token.loc.start .. token.loc.end - 1]);
1077 try out.writeAll("</span>" ++ end_line ++ "\n" ++ start_line);
1078 } else {
1079 try out.writeAll("<span class=\"tok-str\">");
1080 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1081 try out.writeAll("</span>");
1082 }
1083 },
1084
1085 .builtin => {
1086 try out.writeAll("<span class=\"tok-builtin\">");
1087 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1088 try out.writeAll("</span>");
1089 },
1090
1091 .doc_comment,
1092 .container_doc_comment,
1093 => {
1094 try out.writeAll("<span class=\"tok-comment\">");
1095 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1096 try out.writeAll("</span>");
1097 },
1098
1099 .identifier => {
1100 const tok_bytes = src[token.loc.start..token.loc.end];
1101 if (mem.eql(u8, tok_bytes, "undefined") or
1102 mem.eql(u8, tok_bytes, "null") or
1103 mem.eql(u8, tok_bytes, "true") or
1104 mem.eql(u8, tok_bytes, "false"))
1105 {
1106 try out.writeAll("<span class=\"tok-null\">");
1107 try writeEscaped(out, tok_bytes);
1108 try out.writeAll("</span>");
1109 } else if (prev_tok_was_fn) {
1110 try out.writeAll("<span class=\"tok-fn\">");
1111 try writeEscaped(out, tok_bytes);
1112 try out.writeAll("</span>");
1113 } else {
1114 const is_int = blk: {
1115 if (src[token.loc.start] != 'i' and src[token.loc.start] != 'u')
1116 break :blk false;
1117 var i = token.loc.start + 1;
1118 if (i == token.loc.end)
1119 break :blk false;
1120 while (i != token.loc.end) : (i += 1) {
1121 if (src[i] < '0' or src[i] > '9')
1122 break :blk false;
1123 }
1124 break :blk true;
1125 };
1126 if (is_int or isType(tok_bytes)) {
1127 try out.writeAll("<span class=\"tok-type\">");
1128 try writeEscaped(out, tok_bytes);
1129 try out.writeAll("</span>");
1130 } else {
1131 try writeEscaped(out, tok_bytes);
1132 }
1133 }
1134 },
1135
1136 .number_literal => {
1137 try out.writeAll("<span class=\"tok-number\">");
1138 try writeEscaped(out, src[token.loc.start..token.loc.end]);
1139 try out.writeAll("</span>");
1140 },
1141
1142 .bang,
1143 .pipe,
1144 .pipe_pipe,
1145 .pipe_equal,
1146 .equal,
1147 .equal_equal,
1148 .equal_angle_bracket_right,
1149 .bang_equal,
1150 .l_paren,
1151 .r_paren,
1152 .semicolon,
1153 .percent,
1154 .percent_equal,
1155 .l_brace,
1156 .r_brace,
1157 .l_bracket,
1158 .r_bracket,
1159 .period,
1160 .period_asterisk,
1161 .ellipsis2,
1162 .ellipsis3,
1163 .caret,
1164 .caret_equal,
1165 .plus,
1166 .plus_plus,
1167 .plus_equal,
1168 .plus_percent,
1169 .plus_percent_equal,
1170 .plus_pipe,
1171 .plus_pipe_equal,
1172 .minus,
1173 .minus_equal,
1174 .minus_percent,
1175 .minus_percent_equal,
1176 .minus_pipe,
1177 .minus_pipe_equal,
1178 .asterisk,
1179 .asterisk_equal,
1180 .asterisk_asterisk,
1181 .asterisk_percent,
1182 .asterisk_percent_equal,
1183 .asterisk_pipe,
1184 .asterisk_pipe_equal,
1185 .arrow,
1186 .colon,
1187 .slash,
1188 .slash_equal,
1189 .comma,
1190 .ampersand,
1191 .ampersand_equal,
1192 .question_mark,
1193 .angle_bracket_left,
1194 .angle_bracket_left_equal,
1195 .angle_bracket_angle_bracket_left,
1196 .angle_bracket_angle_bracket_left_equal,
1197 .angle_bracket_angle_bracket_left_pipe,
1198 .angle_bracket_angle_bracket_left_pipe_equal,
1199 .angle_bracket_right,
1200 .angle_bracket_right_equal,
1201 .angle_bracket_angle_bracket_right,
1202 .angle_bracket_angle_bracket_right_equal,
1203 .tilde,
1204 => try writeEscaped(out, src[token.loc.start..token.loc.end]),
1205
1206 .invalid, .invalid_periodasterisks => return parseError(
1207 docgen_tokenizer,
1208 source_token,
1209 "syntax error",
1210 .{},
1211 ),
1212 }
1213 index = token.loc.end;
1214 }
1215 try out.writeAll(end_line ++ "</code>");
1216}
1217
1218fn tokenizeAndPrint(
1219 allocator: Allocator,
1220 docgen_tokenizer: *Tokenizer,
1221 out: anytype,
1222 source_token: Token,
1223) !void {
1224 const raw_src = docgen_tokenizer.buffer[source_token.start..source_token.end];
1225 return tokenizeAndPrintRaw(allocator, docgen_tokenizer, out, source_token, raw_src);
1226}
1227
1228fn printSourceBlock(allocator: Allocator, docgen_tokenizer: *Tokenizer, out: anytype, syntax_block: SyntaxBlock) !void {
1229 const source_type = @tagName(syntax_block.source_type);
1230
1231 try out.print("<figure><figcaption class=\"{s}-cap\"><cite class=\"file\">{s}</cite></figcaption><pre>", .{ source_type, syntax_block.name });
1232 switch (syntax_block.source_type) {
1233 .zig => try tokenizeAndPrint(allocator, docgen_tokenizer, out, syntax_block.source_token),
1234 else => {
1235 const raw_source = docgen_tokenizer.buffer[syntax_block.source_token.start..syntax_block.source_token.end];
1236 const trimmed_raw_source = mem.trim(u8, raw_source, " \n");
1237
1238 try out.writeAll("<code>" ++ start_line);
1239 try writeEscapedLines(out, trimmed_raw_source);
1240 try out.writeAll(end_line ++ "</code>");
1241 },
1242 }
1243 try out.writeAll("</pre></figure>");
1244}
1245
1246fn printShell(out: anytype, shell_content: []const u8, escape: bool) !void {
1247 const trimmed_shell_content = mem.trim(u8, shell_content, " \n");
1248 try out.writeAll("<figure><figcaption class=\"shell-cap\">Shell</figcaption><pre><samp>");
1249 var cmd_cont: bool = false;
1250 var iter = std.mem.splitScalar(u8, trimmed_shell_content, '\n');
1251 while (iter.next()) |orig_line| {
1252 const line = mem.trimRight(u8, orig_line, " ");
1253 if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] != '\\') {
1254 try out.writeAll("$ <kbd>");
1255 const s = std.mem.trimLeft(u8, line[1..], " ");
1256 if (escape) {
1257 try writeEscaped(out, s);
1258 } else {
1259 try out.writeAll(s);
1260 }
1261 try out.writeAll("</kbd>" ++ "\n");
1262 } else if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] == '\\') {
1263 try out.writeAll("$ <kbd>");
1264 const s = std.mem.trimLeft(u8, line[1..], " ");
1265 if (escape) {
1266 try writeEscaped(out, s);
1267 } else {
1268 try out.writeAll(s);
1269 }
1270 try out.writeAll("\n");
1271 cmd_cont = true;
1272 } else if (line.len > 0 and line[line.len - 1] != '\\' and cmd_cont) {
1273 if (escape) {
1274 try writeEscaped(out, line);
1275 } else {
1276 try out.writeAll(line);
1277 }
1278 try out.writeAll("</kbd>" ++ "\n");
1279 cmd_cont = false;
1280 } else {
1281 if (escape) {
1282 try writeEscaped(out, line);
1283 } else {
1284 try out.writeAll(line);
1285 }
1286 try out.writeAll("\n");
1287 }
1288 }
1289
1290 try out.writeAll("</samp></pre></figure>");
1291}
1292
1293// Override this to skip to later tests
1294const debug_start_line = 0;
1295
1296fn genHtml(
1297 allocator: Allocator,
1298 tokenizer: *Tokenizer,
1299 toc: *Toc,
1300 out: anytype,
1301 zig_exe: []const u8,
1302 opt_zig_lib_dir: ?[]const u8,
1303 do_code_tests: bool,
1304) !void {
1305 var progress = Progress{ .dont_print_on_dumb = true };
1306 const root_node = progress.start("Generating docgen examples", toc.nodes.len);
1307 defer root_node.end();
1308
1309 var env_map = try process.getEnvMap(allocator);
1310 try env_map.put("YES_COLOR", "1");
1311
1312 const host = try std.zig.system.NativeTargetInfo.detect(.{});
1313 const builtin_code = try getBuiltinCode(allocator, &env_map, zig_exe, opt_zig_lib_dir);
1314
1315 for (toc.nodes) |node| {
1316 defer root_node.completeOne();
1317 switch (node) {
1318 .Content => |data| {
1319 try out.writeAll(data);
1320 },
1321 .Link => |info| {
1322 if (!toc.urls.contains(info.url)) {
1323 return parseError(tokenizer, info.token, "url not found: {s}", .{info.url});
1324 }
1325 try out.print("<a href=\"#{s}\">{s}</a>", .{ info.url, info.name });
1326 },
1327 .Nav => {
1328 try out.writeAll(toc.toc);
1329 },
1330 .Builtin => |tok| {
1331 try out.writeAll("<figure><figcaption class=\"zig-cap\"><cite>@import(\"builtin\")</cite></figcaption><pre>");
1332 try tokenizeAndPrintRaw(allocator, tokenizer, out, tok, builtin_code);
1333 try out.writeAll("</pre></figure>");
1334 },
1335 .HeaderOpen => |info| {
1336 try out.print(
1337 "<h{d} id=\"{s}\"><a href=\"#toc-{s}\">{s}</a> <a class=\"hdr\" href=\"#{s}\">§</a></h{d}>\n",
1338 .{ info.n, info.url, info.url, info.name, info.url, info.n },
1339 );
1340 },
1341 .SeeAlso => |items| {
1342 try out.writeAll("<p>See also:</p><ul>\n");
1343 for (items) |item| {
1344 const url = try urlize(allocator, item.name);
1345 if (!toc.urls.contains(url)) {
1346 return parseError(tokenizer, item.token, "url not found: {s}", .{url});
1347 }
1348 try out.print("<li><a href=\"#{s}\">{s}</a></li>\n", .{ url, item.name });
1349 }
1350 try out.writeAll("</ul>\n");
1351 },
1352 .InlineSyntax => |content_tok| {
1353 try tokenizeAndPrint(allocator, tokenizer, out, content_tok);
1354 },
1355 .Shell => |content_tok| {
1356 const raw_shell_content = tokenizer.buffer[content_tok.start..content_tok.end];
1357 try printShell(out, raw_shell_content, true);
1358 },
1359 .SyntaxBlock => |syntax_block| {
1360 try printSourceBlock(allocator, tokenizer, out, syntax_block);
1361 },
1362 .Code => |code| {
1363 const name_plus_ext = try std.fmt.allocPrint(allocator, "{s}.zig", .{code.name});
1364 const syntax_block = SyntaxBlock{
1365 .source_type = .zig,
1366 .name = name_plus_ext,
1367 .source_token = code.source_token,
1368 };
1369
1370 try printSourceBlock(allocator, tokenizer, out, syntax_block);
1371
1372 if (!do_code_tests) {
1373 continue;
1374 }
1375
1376 if (debug_start_line > 0) {
1377 const loc = tokenizer.getTokenLocation(code.source_token);
1378 if (debug_start_line > loc.line) {
1379 continue;
1380 }
1381 }
1382
1383 const raw_source = tokenizer.buffer[code.source_token.start..code.source_token.end];
1384 const trimmed_raw_source = mem.trim(u8, raw_source, " \n");
1385 const tmp_source_file_name = try fs.path.join(
1386 allocator,
1387 &[_][]const u8{ tmp_dir_name, name_plus_ext },
1388 );
1389 try fs.cwd().writeFile(tmp_source_file_name, trimmed_raw_source);
1390
1391 var shell_buffer = std.ArrayList(u8).init(allocator);
1392 defer shell_buffer.deinit();
1393 var shell_out = shell_buffer.writer();
1394
1395 switch (code.id) {
1396 .exe => |expected_outcome| code_block: {
1397 var build_args = std.ArrayList([]const u8).init(allocator);
1398 defer build_args.deinit();
1399 try build_args.appendSlice(&[_][]const u8{
1400 zig_exe, "build-exe",
1401 "--name", code.name,
1402 "--color", "on",
1403 name_plus_ext,
1404 });
1405 if (opt_zig_lib_dir) |zig_lib_dir| {
1406 try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1407 }
1408
1409 try shell_out.print("$ zig build-exe {s} ", .{name_plus_ext});
1410
1411 switch (code.mode) {
1412 .Debug => {},
1413 else => {
1414 try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1415 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1416 },
1417 }
1418 for (code.link_objects) |link_object| {
1419 const name_with_ext = try std.fmt.allocPrint(allocator, "{s}{s}", .{ link_object, obj_ext });
1420 try build_args.append(name_with_ext);
1421 try shell_out.print("{s} ", .{name_with_ext});
1422 }
1423 if (code.link_libc) {
1424 try build_args.append("-lc");
1425 try shell_out.print("-lc ", .{});
1426 }
1427 const target = try std.zig.CrossTarget.parse(.{
1428 .arch_os_abi = code.target_str orelse "native",
1429 });
1430 if (code.target_str) |triple| {
1431 try build_args.appendSlice(&[_][]const u8{ "-target", triple });
1432 try shell_out.print("-target {s} ", .{triple});
1433 }
1434 if (code.verbose_cimport) {
1435 try build_args.append("--verbose-cimport");
1436 try shell_out.print("--verbose-cimport ", .{});
1437 }
1438 for (code.additional_options) |option| {
1439 try build_args.append(option);
1440 try shell_out.print("{s} ", .{option});
1441 }
1442
1443 try shell_out.print("\n", .{});
1444
1445 if (expected_outcome == .build_fail) {
1446 const result = try ChildProcess.exec(.{
1447 .allocator = allocator,
1448 .argv = build_args.items,
1449 .cwd = tmp_dir_name,
1450 .env_map = &env_map,
1451 .max_output_bytes = max_doc_file_size,
1452 });
1453 switch (result.term) {
1454 .Exited => |exit_code| {
1455 if (exit_code == 0) {
1456 progress.log("", .{});
1457 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1458 dumpArgs(build_args.items);
1459 return parseError(tokenizer, code.source_token, "example incorrectly compiled", .{});
1460 }
1461 },
1462 else => {
1463 progress.log("", .{});
1464 print("{s}\nThe following command crashed:\n", .{result.stderr});
1465 dumpArgs(build_args.items);
1466 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1467 },
1468 }
1469 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1470 const colored_stderr = try termColor(allocator, escaped_stderr);
1471 try shell_out.writeAll(colored_stderr);
1472 break :code_block;
1473 }
1474 const exec_result = exec(allocator, &env_map, tmp_dir_name, build_args.items) catch
1475 return parseError(tokenizer, code.source_token, "example failed to compile", .{});
1476
1477 if (code.verbose_cimport) {
1478 const escaped_build_stderr = try escapeHtml(allocator, exec_result.stderr);
1479 try shell_out.writeAll(escaped_build_stderr);
1480 }
1481
1482 if (code.target_str) |triple| {
1483 if (mem.startsWith(u8, triple, "wasm32") or
1484 mem.startsWith(u8, triple, "riscv64-linux") or
1485 (mem.startsWith(u8, triple, "x86_64-linux") and
1486 builtin.os.tag != .linux or builtin.cpu.arch != .x86_64))
1487 {
1488 // skip execution
1489 break :code_block;
1490 }
1491 }
1492
1493 const path_to_exe = try std.fmt.allocPrint(allocator, "./{s}{s}", .{
1494 code.name,
1495 target.exeFileExt(),
1496 });
1497 const run_args = &[_][]const u8{path_to_exe};
1498
1499 var exited_with_signal = false;
1500
1501 const result = if (expected_outcome == .fail) blk: {
1502 const result = try ChildProcess.exec(.{
1503 .allocator = allocator,
1504 .argv = run_args,
1505 .env_map = &env_map,
1506 .cwd = tmp_dir_name,
1507 .max_output_bytes = max_doc_file_size,
1508 });
1509 switch (result.term) {
1510 .Exited => |exit_code| {
1511 if (exit_code == 0) {
1512 progress.log("", .{});
1513 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1514 dumpArgs(run_args);
1515 return parseError(tokenizer, code.source_token, "example incorrectly compiled", .{});
1516 }
1517 },
1518 .Signal => exited_with_signal = true,
1519 else => {},
1520 }
1521 break :blk result;
1522 } else blk: {
1523 break :blk exec(allocator, &env_map, tmp_dir_name, run_args) catch return parseError(tokenizer, code.source_token, "example crashed", .{});
1524 };
1525
1526 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1527 const escaped_stdout = try escapeHtml(allocator, result.stdout);
1528
1529 const colored_stderr = try termColor(allocator, escaped_stderr);
1530 const colored_stdout = try termColor(allocator, escaped_stdout);
1531
1532 try shell_out.print("$ ./{s}\n{s}{s}", .{ code.name, colored_stdout, colored_stderr });
1533 if (exited_with_signal) {
1534 try shell_out.print("(process terminated by signal)", .{});
1535 }
1536 try shell_out.writeAll("\n");
1537 },
1538 .@"test" => {
1539 var test_args = std.ArrayList([]const u8).init(allocator);
1540 defer test_args.deinit();
1541
1542 try test_args.appendSlice(&[_][]const u8{
1543 zig_exe, "test",
1544 tmp_source_file_name,
1545 });
1546 if (opt_zig_lib_dir) |zig_lib_dir| {
1547 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1548 }
1549 try shell_out.print("$ zig test {s}.zig ", .{code.name});
1550
1551 switch (code.mode) {
1552 .Debug => {},
1553 else => {
1554 try test_args.appendSlice(&[_][]const u8{
1555 "-O", @tagName(code.mode),
1556 });
1557 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1558 },
1559 }
1560 if (code.link_libc) {
1561 try test_args.append("-lc");
1562 try shell_out.print("-lc ", .{});
1563 }
1564 if (code.target_str) |triple| {
1565 try test_args.appendSlice(&[_][]const u8{ "-target", triple });
1566 try shell_out.print("-target {s} ", .{triple});
1567
1568 const cross_target = try std.zig.CrossTarget.parse(.{
1569 .arch_os_abi = triple,
1570 });
1571 const target_info = try std.zig.system.NativeTargetInfo.detect(
1572 cross_target,
1573 );
1574 switch (host.getExternalExecutor(target_info, .{
1575 .link_libc = code.link_libc,
1576 })) {
1577 .native => {},
1578 else => {
1579 try test_args.appendSlice(&[_][]const u8{"--test-no-exec"});
1580 try shell_out.writeAll("--test-no-exec");
1581 },
1582 }
1583 }
1584 const result = exec(allocator, &env_map, null, test_args.items) catch
1585 return parseError(tokenizer, code.source_token, "test failed", .{});
1586 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1587 const escaped_stdout = try escapeHtml(allocator, result.stdout);
1588 try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
1589 },
1590 .test_error => |error_match| {
1591 var test_args = std.ArrayList([]const u8).init(allocator);
1592 defer test_args.deinit();
1593
1594 try test_args.appendSlice(&[_][]const u8{
1595 zig_exe, "test",
1596 "--color", "on",
1597 tmp_source_file_name,
1598 });
1599 if (opt_zig_lib_dir) |zig_lib_dir| {
1600 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1601 }
1602 try shell_out.print("$ zig test {s}.zig ", .{code.name});
1603
1604 switch (code.mode) {
1605 .Debug => {},
1606 else => {
1607 try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1608 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1609 },
1610 }
1611 if (code.link_libc) {
1612 try test_args.append("-lc");
1613 try shell_out.print("-lc ", .{});
1614 }
1615 const result = try ChildProcess.exec(.{
1616 .allocator = allocator,
1617 .argv = test_args.items,
1618 .env_map = &env_map,
1619 .max_output_bytes = max_doc_file_size,
1620 });
1621 switch (result.term) {
1622 .Exited => |exit_code| {
1623 if (exit_code == 0) {
1624 progress.log("", .{});
1625 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1626 dumpArgs(test_args.items);
1627 return parseError(tokenizer, code.source_token, "example incorrectly compiled", .{});
1628 }
1629 },
1630 else => {
1631 progress.log("", .{});
1632 print("{s}\nThe following command crashed:\n", .{result.stderr});
1633 dumpArgs(test_args.items);
1634 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1635 },
1636 }
1637 if (mem.indexOf(u8, result.stderr, error_match) == null) {
1638 progress.log("", .{});
1639 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
1640 return parseError(tokenizer, code.source_token, "example did not have expected compile error", .{});
1641 }
1642 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1643 const colored_stderr = try termColor(allocator, escaped_stderr);
1644 try shell_out.print("\n{s}\n", .{colored_stderr});
1645 },
1646 .test_safety => |error_match| {
1647 var test_args = std.ArrayList([]const u8).init(allocator);
1648 defer test_args.deinit();
1649
1650 try test_args.appendSlice(&[_][]const u8{
1651 zig_exe, "test",
1652 tmp_source_file_name,
1653 });
1654 if (opt_zig_lib_dir) |zig_lib_dir| {
1655 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1656 }
1657 var mode_arg: []const u8 = "";
1658 switch (code.mode) {
1659 .Debug => {},
1660 .ReleaseSafe => {
1661 try test_args.append("-OReleaseSafe");
1662 mode_arg = "-OReleaseSafe";
1663 },
1664 .ReleaseFast => {
1665 try test_args.append("-OReleaseFast");
1666 mode_arg = "-OReleaseFast";
1667 },
1668 .ReleaseSmall => {
1669 try test_args.append("-OReleaseSmall");
1670 mode_arg = "-OReleaseSmall";
1671 },
1672 }
1673
1674 const result = try ChildProcess.exec(.{
1675 .allocator = allocator,
1676 .argv = test_args.items,
1677 .env_map = &env_map,
1678 .max_output_bytes = max_doc_file_size,
1679 });
1680 switch (result.term) {
1681 .Exited => |exit_code| {
1682 if (exit_code == 0) {
1683 progress.log("", .{});
1684 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1685 dumpArgs(test_args.items);
1686 return parseError(tokenizer, code.source_token, "example test incorrectly succeeded", .{});
1687 }
1688 },
1689 else => {
1690 progress.log("", .{});
1691 print("{s}\nThe following command crashed:\n", .{result.stderr});
1692 dumpArgs(test_args.items);
1693 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1694 },
1695 }
1696 if (mem.indexOf(u8, result.stderr, error_match) == null) {
1697 progress.log("", .{});
1698 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
1699 return parseError(tokenizer, code.source_token, "example did not have expected runtime safety error message", .{});
1700 }
1701 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1702 const colored_stderr = try termColor(allocator, escaped_stderr);
1703 try shell_out.print("$ zig test {s}.zig {s}\n{s}\n", .{
1704 code.name,
1705 mode_arg,
1706 colored_stderr,
1707 });
1708 },
1709 .obj => |maybe_error_match| {
1710 const name_plus_obj_ext = try std.fmt.allocPrint(allocator, "{s}{s}", .{ code.name, obj_ext });
1711 var build_args = std.ArrayList([]const u8).init(allocator);
1712 defer build_args.deinit();
1713
1714 try build_args.appendSlice(&[_][]const u8{
1715 zig_exe, "build-obj",
1716 "--color", "on",
1717 "--name", code.name,
1718 tmp_source_file_name,
1719 try std.fmt.allocPrint(allocator, "-femit-bin={s}{c}{s}", .{
1720 tmp_dir_name, fs.path.sep, name_plus_obj_ext,
1721 }),
1722 });
1723 if (opt_zig_lib_dir) |zig_lib_dir| {
1724 try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1725 }
1726
1727 try shell_out.print("$ zig build-obj {s}.zig ", .{code.name});
1728
1729 switch (code.mode) {
1730 .Debug => {},
1731 else => {
1732 try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1733 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1734 },
1735 }
1736
1737 if (code.target_str) |triple| {
1738 try build_args.appendSlice(&[_][]const u8{ "-target", triple });
1739 try shell_out.print("-target {s} ", .{triple});
1740 }
1741 for (code.additional_options) |option| {
1742 try build_args.append(option);
1743 try shell_out.print("{s} ", .{option});
1744 }
1745
1746 if (maybe_error_match) |error_match| {
1747 const result = try ChildProcess.exec(.{
1748 .allocator = allocator,
1749 .argv = build_args.items,
1750 .env_map = &env_map,
1751 .max_output_bytes = max_doc_file_size,
1752 });
1753 switch (result.term) {
1754 .Exited => |exit_code| {
1755 if (exit_code == 0) {
1756 progress.log("", .{});
1757 print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr});
1758 dumpArgs(build_args.items);
1759 return parseError(tokenizer, code.source_token, "example build incorrectly succeeded", .{});
1760 }
1761 },
1762 else => {
1763 progress.log("", .{});
1764 print("{s}\nThe following command crashed:\n", .{result.stderr});
1765 dumpArgs(build_args.items);
1766 return parseError(tokenizer, code.source_token, "example compile crashed", .{});
1767 },
1768 }
1769 if (mem.indexOf(u8, result.stderr, error_match) == null) {
1770 progress.log("", .{});
1771 print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match });
1772 return parseError(tokenizer, code.source_token, "example did not have expected compile error message", .{});
1773 }
1774 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1775 const colored_stderr = try termColor(allocator, escaped_stderr);
1776 try shell_out.print("\n{s} ", .{colored_stderr});
1777 } else {
1778 _ = exec(allocator, &env_map, null, build_args.items) catch return parseError(tokenizer, code.source_token, "example failed to compile", .{});
1779 }
1780 try shell_out.writeAll("\n");
1781 },
1782 .lib => {
1783 const bin_basename = try std.zig.binNameAlloc(allocator, .{
1784 .root_name = code.name,
1785 .target = builtin.target,
1786 .output_mode = .Lib,
1787 });
1788
1789 var test_args = std.ArrayList([]const u8).init(allocator);
1790 defer test_args.deinit();
1791
1792 try test_args.appendSlice(&[_][]const u8{
1793 zig_exe, "build-lib",
1794 tmp_source_file_name,
1795 try std.fmt.allocPrint(allocator, "-femit-bin={s}{s}{s}", .{
1796 tmp_dir_name, fs.path.sep_str, bin_basename,
1797 }),
1798 });
1799 if (opt_zig_lib_dir) |zig_lib_dir| {
1800 try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir });
1801 }
1802 try shell_out.print("$ zig build-lib {s}.zig ", .{code.name});
1803
1804 switch (code.mode) {
1805 .Debug => {},
1806 else => {
1807 try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
1808 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
1809 },
1810 }
1811 if (code.target_str) |triple| {
1812 try test_args.appendSlice(&[_][]const u8{ "-target", triple });
1813 try shell_out.print("-target {s} ", .{triple});
1814 }
1815 if (code.link_mode) |link_mode| {
1816 switch (link_mode) {
1817 .Static => {
1818 try test_args.append("-static");
1819 try shell_out.print("-static ", .{});
1820 },
1821 .Dynamic => {
1822 try test_args.append("-dynamic");
1823 try shell_out.print("-dynamic ", .{});
1824 },
1825 }
1826 }
1827 for (code.additional_options) |option| {
1828 try test_args.append(option);
1829 try shell_out.print("{s} ", .{option});
1830 }
1831 const result = exec(allocator, &env_map, null, test_args.items) catch return parseError(tokenizer, code.source_token, "test failed", .{});
1832 const escaped_stderr = try escapeHtml(allocator, result.stderr);
1833 const escaped_stdout = try escapeHtml(allocator, result.stdout);
1834 try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
1835 },
1836 }
1837
1838 if (!code.just_check_syntax) {
1839 try printShell(out, shell_buffer.items, false);
1840 }
1841 },
1842 }
1843 }
1844}
1845
1846fn exec(
1847 allocator: Allocator,
1848 env_map: *process.EnvMap,
1849 cwd: ?[]const u8,
1850 args: []const []const u8,
1851) !ChildProcess.ExecResult {
1852 const result = try ChildProcess.exec(.{
1853 .allocator = allocator,
1854 .argv = args,
1855 .env_map = env_map,
1856 .cwd = cwd,
1857 .max_output_bytes = max_doc_file_size,
1858 });
1859 switch (result.term) {
1860 .Exited => |exit_code| {
1861 if (exit_code != 0) {
1862 print("{s}\nThe following command exited with code {}:\n", .{ result.stderr, exit_code });
1863 dumpArgs(args);
1864 return error.ChildExitError;
1865 }
1866 },
1867 else => {
1868 print("{s}\nThe following command crashed:\n", .{result.stderr});
1869 dumpArgs(args);
1870 return error.ChildCrashed;
1871 },
1872 }
1873 return result;
1874}
1875
1876fn getBuiltinCode(
1877 allocator: Allocator,
1878 env_map: *process.EnvMap,
1879 zig_exe: []const u8,
1880 opt_zig_lib_dir: ?[]const u8,
1881) ![]const u8 {
1882 if (opt_zig_lib_dir) |zig_lib_dir| {
1883 const result = try exec(allocator, env_map, null, &.{
1884 zig_exe, "build-obj", "--show-builtin", "--zig-lib-dir", zig_lib_dir,
1885 });
1886 return result.stdout;
1887 } else {
1888 const result = try exec(allocator, env_map, null, &.{
1889 zig_exe, "build-obj", "--show-builtin",
1890 });
1891 return result.stdout;
1892 }
1893}
1894
1895fn dumpArgs(args: []const []const u8) void {
1896 for (args) |arg|
1897 print("{s} ", .{arg})
1898 else
1899 print("\n", .{});
1900}
1901
1902test "term supported colors" {
1903 const test_allocator = testing.allocator;
1904
1905 {
1906 const input = "A\x1b[31;1mred\x1b[0mB";
1907 const expect = "A<span class=\"sgr-31_1m\">red</span>B";
1908
1909 const result = try termColor(test_allocator, input);
1910 defer test_allocator.free(result);
1911 try testing.expectEqualSlices(u8, expect, result);
1912 }
1913
1914 {
1915 const input = "A\x1b[32;1mgreen\x1b[0mB";
1916 const expect = "A<span class=\"sgr-32_1m\">green</span>B";
1917
1918 const result = try termColor(test_allocator, input);
1919 defer test_allocator.free(result);
1920 try testing.expectEqualSlices(u8, expect, result);
1921 }
1922
1923 {
1924 const input = "A\x1b[36;1mcyan\x1b[0mB";
1925 const expect = "A<span class=\"sgr-36_1m\">cyan</span>B";
1926
1927 const result = try termColor(test_allocator, input);
1928 defer test_allocator.free(result);
1929 try testing.expectEqualSlices(u8, expect, result);
1930 }
1931
1932 {
1933 const input = "A\x1b[1mbold\x1b[0mB";
1934 const expect = "A<span class=\"sgr-1m\">bold</span>B";
1935
1936 const result = try termColor(test_allocator, input);
1937 defer test_allocator.free(result);
1938 try testing.expectEqualSlices(u8, expect, result);
1939 }
1940
1941 {
1942 const input = "A\x1b[2mdim\x1b[0mB";
1943 const expect = "A<span class=\"sgr-2m\">dim</span>B";
1944
1945 const result = try termColor(test_allocator, input);
1946 defer test_allocator.free(result);
1947 try testing.expectEqualSlices(u8, expect, result);
1948 }
1949}
1950
1951test "term output from zig" {
1952 // Use data generated by https://github.com/perillo/zig-tty-test-data,
1953 // with zig version 0.11.0-dev.1898+36d47dd19.
1954 const test_allocator = testing.allocator;
1955
1956 {
1957 // 1.1-with-build-progress.out
1958 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";
1959 const expect = "";
1960
1961 const result = try termColor(test_allocator, input);
1962 defer test_allocator.free(result);
1963 try testing.expectEqualSlices(u8, expect, result);
1964 }
1965
1966 {
1967 // 2.1-with-reference-traces.out
1968 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";
1969 const expect =
1970 \\<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
1971 \\</span> x += 1;
1972 \\ <span class="sgr-32_1m">~~^~~~
1973 \\</span><span class="sgr-2m">referenced by:
1974 \\ main: src/2.1-with-reference-traces.zig:7:5
1975 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
1976 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces
1977 \\
1978 \\</span>
1979 ;
1980
1981 const result = try termColor(test_allocator, input);
1982 defer test_allocator.free(result);
1983 try testing.expectEqualSlices(u8, expect, result);
1984 }
1985
1986 {
1987 // 2.2-without-reference-traces.out
1988 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";
1989 const expect =
1990 \\<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
1991 \\</span> else => @compileError("invalid type given to fixedBufferStream"),
1992 \\ <span class="sgr-32_1m">^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1993 \\</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
1994 \\</span>pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {
1995 \\; <span class="sgr-32_1m">~~~~~^~~~~~~~~~~~~~~~~
1996 \\</span>
1997 ;
1998
1999 const result = try termColor(test_allocator, input);
2000 defer test_allocator.free(result);
2001 try testing.expectEqualSlices(u8, expect, result);
2002 }
2003
2004 {
2005 // 2.3-with-notes.out
2006 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";
2007 const expect =
2008 \\<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'
2009 \\</span> bar(w);
2010 \\ <span class="sgr-32_1m">^
2011 \\</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'
2012 \\</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
2013 \\</span>const Wat = opaque {};
2014 \\ <span class="sgr-32_1m">^~~~~~~~~
2015 \\</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
2016 \\</span>const Derp = opaque {};
2017 \\ <span class="sgr-32_1m">^~~~~~~~~
2018 \\</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
2019 \\</span>extern fn bar(d: *Derp) void;
2020 \\ <span class="sgr-32_1m">^~~~~
2021 \\</span><span class="sgr-2m">referenced by:
2022 \\ main: src/2.3-with-notes.zig:10:5
2023 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17
2024 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces
2025 \\
2026 \\</span>
2027 ;
2028
2029 const result = try termColor(test_allocator, input);
2030 defer test_allocator.free(result);
2031 try testing.expectEqualSlices(u8, expect, result);
2032 }
2033
2034 {
2035 // 3.1-with-error-return-traces.out
2036
2037 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";
2038 const expect =
2039 \\error: Error
2040 \\<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>
2041 \\ return error.Error;
2042 \\ <span class="sgr-32_1m">^</span>
2043 \\<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>
2044 \\ try callee();
2045 \\ <span class="sgr-32_1m">^</span>
2046 \\<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>
2047 \\ try caller();
2048 \\ <span class="sgr-32_1m">^</span>
2049 \\
2050 ;
2051
2052 const result = try termColor(test_allocator, input);
2053 defer test_allocator.free(result);
2054 try testing.expectEqualSlices(u8, expect, result);
2055 }
2056
2057 {
2058 // 3.2-with-stack-trace.out
2059 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";
2060 const expect =
2061 \\<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>
2062 \\ while (it.next()) |return_address| {
2063 \\ <span class="sgr-32_1m">^</span>
2064 \\<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>
2065 \\ writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {
2066 \\ <span class="sgr-32_1m">^</span>
2067 \\<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>
2068 \\ std.debug.dumpCurrentStackTrace(null);
2069 \\ <span class="sgr-32_1m">^</span>
2070 \\<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>
2071 \\ foo();
2072 \\ <span class="sgr-32_1m">^</span>
2073 \\<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>
2074 \\ root.main();
2075 \\ <span class="sgr-32_1m">^</span>
2076 \\<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>
2077 \\ @call(.never_inline, posixCallMainAndExit, .{});
2078 \\ <span class="sgr-32_1m">^</span>
2079 \\
2080 ;
2081
2082 const result = try termColor(test_allocator, input);
2083 defer test_allocator.free(result);
2084 try testing.expectEqualSlices(u8, expect, result);
2085 }
2086}
2087
2088test "printShell" {
2089 const test_allocator = std.testing.allocator;
2090
2091 {
2092 const shell_out =
2093 \\$ zig build test.zig
2094 ;
2095 const expected =
2096 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2097 \\</samp></pre></figure>
2098 ;
2099
2100 var buffer = std.ArrayList(u8).init(test_allocator);
2101 defer buffer.deinit();
2102
2103 try printShell(buffer.writer(), shell_out, false);
2104 try testing.expectEqualSlices(u8, expected, buffer.items);
2105 }
2106 {
2107 const shell_out =
2108 \\$ zig build test.zig
2109 \\build output
2110 ;
2111 const expected =
2112 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2113 \\build output
2114 \\</samp></pre></figure>
2115 ;
2116
2117 var buffer = std.ArrayList(u8).init(test_allocator);
2118 defer buffer.deinit();
2119
2120 try printShell(buffer.writer(), shell_out, false);
2121 try testing.expectEqualSlices(u8, expected, buffer.items);
2122 }
2123 {
2124 const shell_out =
2125 \\$ zig build test.zig
2126 \\build output
2127 \\$ ./test
2128 ;
2129 const expected =
2130 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2131 \\build output
2132 \\$ <kbd>./test</kbd>
2133 \\</samp></pre></figure>
2134 ;
2135
2136 var buffer = std.ArrayList(u8).init(test_allocator);
2137 defer buffer.deinit();
2138
2139 try printShell(buffer.writer(), shell_out, false);
2140 try testing.expectEqualSlices(u8, expected, buffer.items);
2141 }
2142 {
2143 const shell_out =
2144 \\$ zig build test.zig
2145 \\
2146 \\$ ./test
2147 \\output
2148 ;
2149 const expected =
2150 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2151 \\
2152 \\$ <kbd>./test</kbd>
2153 \\output
2154 \\</samp></pre></figure>
2155 ;
2156
2157 var buffer = std.ArrayList(u8).init(test_allocator);
2158 defer buffer.deinit();
2159
2160 try printShell(buffer.writer(), shell_out, false);
2161 try testing.expectEqualSlices(u8, expected, buffer.items);
2162 }
2163 {
2164 const shell_out =
2165 \\$ zig build test.zig
2166 \\$ ./test
2167 \\output
2168 ;
2169 const expected =
2170 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2171 \\$ <kbd>./test</kbd>
2172 \\output
2173 \\</samp></pre></figure>
2174 ;
2175
2176 var buffer = std.ArrayList(u8).init(test_allocator);
2177 defer buffer.deinit();
2178
2179 try printShell(buffer.writer(), shell_out, false);
2180 try testing.expectEqualSlices(u8, expected, buffer.items);
2181 }
2182 {
2183 const shell_out =
2184 \\$ zig build test.zig \
2185 \\ --build-option
2186 \\build output
2187 \\$ ./test
2188 \\output
2189 ;
2190 const expected =
2191 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
2192 \\ --build-option</kbd>
2193 \\build output
2194 \\$ <kbd>./test</kbd>
2195 \\output
2196 \\</samp></pre></figure>
2197 ;
2198
2199 var buffer = std.ArrayList(u8).init(test_allocator);
2200 defer buffer.deinit();
2201
2202 try printShell(buffer.writer(), shell_out, false);
2203 try testing.expectEqualSlices(u8, expected, buffer.items);
2204 }
2205 {
2206 // intentional space after "--build-option1 \"
2207 const shell_out =
2208 \\$ zig build test.zig \
2209 \\ --build-option1 \
2210 \\ --build-option2
2211 \\$ ./test
2212 ;
2213 const expected =
2214 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
2215 \\ --build-option1 \
2216 \\ --build-option2</kbd>
2217 \\$ <kbd>./test</kbd>
2218 \\</samp></pre></figure>
2219 ;
2220
2221 var buffer = std.ArrayList(u8).init(test_allocator);
2222 defer buffer.deinit();
2223
2224 try printShell(buffer.writer(), shell_out, false);
2225 try testing.expectEqualSlices(u8, expected, buffer.items);
2226 }
2227 {
2228 const shell_out =
2229 \\$ zig build test.zig \
2230 \\$ ./test
2231 ;
2232 const expected =
2233 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig \
2234 \\$ ./test</kbd>
2235 \\</samp></pre></figure>
2236 ;
2237
2238 var buffer = std.ArrayList(u8).init(test_allocator);
2239 defer buffer.deinit();
2240
2241 try printShell(buffer.writer(), shell_out, false);
2242 try testing.expectEqualSlices(u8, expected, buffer.items);
2243 }
2244 {
2245 const shell_out =
2246 \\$ zig build test.zig
2247 \\$ ./test
2248 \\$1
2249 ;
2250 const expected =
2251 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$ <kbd>zig build test.zig</kbd>
2252 \\$ <kbd>./test</kbd>
2253 \\$1
2254 \\</samp></pre></figure>
2255 ;
2256
2257 var buffer = std.ArrayList(u8).init(test_allocator);
2258 defer buffer.deinit();
2259
2260 try printShell(buffer.writer(), shell_out, false);
2261 try testing.expectEqualSlices(u8, expected, buffer.items);
2262 }
2263 {
2264 const shell_out =
2265 \\$zig build test.zig
2266 ;
2267 const expected =
2268 \\<figure><figcaption class="shell-cap">Shell</figcaption><pre><samp>$zig build test.zig
2269 \\</samp></pre></figure>
2270 ;
2271
2272 var buffer = std.ArrayList(u8).init(test_allocator);
2273 defer buffer.deinit();
2274
2275 try printShell(buffer.writer(), shell_out, false);
2276 try testing.expectEqualSlices(u8, expected, buffer.items);
2277 }
2278}