1//! Language reference compiler. Converts markdown into HTML, using
2//! the Tokenizer to highlight code, and the compiler and QJS to
3//! show code results. Only ever built in Debug mode. We are not
4//! smart about memory. Initially written at 3am on a Thursday. Enjoy
5pub const std = @import("std");
6pub const core = @import("core");
7const NaiveHtmlMinifier = @import("NaiveHtmlMinifier.zig");
8
9const mem = std.mem;
10const Allocator = mem.Allocator;
11const ArrayList = std.ArrayList;
12const ArrayListUnmanaged = std.ArrayListUnmanaged;
13const assert = std.debug.assert;
14
15comptime {
16 assert(@import("builtin").mode == .Debug);
17}
18
19const minify_html = true;
20const minify_css = false;
21
22pub fn main() !void {
23 comptime std.debug.assert(@import("builtin").mode == .Debug);
24 const gpa = std.heap.c_allocator;
25
26 const progress = std.Progress.start(.{
27 .root_name = "Build manual",
28 .estimated_total_items = 0,
29 });
30
31 var args = try std.process.argsWithAllocator(gpa);
32 _ = args.next().?;
33
34 const compiler_path = args.next().?;
35 const runtime_path = args.next().?;
36 const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
37 const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
38 const style_src_raw = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
39 const out_path = args.next().?;
40
41 assert(args.next() == null);
42
43 var contents = ArrayList(u8).init(gpa);
44 var header: HeaderWriter = .{};
45
46 try processMarkdown(
47 gpa,
48 progress,
49 markdown_src,
50 compiler_path,
51 runtime_path,
52 &header,
53 contents.writer(),
54 );
55
56 try header.end(gpa);
57
58 const minified_template = if (minify_html)
59 try NaiveHtmlMinifier.minify(template_src, gpa)
60 else
61 template_src;
62
63 const style_src = try std.fmt.allocPrint(gpa, "<style>{s}</style>", .{style_src_raw});
64 const minified_style = if (minify_css)
65 try NaiveHtmlMinifier.minify(style_src, gpa)
66 else
67 style_src;
68
69 const applied_template = try replaceInTemplate(gpa, minified_template, .{
70 .contents = contents.items,
71 .navigation = header.data.items,
72 .nav_h1 = "Ix Language",
73 .repository = "https://github.com/paperdave/ix",
74 .style = minified_style,
75 });
76
77 const out_file = try std.fs.createFileAbsolute(out_path, .{});
78 defer out_file.close();
79 try out_file.writeAll(applied_template);
80
81 progress.end();
82
83 std.debug.lockStdErr();
84 std.debug.unlockStdErr();
85
86 std.process.exit(if (header.had_signaled) 1 else 0);
87}
88
89const BlockType = enum {
90 para,
91 h1,
92 h2,
93 h3,
94 h4,
95 h5,
96 h6,
97 code_run,
98 code_build,
99 code,
100 code_generic,
101 ul,
102 table,
103 blockquote,
104};
105
106const InlineType = enum {
107 bold,
108 code,
109 link,
110 comment,
111};
112
113const MarkdownTokenizer = struct {
114 /// Remaining text
115 str: []const u8,
116
117 fn skipWhitespace(t: *MarkdownTokenizer) void {
118 t.str = mem.trimLeft(u8, t.str, " \r\n");
119 }
120
121 fn advance(t: *MarkdownTokenizer, n: usize) []const u8 {
122 defer {
123 t.str = t.str[n..];
124 }
125 return t.str[0..n];
126 }
127
128 fn advanceLine(t: *MarkdownTokenizer) []const u8 {
129 return mem.trim(u8, t.advanceLineRaw(), " \r\n");
130 }
131
132 fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 {
133 const n = mem.indexOfScalar(u8, t.str, '\n');
134 const line = t.advance(if (n) |x| x + 1 else t.str.len);
135 return line;
136 }
137
138 fn blockType(t: *MarkdownTokenizer) BlockType {
139 switch (t.str[0]) {
140 '#' => {
141 defer {
142 while (t.str[0] == '#')
143 _ = t.advance(1);
144 }
145
146 if (t.str[1] != '#') return .h1;
147 if (t.str[2] != '#') return .h2;
148 if (t.str[3] != '#') return .h3;
149 if (t.str[4] != '#') return .h4;
150 if (t.str[5] != '#') return .h5;
151 if (t.str[6] != '#') return .h6;
152 @panic("headers nested too deep");
153 },
154 '>' => {
155 _ = t.advance(1);
156 return .blockquote;
157 },
158 '|' => {
159 return .table;
160 },
161 '`' => {
162 if (mem.startsWith(u8, t.str, "```")) {
163 defer _ = t.advanceLine();
164 if (mem.startsWith(u8, t.str, "```lang:run"))
165 return .code_run;
166 if (mem.startsWith(u8, t.str, "```lang:build"))
167 return .code_build;
168 if (mem.startsWith(u8, t.str, "```lang"))
169 return .code;
170 return .code_generic;
171 }
172 return .para;
173 },
174 '-' => {
175 return .ul;
176 },
177 else => {
178 return .para;
179 },
180 }
181 }
182};
183
184const HeaderWriter = struct {
185 data: std.ArrayListUnmanaged(u8) = .{},
186 level: u8 = 0,
187 h1: ?[]const u8 = null,
188
189 numbers: [5]u32 = .{ 0, 0, 0, 0, 0 },
190
191 has_closed_li: bool = true,
192
193 had_signaled: bool = false,
194
195 fn add(header: *HeaderWriter, alloc: Allocator, name: []const u8, id: []const u8, level: u8) !void {
196 const w = header.data.writer(alloc);
197
198 if (header.level == level) {
199 try w.writeAll("</li>");
200 }
201
202 while (header.level > level) {
203 header.level -= 1;
204 header.numbers[header.level] = 0;
205 try w.writeAll("</ul></li>");
206 }
207
208 while (header.level < level) {
209 if (header.level == 0) {
210 try w.writeAll("<ul id=\"toc\">");
211 } else {
212 try w.writeAll("<ul>");
213 }
214 header.level += 1;
215 }
216
217 header.numbers[level - 1] += 1;
218
219 try w.print("<li><a class=\"header-link\" href=\"#{s}\"><span class=\"number\">", .{id});
220
221 for (0..level) |i| {
222 if (i != 0) try w.print(".", .{});
223 try w.print("{d}", .{header.numbers[i]});
224 }
225
226 try w.print("</span>. {s}</a>", .{name});
227 }
228
229 fn end(header: *HeaderWriter, alloc: Allocator) !void {
230 const w = header.data.writer(alloc);
231 while (header.level > 1) {
232 header.level -= 1;
233 try w.writeAll("</ul></li>");
234 }
235 if (header.level == 1) {
236 header.level -= 1;
237 try w.writeAll("</ul>");
238 }
239 }
240};
241
242const CodeExample = struct {
243 filename: []const u8,
244 source: union(enum) { tokenized: []const u8, raw: []const u8 },
245 terminal: ?[]const u8 = null,
246 javascript: ?[]const u8 = null,
247
248 const copy_button = NaiveHtmlMinifier.minifyComptime(
249 \\<button class="copy-button">
250 \\ <svg
251 \\ xmlns="http://www.w3.org/2000/svg"
252 \\ height="20px"
253 \\ viewBox="0 -960 960 960"
254 \\ width="20px"
255 \\ fill="currentColor"
256 \\ >
257 \\ <path
258 \\ d="M355-240q-30.94 0-52.97-22.03Q280-284.06 280-315v-480q0-30.94 22.03-52.97Q324.06-870 355-870h360q30.94 0 52.97 22.03Q790-825.94 790-795v480q0 30.94-22.03 52.97Q745.94-240 715-240H355Zm0-75h360v-480H355v480ZM205-90q-30.94 0-52.97-22.03Q130-134.06 130-165v-555h75v555h435v75H205Zm150-225v-480 480Z"
259 \\ />
260 \\ </svg>
261 \\ <svg
262 \\ xmlns="http://www.w3.org/2000/svg"
263 \\ height="20px"
264 \\ viewBox="0 -960 960 960"
265 \\ width="20px"
266 \\ fill="currentColor"
267 \\ class="hidden"
268 \\ >
269 \\ <path
270 \\ d="M383-246 162-467l53.5-53.5L383-353l361.5-361.5L798-661 383-246Z"
271 \\ />
272 \\ </svg>
273 \\</button>
274 );
275
276 var next_tab_id: usize = 0;
277
278 pub fn write(code: CodeExample, w: std.io.AnyWriter) !void {
279 try w.writeAll("<figure class=\"file\">");
280 try w.print("<figcaption>{}</figcaption>", .{fmtHtmlEscape(code.filename, false)});
281 try w.writeAll("<pre>");
282 try w.writeAll(copy_button);
283 try w.writeAll("<code>");
284 switch (code.source) {
285 .tokenized => |str| try w.writeAll(str),
286 .raw => |str| try writeHtmlEscapedSlice(str, w, false),
287 }
288 try w.writeAll("</code></pre>");
289
290 const tab_id = next_tab_id;
291 next_tab_id += 1;
292
293 if (code.terminal) |terminal| {
294 try w.writeAll("<section class=\"tabs\">");
295 try w.print("<input class=\"tab-output\"name=\"tab{0d}\"type=\"radio\"id=\"output{0d}\"value=\"output\"checked=true/>", .{tab_id});
296 try w.print("<input class=\"tab-js\"name=\"tab{0d}\"type=\"radio\"id=\"js{0d}\"value=\"js\"/>", .{tab_id});
297 if (code.javascript != null) {
298 try w.writeAll("<div role=\"tablist\">");
299 try w.print("<label class=\"button-output\"id=\"output-tab\"role=\"tab\"for=\"output{d}\">Output</label>", .{tab_id});
300 try w.print("<label class=\"button-js\"id=\"js-tab\"role=\"tab\"for=\"js{d}\">Compiled JS</label>", .{tab_id});
301 try w.writeAll("</div>");
302 } else {
303 // ???
304 }
305
306 try w.writeAll("<div class=\"bottom output\" aria-labelledby=\"output-tab\">");
307 try w.writeAll("<pre class=\"terminal\"><code>");
308 try writeAnsi(terminal, w);
309 try w.writeAll("</code></pre>");
310 try w.writeAll("</div>");
311
312 if (code.javascript) |js| {
313 try w.writeAll("<div class=\"bottom js\" aria-labelledby=\"js-tab\">");
314 try w.print("<pre><code>{}</code></pre>", .{fmtHtmlEscape(js, false)});
315 try w.writeAll("</div>");
316 }
317
318 try w.writeAll("</section>");
319 } else {
320 assert(code.javascript == null);
321 }
322 try w.writeAll("</figure>");
323 }
324};
325
326fn processMarkdown(
327 gpa: Allocator,
328 progress: std.Progress.Node,
329 src: []const u8,
330 compiler_path: []const u8,
331 runtime_path: []const u8,
332 header: *HeaderWriter,
333 writer: anytype,
334) !void {
335 var t = MarkdownTokenizer{ .str = src };
336
337 t.skipWhitespace();
338 while (t.str.len > 0) : (t.skipWhitespace()) {
339 switch (t.blockType()) {
340 .para => {
341 try writer.writeAll("<p>");
342
343 var para = ArrayList(u8).init(gpa);
344 try para.appendSlice(t.advanceLine());
345
346 var next_line = t.advanceLine();
347 while (t.str.len > 0 and next_line.len > 0) {
348 try para.append(' ');
349 try para.appendSlice(next_line);
350 next_line = t.advanceLine();
351 }
352
353 try processInlineMarkdown(gpa, para.items, writer);
354
355 try writer.writeAll("</p>");
356 },
357 .h1,
358 .h2,
359 .h3,
360 .h4,
361 .h5,
362 .h6,
363 => |tag| {
364 const html_tag = @tagName(tag);
365 const name = t.advanceLine();
366 const id = try nameToHeaderId(gpa, name);
367
368 if (tag != .h1) {
369 try writer.print("<{0s} id=\"{2s}\"><a class=\"heading\" href=\"#{2s}\">{1s}</a></{0s}>", .{
370 html_tag,
371 name,
372 id,
373 });
374
375 if (tag != .h1)
376 try header.add(gpa, name, id, switch (tag) {
377 .h2 => 1,
378 .h3 => 2,
379 .h4 => 3,
380 .h5 => 4,
381 .h6 => 5,
382 else => unreachable,
383 });
384 } else {
385 assert(header.h1 == null);
386
387 header.h1 = try std.fmt.allocPrint(
388 gpa,
389 "<a href=\"#top\">{s}</a>",
390 .{name},
391 );
392
393 try writer.print("<{0s}><a class=\"heading\" href=\"#top\">{1s}</a></{0s}>", .{
394 html_tag,
395 name,
396 });
397 }
398 },
399 .code_run,
400 .code_build,
401 .code,
402 .code_generic,
403 => |tag| {
404 var code = ArrayList(u8).init(gpa);
405
406 var next_line = t.advanceLineRaw();
407 while (t.str.len > 0 and !mem.startsWith(u8, next_line, "```")) {
408 try code.appendSlice(next_line);
409 next_line = t.advanceLineRaw();
410 }
411
412 var example: CodeExample = .{
413 .filename = "example.ix",
414 .source = .{ .raw = code.items },
415 };
416
417 if (tag != .code_generic) {
418 var tmp = ArrayList(u8).init(gpa);
419 try writeTokenizedCode(gpa, code.items, tmp.writer());
420 example.source = .{ .tokenized = tmp.items };
421 }
422
423 try code.append('\n');
424
425 if (tag == .code_run or tag == .code_build) {
426 var prog = progress.start("Code Snippet", if (tag == .code_run) 2 else 1);
427 defer prog.end();
428
429 var prog_step = prog.start("Compile", 1);
430 var build = std.process.Child.init(&.{ compiler_path, "build", "/dev/stdin" }, gpa);
431 build.stdin_behavior = .Pipe;
432 build.stdout_behavior = .Pipe;
433 build.stderr_behavior = .Pipe;
434 var env_map = try std.process.getEnvMap(gpa);
435 defer env_map.deinit();
436 try env_map.put("YES_COLOR", "1");
437 build.env_map = &env_map;
438 try build.spawn();
439 try build.stdin.?.writeAll(code.items);
440 var out = ArrayList(u8).init(gpa);
441 var err = ArrayList(u8).init(gpa);
442
443 try build.collectOutput(&out, &err, std.math.maxInt(usize));
444 const result = try build.wait();
445 if (result == .Signal) {
446 std.debug.print("{s}\n", .{err.items});
447 header.had_signaled = true;
448 }
449 if (result != .Exited or result.Exited != 0) {
450 try err.writer().print("[Process {s} with {d}]", .{
451 switch (result) {
452 .Exited => "exited",
453 .Stopped => "stopped",
454 .Signal => "signaled",
455 .Unknown => "hit unknown state",
456 },
457 switch (result) {
458 inline else => |d| @as(u32, d),
459 },
460 });
461 }
462 example.terminal = try mem.concat(gpa, u8, &.{
463 "$ ",
464 switch (tag) {
465 .code_run => "ix run",
466 .code_build => "ix build",
467 else => unreachable,
468 },
469 " ",
470 example.filename,
471 "\n",
472 mem.trim(u8, err.items, " \n\r"),
473 });
474 const trimmed_out = mem.trim(u8, out.items, " \n\r");
475 if (trimmed_out.len > 0) {
476 example.javascript = try std.mem.concat(gpa, u8, &.{
477 "// Compiled JavaScript Code",
478 trimmed_out[mem.indexOfScalar(u8, trimmed_out, '\n').?..],
479 });
480 }
481
482 prog.completeOne();
483 prog_step.end();
484
485 if (tag == .code_run and runtime_path.len > 0 and trimmed_out.len > 0) {
486 const run_step = prog.start("Execute", 0);
487 defer run_step.end();
488
489 var temp_dir = std.testing.tmpDir(.{});
490 defer temp_dir.cleanup();
491
492 const input_sub_path = "result.js";
493 try temp_dir.dir.writeFile(.{
494 .sub_path = input_sub_path,
495 .data = example.javascript.?,
496 });
497
498 var env_map_run = try std.process.getEnvMap(gpa);
499 defer env_map_run.deinit();
500 try env_map_run.put("NO_COLOR", "1");
501 try env_map_run.put("TERM", "dumb");
502
503 var run_out = ArrayList(u8).init(gpa);
504 var run_err = ArrayList(u8).init(gpa);
505
506 const run_sub_command = std.mem.indexOf(u8, runtime_path, "deno") != null or
507 std.mem.indexOf(u8, runtime_path, "porf") != null;
508
509 const cmd: []const []const u8 = if (run_sub_command)
510 &.{ runtime_path, "run", input_sub_path }
511 else
512 &.{ runtime_path, input_sub_path };
513
514 var run = std.process.Child.init(cmd, gpa);
515
516 run.cwd_dir = temp_dir.dir;
517 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
518 if (@import("builtin").os.tag == .windows)
519 run.cwd = try std.os.getFdPath(temp_dir.dir.fd, &path_buf);
520
521 run.stdin_behavior = .Ignore;
522 run.stdout_behavior = .Pipe;
523 run.stderr_behavior = .Pipe;
524
525 run.env_map = &env_map_run;
526 try run.spawn();
527
528 try run.collectOutput(&run_out, &run_err, std.math.maxInt(usize));
529 const run_result = run.wait() catch |e| {
530 if (e == error.FileNotFound) {
531 std.debug.panic(
532 \\Missing JavaScript evaluator: {s}
533 \\Specify another one using -Djs_runtime=... or set to "" to skip evaluation.
534 \\
535 , .{runtime_path});
536 }
537 return e;
538 };
539 if (run_result == .Signal) {
540 header.had_signaled = true;
541 }
542 if (run_result != .Exited or run_result.Exited != 0) {
543 try run_err.writer().print("[Process {s} with {d}]", .{
544 switch (run_result) {
545 .Exited => "exited",
546 .Stopped => "stopped",
547 .Signal => "signaled",
548 .Unknown => "hit unknown state",
549 },
550 switch (run_result) {
551 inline else => |d| @as(u32, d),
552 },
553 });
554 }
555
556 example.terminal = try std.mem.concat(gpa, u8, &.{
557 example.terminal.?,
558 run_out.items,
559 run_err.items,
560 });
561 }
562 }
563
564 try example.write(writer.any());
565 },
566 .ul => {
567 try writer.writeAll("<ul>");
568 while (t.str.len > 0 and (t.str[0] == '-')) {
569 _ = t.advance(1);
570 const line = t.advanceLine();
571 try writer.writeAll("<li>");
572 try processInlineMarkdown(gpa, line, writer);
573 while (t.str.len > 0 and (t.str[0] != '-' and t.str[0] != '\n')) {
574 const next_line = t.advanceLine();
575 try writer.writeAll(" ");
576 try processInlineMarkdown(gpa, next_line, writer);
577 }
578 try writer.writeAll("</li>");
579 }
580 try writer.writeAll("</ul>");
581 },
582 .table => {
583 try writer.writeAll("<table><tbody>");
584 {
585 try writer.writeAll("<tr>");
586 var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
587 while (it.next()) |tok| {
588 try writer.writeAll("<th>");
589 try processInlineMarkdown(gpa, tok, writer);
590 try writer.writeAll("</th>");
591 }
592 try writer.writeAll("</tr>");
593 }
594 _ = t.advanceLine();
595 t.skipWhitespace();
596 while (t.str.len > 0 and t.str[0] == '|') {
597 try writer.writeAll("<tr>");
598 var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
599 while (it.next()) |tok| {
600 try writer.writeAll("<td>");
601 try processInlineMarkdown(gpa, tok, writer);
602 try writer.writeAll("</td>");
603 }
604 try writer.writeAll("</tr>");
605 }
606 try writer.writeAll("</tbody></table>");
607 },
608 .blockquote => {
609 try writer.writeAll("<blockquote>");
610
611 var para = ArrayList(u8).init(gpa);
612 try para.appendSlice(t.advanceLine());
613
614 var next_line = t.advanceLine();
615 while (t.str.len > 0 and next_line.len > 0 and next_line[0] == '>') {
616 try para.append(' ');
617 try para.appendSlice(next_line[1..]);
618 next_line = t.advanceLine();
619 }
620
621 try processInlineMarkdown(gpa, para.items, writer);
622
623 try writer.writeAll("</blockquote>");
624 },
625 }
626 }
627}
628
629pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
630 var pos: usize = 0;
631 while (true) : (pos += 1) {
632 pos = mem.indexOfAnyPos(u8, src, pos, "*`[<") orelse return null;
633
634 switch (src[pos]) {
635 '*' => switch (src[pos + 1]) {
636 '*' => return .{ .bold, pos },
637 else => continue,
638 },
639 '`' => return .{ .code, pos },
640 '[' => return .{ .link, pos },
641 '<' => if (src[pos + 1] == '!' and src[pos + 2] == '-' and src[pos + 3] == '-')
642 return .{ .comment, pos }
643 else
644 continue,
645 else => unreachable,
646 }
647 }
648}
649
650fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void {
651 var bold = false;
652 var remain = src;
653
654 while (nextInlineToken(remain)) |tok| {
655 try writer.writeAll(remain[0..tok[1]]);
656 remain = remain[tok[1]..];
657 switch (tok[0]) {
658 .bold => {
659 bold = !bold;
660 try writer.writeAll(switch (bold) {
661 true => "<strong>",
662 false => "</strong>",
663 });
664 remain = remain[2..];
665 },
666 .code => {
667 const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
668 const code = remain[1..end];
669 try writer.writeAll("<code>");
670 try writeTokenizedCode(gpa, code, writer);
671 try writer.writeAll("</code>");
672 remain = remain[end + 1 ..];
673 },
674 .link => {
675 const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block");
676 assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented
677
678 const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block");
679
680 const label = remain[1..label_end];
681 const href = remain[label_end + 2 .. href_end];
682
683 try writer.writeAll("<a href=\"");
684 try writeHtmlEscapedSlice(href, writer, true);
685 if (!mem.startsWith(u8, href, "#")) {
686 try writer.writeAll("\" class=\"external\" target=\"_blank\" ref=\"noopener noreferrer");
687 }
688 try writer.writeAll("\">");
689 try processInlineMarkdown(gpa, label, writer);
690 try writer.writeAll("</a>");
691 remain = remain[href_end + 1 ..];
692 },
693 .comment => {
694 const end = mem.indexOfPos(u8, remain, 4, "-->") orelse @panic("missing comment end");
695 remain = remain[1 .. end + "-->".len];
696 },
697 }
698 }
699 try writer.writeAll(remain);
700
701 if (bold) {
702 std.debug.panic("Unterminated Bold in '{s}'", .{src});
703 }
704}
705
706// TODO: Change this to use Tree-sitter, which would also make it very simple
707// to highlight JavaScript code in addition to Ix source code
708fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void {
709 const code_z = try gpa.dupeZ(u8, code);
710 defer gpa.free(code_z);
711
712 var t = core.Tokenizer{ .code = code_z };
713
714 t.skipWhitespace();
715 var last: usize = 0;
716 while (true) {
717 t.skipWhitespace();
718 const tok = t.next() orelse break;
719 try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false);
720 try writer.print("<span class='tok-{s}{s}'>{s}</span>", .{
721 @tagName(tok.tag),
722 if (tok.tag.isKeyword()) " kw" else "",
723 t.code[tok.offset..t.pos],
724 });
725 last = t.pos;
726 }
727 try writeHtmlEscapedSlice(t.code[last..], writer, false);
728}
729
730fn writeAnsi(code: []const u8, writer: anytype) !void {
731 var remain = code;
732 var count: usize = 0;
733 while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
734 try writeHtmlEscapedSlice(remain[0..esc_i], writer, false);
735 switch (remain[esc_i + 1]) {
736 '[' => switch (remain[esc_i + 2]) {
737 '0'...'9' => {
738 const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
739 const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
740 switch (int) {
741 else => {
742 try writer.print("<span class='ansi-{d}'>", .{int});
743 count += 1;
744 },
745 0 => {
746 for (0..count) |_| {
747 try writer.writeAll("</span>");
748 }
749 count = 0;
750 },
751 }
752 remain = remain[end + 1 ..];
753 },
754 else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
755 },
756 else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
757 }
758 }
759 try writeHtmlEscapedSlice(remain, writer, false);
760}
761
762fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 {
763 var out = try ArrayList(u8).initCapacity(alloc, input.len);
764
765 var is_split = false;
766 for (input) |char| {
767 switch (char) {
768 'a'...'z', 'A'...'Z', '0'...'9' => {
769 if (is_split) {
770 out.appendAssumeCapacity('-');
771 is_split = false;
772 }
773 out.appendAssumeCapacity(std.ascii.toLower(char));
774 },
775 else => {
776 is_split = true;
777 },
778 }
779 }
780
781 return try out.toOwnedSlice();
782}
783
784fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 {
785 const Replacements = @TypeOf(replacements);
786 const Map = std.StringArrayHashMapUnmanaged([]const u8);
787
788 var map: Map = .{};
789 inline for (@typeInfo(Replacements).@"struct".fields) |field| {
790 try map.put(alloc, field.name, @field(replacements, field.name));
791 }
792
793 var out: ArrayListUnmanaged(u8) = .{};
794 const w = out.writer(alloc);
795
796 var i: usize = 0;
797 while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| {
798 const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break;
799 const inner = template[open_at + 1 .. end];
800
801 if (map.get(inner)) |replacement| {
802 // write previous
803 try w.writeAll(template[i..open_at]);
804 // write template
805 try w.writeAll(replacement);
806 // move one past the ending '}'
807 i = end + 1;
808 } else {
809 // write up to and including the {
810 try w.writeAll(template[i .. open_at + 1]);
811 // move one past it
812 i = open_at + 1;
813 }
814 }
815
816 // write ending
817 try w.writeAll(template[i..]);
818
819 return out.items;
820}
821
822fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void {
823 var i: usize = 0;
824 while (mem.indexOfAnyPos(
825 u8,
826 text,
827 i,
828 if (escape_quotes)
829 &.{ '&', '<', '>', '"' }
830 else
831 &.{ '&', '<', '>' },
832 )) |at| {
833 const replace = switch (text[at]) {
834 '&' => "&amp;",
835 '<' => "&lt;",
836 '>' => "&gt;",
837 '"' => "&quot;",
838 else => unreachable,
839 };
840 try writer.writeAll(text[i..at]);
841 try writer.writeAll(replace);
842 i = at + 1;
843 }
844 try writer.writeAll(text[i..]);
845}
846
847fn fmtHtmlEscape(text: []const u8, escape_quotes: bool) std.fmt.Formatter(fmtHtmlEscapeImpl) {
848 return .{ .data = .{ .text = text, .escape_quotes = escape_quotes } };
849}
850
851fn fmtHtmlEscapeImpl(data: struct { text: []const u8, escape_quotes: bool }, _: []const u8, _: std.fmt.FormatOptions, w: std.io.AnyWriter) !void {
852 try writeHtmlEscapedSlice(data.text, w, data.escape_quotes);
853}