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