hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

html.ha (30206B)


      1 // License: GPL-3.0
      2 // (c) 2021-2022 Alexey Yerin <yyp@disroot.org>
      3 // (c) 2022 Byron Torres <b@torresjrjr.com>
      4 // (c) 2021-2022 Drew DeVault <sir@cmpwn.com>
      5 // (c) 2021 Ember Sawady <ecs@d2evs.net>
      6 // (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz>
      7 // (c) 2022 Umar Getagazov <umar@handlerug.me>
      8 
      9 // Note: ast::ident should never have to be escaped
     10 use encoding::utf8;
     11 use fmt;
     12 use hare::ast;
     13 use hare::ast::{variadism};
     14 use hare::lex;
     15 use hare::module;
     16 use hare::unparse;
     17 use io;
     18 use memio;
     19 use net::ip;
     20 use net::uri;
     21 use os;
     22 use path;
     23 use strings;
     24 
     25 // Prints a string to an output handle, escaping any of HTML's reserved
     26 // characters.
     27 fn html_escape(out: io::handle, in: str) (size | io::error) = {
     28 	let z = 0z;
     29 	let iter = strings::iter(in);
     30 	for (true) {
     31 		match (strings::next(&iter)) {
     32 		case void => break;
     33 		case let rn: rune =>
     34 			z += fmt::fprint(out, switch (rn) {
     35 			case '&' =>
     36 				yield "&amp;";
     37 			case '<' =>
     38 				yield "&lt;";
     39 			case '>' =>
     40 				yield "&gt;";
     41 			case '"' =>
     42 				yield "&quot;";
     43 			case '\'' =>
     44 				yield "&apos;";
     45 			case =>
     46 				yield strings::fromutf8(utf8::encoderune(rn))!;
     47 			})?;
     48 		};
     49 	};
     50 	return z;
     51 };
     52 
     53 @test fn html_escape() void = {
     54 	let sink = memio::dynamic();
     55 	defer io::close(&sink)!;
     56 	html_escape(&sink, "hello world!")!;
     57 	assert(memio::string(&sink)! == "hello world!");
     58 
     59 	let sink = memio::dynamic();
     60 	defer io::close(&sink)!;
     61 	html_escape(&sink, "\"hello world!\"")!;
     62 	assert(memio::string(&sink)! == "&quot;hello world!&quot;");
     63 
     64 	let sink = memio::dynamic();
     65 	defer io::close(&sink)!;
     66 	html_escape(&sink, "<hello & 'world'!>")!;
     67 	assert(memio::string(&sink)! == "&lt;hello &amp; &apos;world&apos;!&gt;");
     68 };
     69 
     70 // Formats output as HTML
     71 fn emit_html(ctx: *context) (void | error) = {
     72 	const decls = ctx.summary;
     73 	const ident = unparse::identstr(ctx.ident);
     74 	defer free(ident);
     75 
     76 	if (ctx.template) head(ctx.ident)?;
     77 
     78 	if (len(ident) == 0) {
     79 		fmt::fprintf(ctx.out, "<h2>The Hare standard library <span class='heading-extra'>")?;
     80 	} else {
     81 		fmt::fprintf(ctx.out, "<h2><span class='heading-body'>{}</span><span class='heading-extra'>", ident)?;
     82 	};
     83 	for (let i = 0z; i < len(ctx.tags); i += 1) {
     84 		const mode = switch (ctx.tags[i].mode) {
     85 		case module::tag_mode::INCLUSIVE =>
     86 			yield '+';
     87 		case module::tag_mode::EXCLUSIVE =>
     88 			yield '-';
     89 		};
     90 		fmt::fprintf(ctx.out, "{}{} ", mode, ctx.tags[i].name)?;
     91 	};
     92 	fmt::fprintln(ctx.out, "</span></h2>")?;
     93 
     94 	match (ctx.readme) {
     95 	case void => void;
     96 	case let f: io::file =>
     97 		fmt::fprintln(ctx.out, "<div class='readme'>")?;
     98 		markup_html(ctx, f)?;
     99 		fmt::fprintln(ctx.out, "</div>")?;
    100 	};
    101 
    102 	let identpath = module::identpath(ctx.ident);
    103 	defer free(identpath);
    104 
    105 	let submodules: []str = [];
    106 	defer free(submodules);
    107 
    108 	for (let i = 0z; i < len(ctx.version.subdirs); i += 1) {
    109 		let dir = ctx.version.subdirs[i];
    110 		// XXX: the list of reserved directory names is not yet
    111 		// finalized. See https://todo.sr.ht/~sircmpwn/hare/516
    112 		if (dir == "contrib") continue;
    113 		if (dir == "cmd") continue;
    114 		if (dir == "docs") continue;
    115 		if (dir == "ext") continue;
    116 		if (dir == "vendor") continue;
    117 		if (dir == "scripts") continue;
    118 
    119 		let submod = [identpath, dir]: ast::ident;
    120 		if (module::lookup(ctx.mctx, submod) is module::error) {
    121 			continue;
    122 		};
    123 
    124 		append(submodules, dir);
    125 	};
    126 
    127 	if (len(submodules) != 0) {
    128 		if (len(ctx.ident) == 0) {
    129 			fmt::fprintln(ctx.out, "<h3>Modules</h3>")?;
    130 		} else {
    131 			fmt::fprintln(ctx.out, "<h3>Submodules</h3>")?;
    132 		};
    133 		fmt::fprintln(ctx.out, "<ul class='submodules'>")?;
    134 		for (let i = 0z; i < len(submodules); i += 1) {
    135 			let submodule = submodules[i];
    136 			let path = path::init("/", identpath, submodule)!;
    137 
    138 			fmt::fprintf(ctx.out, "<li><a href='")?;
    139 			html_escape(ctx.out, path::string(&path))?;
    140 			fmt::fprintf(ctx.out, "'>")?;
    141 			html_escape(ctx.out, submodule)?;
    142 			fmt::fprintfln(ctx.out, "</a></li>")?;
    143 		};
    144 		fmt::fprintln(ctx.out, "</ul>")?;
    145 	};
    146 
    147 	if (len(decls.types) == 0
    148 			&& len(decls.errors) == 0
    149 			&& len(decls.constants) == 0
    150 			&& len(decls.globals) == 0
    151 			&& len(decls.funcs) == 0) {
    152 		return;
    153 	};
    154 
    155 	fmt::fprintln(ctx.out, "<h3>Index</h3>")?;
    156 	tocentries(ctx.out, decls.types, "Types", "types")?;
    157 	tocentries(ctx.out, decls.errors, "Errors", "Errors")?;
    158 	tocentries(ctx.out, decls.constants, "Constants", "constants")?;
    159 	tocentries(ctx.out, decls.globals, "Globals", "globals")?;
    160 	tocentries(ctx.out, decls.funcs, "Functions", "functions")?;
    161 
    162 	if (len(decls.types) != 0) {
    163 		fmt::fprintln(ctx.out, "<h3>Types</h3>")?;
    164 		for (let i = 0z; i < len(decls.types); i += 1) {
    165 			details(ctx, decls.types[i])?;
    166 		};
    167 	};
    168 
    169 	if (len(decls.errors) != 0) {
    170 		fmt::fprintln(ctx.out, "<h3>Errors</h3>")?;
    171 		for (let i = 0z; i < len(decls.errors); i += 1) {
    172 			details(ctx, decls.errors[i])?;
    173 		};
    174 	};
    175 
    176 	if (len(decls.constants) != 0) {
    177 		fmt::fprintln(ctx.out, "<h3>Constants</h3>")?;
    178 		for (let i = 0z; i < len(decls.constants); i += 1) {
    179 			details(ctx, decls.constants[i])?;
    180 		};
    181 	};
    182 
    183 	if (len(decls.globals) != 0) {
    184 		fmt::fprintln(ctx.out, "<h3>Globals</h3>")?;
    185 		for (let i = 0z; i < len(decls.globals); i += 1) {
    186 			details(ctx, decls.globals[i])?;
    187 		};
    188 	};
    189 
    190 	if (len(decls.funcs) != 0) {
    191 		fmt::fprintln(ctx.out, "<h3>Functions</h3>")?;
    192 		for (let i = 0z; i < len(decls.funcs); i += 1) {
    193 			details(ctx, decls.funcs[i])?;
    194 		};
    195 	};
    196 };
    197 
    198 fn comment_html(out: io::handle, s: str) (size | io::error) = {
    199 	// TODO: handle [[references]]
    200 	let z = fmt::fprint(out, "<span class='comment'>//")?;
    201 	z += html_escape(out, s)?;
    202 	z += fmt::fprint(out, "</span><br>")?;
    203 	return z;
    204 };
    205 
    206 fn docs_html(out: io::handle, s: str, indent: size) (size | io::error) = {
    207 	const iter = strings::tokenize(s, "\n");
    208 	let z = 0z;
    209 	for (true) match (strings::next_token(&iter)) {
    210 	case let s: str =>
    211 		if (!(strings::peek_token(&iter) is void)) {
    212 			z += comment_html(out, s)?;
    213 			for (let i = 0z; i < indent; i += 1) {
    214 				z += fmt::fprint(out, "\t")?;
    215 			};
    216 		};
    217 	case void => break;
    218 	};
    219 
    220 	return z;
    221 };
    222 
    223 fn tocentries(
    224 	out: io::handle,
    225 	decls: []ast::decl,
    226 	name: str,
    227 	lname: str,
    228 ) (void | error) = {
    229 	if (len(decls) == 0) {
    230 		return;
    231 	};
    232 	fmt::fprintfln(out, "<h4>{}</h4>", name)?;
    233 	fmt::fprintln(out, "<pre>")?;
    234 	let undoc = false;
    235 	for (let i = 0z; i < len(decls); i += 1) {
    236 		if (!undoc && decls[i].docs == "") {
    237 			fmt::fprintfln(
    238 				out,
    239 				"{}<span class='comment'>// Undocumented {}:</span>",
    240 				if (i == 0) "" else "\n",
    241 				lname)?;
    242 			undoc = true;
    243 		};
    244 		tocentry(out, decls[i])?;
    245 	};
    246 	fmt::fprint(out, "</pre>")?;
    247 	return;
    248 };
    249 
    250 fn tocentry(out: io::handle, decl: ast::decl) (void | error) = {
    251 	fmt::fprintf(out, "{} ",
    252 		match (decl.decl) {
    253 		case ast::decl_func =>
    254 			yield "fn";
    255 		case []ast::decl_type =>
    256 			yield "type";
    257 		case []ast::decl_const =>
    258 			yield "const";
    259 		case []ast::decl_global =>
    260 			yield "let";
    261 		})?;
    262 	fmt::fprintf(out, "<a href='#")?;
    263 	unparse::ident(out, decl_ident(decl))?;
    264 	fmt::fprintf(out, "'>")?;
    265 	unparse::ident(out, decl_ident(decl))?;
    266 	fmt::fprint(out, "</a>")?;
    267 
    268 	match (decl.decl) {
    269 	case let t: []ast::decl_type => void;
    270 	case let g: []ast::decl_global =>
    271 		let g = g[0];
    272 		match (g._type) {
    273 		case null =>
    274 			yield;
    275 		case let ty: *ast::_type =>
    276 			fmt::fprint(out, ": ")?;
    277 			type_html(out, 0, *ty, true)?;
    278 		};
    279 	case let c: []ast::decl_const =>
    280 		let c = c[0];
    281 		match (c._type) {
    282 		case null =>
    283 			yield;
    284 		case let ty: *ast::_type =>
    285 			fmt::fprint(out, ": ")?;
    286 			type_html(out, 0, *ty, true)?;
    287 		};
    288 	case let f: ast::decl_func =>
    289 		prototype_html(out, 0,
    290 			f.prototype.repr as ast::func_type,
    291 			true)?;
    292 	};
    293 	fmt::fprintln(out, ";")?;
    294 	return;
    295 };
    296 
    297 fn details(ctx: *context, decl: ast::decl) (void | error) = {
    298 	fmt::fprintln(ctx.out, "<section class='member'>")?;
    299 	fmt::fprint(ctx.out, "<h4 id='")?;
    300 	unparse::ident(ctx.out, decl_ident(decl))?;
    301 	fmt::fprint(ctx.out, "'><span class='heading-body'>")?;
    302 	fmt::fprintf(ctx.out, "{} ", match (decl.decl) {
    303 		case ast::decl_func =>
    304 			yield "fn";
    305 		case []ast::decl_type =>
    306 			yield "type";
    307 		case []ast::decl_const =>
    308 			yield "def";
    309 		case []ast::decl_global =>
    310 			yield "let";
    311 		})?;
    312 	unparse::ident(ctx.out, decl_ident(decl))?;
    313 	// TODO: Add source URL
    314 	fmt::fprint(ctx.out, "</span><span class='heading-extra'><a href='#")?;
    315 	unparse::ident(ctx.out, decl_ident(decl))?;
    316 	fmt::fprint(ctx.out, "'>[link]</a>
    317 	</span>")?;
    318 	fmt::fprintln(ctx.out, "</h4>")?;
    319 
    320 	if (len(decl.docs) == 0) {
    321 		fmt::fprintln(ctx.out, "<details>")?;
    322 		fmt::fprintln(ctx.out, "<summary>Show undocumented member</summary>")?;
    323 	};
    324 
    325 	fmt::fprintln(ctx.out, "<pre class='decl'>")?;
    326 	unparse_html(ctx.out, decl)?;
    327 	fmt::fprintln(ctx.out, "</pre>")?;
    328 
    329 	if (len(decl.docs) != 0) {
    330 		const trimmed = trim_comment(decl.docs);
    331 		defer free(trimmed);
    332 		const buf = strings::toutf8(trimmed);
    333 		markup_html(ctx, &memio::fixed(buf))?;
    334 	} else {
    335 		fmt::fprintln(ctx.out, "</details>")?;
    336 	};
    337 
    338 	fmt::fprintln(ctx.out, "</section>")?;
    339 	return;
    340 };
    341 
    342 fn htmlref(ctx: *context, ref: ast::ident) (void | io::error) = {
    343 	const ik =
    344 		match (resolve(ctx, ref)) {
    345 		case let ik: (ast::ident, symkind) =>
    346 			yield ik;
    347 		case void =>
    348 			const ident = unparse::identstr(ref);
    349 			fmt::errorfln("Warning: Unresolved reference: {}", ident)?;
    350 			fmt::fprintf(ctx.out, "<a href='#' "
    351 				"class='ref invalid' "
    352 				"title='This reference could not be found'>{}</a>",
    353 				ident)?;
    354 			free(ident);
    355 			return;
    356 		};
    357 
    358 	// TODO: The reference is not necessarily in the stdlib
    359 	const kind = ik.1, id = ik.0;
    360 	const ident = unparse::identstr(id);
    361 	switch (kind) {
    362 	case symkind::LOCAL =>
    363 		fmt::fprintf(ctx.out, "<a href='#{0}' class='ref'>{0}</a>", ident)?;
    364 	case symkind::MODULE =>
    365 		let ipath = module::identpath(id);
    366 		defer free(ipath);
    367 		fmt::fprintf(ctx.out, "<a href='/{}' class='ref'>{}</a>",
    368 			ipath, ident)?;
    369 	case symkind::SYMBOL =>
    370 		let ipath = module::identpath(id[..len(id) - 1]);
    371 		defer free(ipath);
    372 		fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>",
    373 			ipath, id[len(id) - 1], ident)?;
    374 	case symkind::ENUM_LOCAL =>
    375 		fmt::fprintf(ctx.out, "<a href='#{}' class='ref'>{}</a>",
    376 			id[len(id) - 2], ident)?;
    377 	case symkind::ENUM_REMOTE =>
    378 		let ipath = module::identpath(id[..len(id) - 2]);
    379 		defer free(ipath);
    380 		fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>",
    381 			ipath, id[len(id) - 2], ident)?;
    382 	};
    383 	free(ident);
    384 };
    385 
    386 fn markup_html(ctx: *context, in: io::handle) (void | io::error) = {
    387 	let parser = parsedoc(in);
    388 	let waslist = false;
    389 	for (true) {
    390 		const tok = match (scandoc(&parser)) {
    391 			case void =>
    392 				if (waslist) {
    393 					fmt::fprintln(ctx.out, "</ul>")?;
    394 				};
    395 				break;
    396 			case let tok: token =>
    397 				yield tok;
    398 			};
    399 		match (tok) {
    400 		case paragraph =>
    401 			if (waslist) {
    402 				fmt::fprintln(ctx.out, "</ul>")?;
    403 				waslist = false;
    404 			};
    405 			fmt::fprintln(ctx.out)?;
    406 			fmt::fprint(ctx.out, "<p>")?;
    407 		case let tx: text =>
    408 			defer free(tx);
    409 			match (uri::parse(strings::trim(tx))) {
    410 			case let uri: uri::uri =>
    411 				defer uri::finish(&uri);
    412 				if (uri.host is net::ip::addr || len(uri.host as str) > 0) {
    413 					fmt::fprint(ctx.out, "<a rel='nofollow noopener' href='")?;
    414 					uri::fmt(ctx.out, &uri)?;
    415 					fmt::fprint(ctx.out, "'>")?;
    416 					html_escape(ctx.out, tx)?;
    417 					fmt::fprint(ctx.out, "</a>")?;
    418 				} else {
    419 					html_escape(ctx.out, tx)?;
    420 				};
    421 			case uri::invalid =>
    422 				html_escape(ctx.out, tx)?;
    423 			};
    424 		case let re: reference =>
    425 			htmlref(ctx, re)?;
    426 		case let sa: sample =>
    427 			if (waslist) {
    428 				fmt::fprintln(ctx.out, "</ul>")?;
    429 				waslist = false;
    430 			};
    431 			fmt::fprint(ctx.out, "<pre class='sample'>")?;
    432 			html_escape(ctx.out, sa)?;
    433 			fmt::fprint(ctx.out, "</pre>")?;
    434 			free(sa);
    435 		case listitem =>
    436 			if (!waslist) {
    437 				fmt::fprintln(ctx.out, "<ul>")?;
    438 				waslist = true;
    439 			};
    440 			fmt::fprint(ctx.out, "<li>")?;
    441 		};
    442 	};
    443 	fmt::fprintln(ctx.out)?;
    444 	return;
    445 };
    446 
    447 // Forked from [[hare::unparse]]
    448 fn unparse_html(out: io::handle, d: ast::decl) (size | io::error) = {
    449 	let n = 0z;
    450 	match (d.decl) {
    451 	case let c: []ast::decl_const =>
    452 		n += fmt::fprintf(out, "<span class='keyword'>def</span> ")?;
    453 		for (let i = 0z; i < len(c); i += 1) {
    454 			n += unparse::ident(out, c[i].ident)?;
    455 			match (c[i]._type) {
    456 			case null =>
    457 				yield;
    458 			case let ty: *ast::_type =>
    459 				n += fmt::fprint(out, ": ")?;
    460 				n += type_html(out, 0, *ty, false)?;
    461 			};
    462 			if (i + 1 < len(c)) {
    463 				n += fmt::fprint(out, ", ")?;
    464 			};
    465 		};
    466 	case let g: []ast::decl_global =>
    467 		n += fmt::fprintf(out, "<span class='keyword'>{}</span>",
    468 			if (g[0].is_const) "const " else "let ")?;
    469 		for (let i = 0z; i < len(g); i += 1) {
    470 			n += unparse::ident(out, g[i].ident)?;
    471 			match (g[i]._type) {
    472 			case null =>
    473 				yield;
    474 			case let ty: *ast::_type =>
    475 				n += fmt::fprint(out, ": ")?;
    476 				n += type_html(out, 0, *ty, false)?;
    477 			};
    478 			if (i + 1 < len(g)) {
    479 				n += fmt::fprint(out, ", ")?;
    480 			};
    481 		};
    482 	case let t: []ast::decl_type =>
    483 		n += fmt::fprint(out, "<span class='keyword'>type</span> ")?;
    484 		for (let i = 0z; i < len(t); i += 1) {
    485 			n += unparse::ident(out, t[i].ident)?;
    486 			n += fmt::fprint(out, " = ")?;
    487 			n += type_html(out, 0, t[i]._type, false)?;
    488 			if (i + 1 < len(t)) {
    489 				n += fmt::fprint(out, ", ")?;
    490 			};
    491 		};
    492 	case let f: ast::decl_func =>
    493 		n += fmt::fprint(out, switch (f.attrs) {
    494 		case ast::fndecl_attrs::NONE =>
    495 			yield "";
    496 		case ast::fndecl_attrs::FINI =>
    497 			yield "@fini ";
    498 		case ast::fndecl_attrs::INIT =>
    499 			yield "@init ";
    500 		case ast::fndecl_attrs::TEST =>
    501 			yield "@test ";
    502 		})?;
    503 		let p = f.prototype.repr as ast::func_type;
    504 		if (p.attrs & ast::func_attrs::NORETURN != 0) {
    505 			n += fmt::fprint(out, "@noreturn ")?;
    506 		};
    507 		n += fmt::fprint(out, "<span class='keyword'>fn</span> ")?;
    508 		n += unparse::ident(out, f.ident)?;
    509 		n += prototype_html(out, 0,
    510 			f.prototype.repr as ast::func_type,
    511 			false)?;
    512 	};
    513 	n += fmt::fprint(out, ";")?;
    514 	return n;
    515 };
    516 
    517 fn enum_html(
    518 	out: io::handle,
    519 	indent: size,
    520 	t: ast::enum_type
    521 ) (size | io::error) = {
    522 	let z = 0z;
    523 
    524 	z += fmt::fprint(out, "<span class='type'>enum</span> ")?;
    525 	if (t.storage != ast::builtin_type::INT) {
    526 		z += fmt::fprintf(out, "<span class='type'>{}</span> ",
    527 			unparse::builtin_type(t.storage))?;
    528 	};
    529 	z += fmt::fprintln(out, "{")?;
    530 	indent += 1;
    531 	for (let i = 0z; i < len(t.values); i += 1) {
    532 		for (let i = 0z; i < indent; i += 1) {
    533 			z += fmt::fprint(out, "\t")?;
    534 		};
    535 		const val = t.values[i];
    536 		let wrotedocs = false;
    537 		if (val.docs != "") {
    538 			// Check if comment should go above or next to field
    539 			if (multiline_comment(val.docs)) {
    540 				z += docs_html(out, val.docs, indent)?;
    541 				wrotedocs = true;
    542 			};
    543 		};
    544 
    545 		z += fmt::fprint(out, val.name)?;
    546 
    547 		match (val.value) {
    548 		case null => void;
    549 		case let expr: *ast::expr =>
    550 			z += fmt::fprint(out, " = ")?;
    551 			z += unparse::expr(out, indent, *expr)?;
    552 		};
    553 
    554 		z += fmt::fprint(out, ",")?;
    555 
    556 		if (val.docs != "" && !wrotedocs) {
    557 			z += fmt::fprint(out, " ")?;
    558 			z += docs_html(out, val.docs, 0)?;
    559 		} else {
    560 			z += fmt::fprintln(out)?;
    561 		};
    562 	};
    563 	indent -= 1;
    564 	for (let i = 0z; i < indent; i += 1) {
    565 		z += fmt::fprint(out, "\t")?;
    566 	};
    567 	z += newline(out, indent)?;
    568 	z += fmt::fprint(out, "}")?;
    569 	return z;
    570 };
    571 
    572 fn struct_union_html(
    573 	out: io::handle,
    574 	indent: size,
    575 	t: ast::_type,
    576 	brief: bool,
    577 ) (size | io::error) = {
    578 	let z = 0z;
    579 	let members = match (t.repr) {
    580 	case let t: ast::struct_type =>
    581 		z += fmt::fprint(out, "<span class='keyword'>struct</span>")?;
    582 		if (t.packed) {
    583 			z += fmt::fprint(out, " @packed")?;
    584 		};
    585 		z += fmt::fprint(out, " {")?;
    586 		yield t.members: []ast::struct_member;
    587 	case let t: ast::union_type =>
    588 		z += fmt::fprint(out, "<span class='keyword'>union</span> {")?;
    589 		yield t: []ast::struct_member;
    590 	};
    591 
    592 	indent += 1;
    593 	for (let i = 0z; i < len(members); i += 1) {
    594 		const member = members[i];
    595 
    596 		z += newline(out, indent)?;
    597 		if (member.docs != "" && !brief) {
    598 			z += docs_html(out, member.docs, indent)?;
    599 		};
    600 		match (member._offset) {
    601 		case null => void;
    602 		case let expr: *ast::expr =>
    603 			z += fmt::fprint(out, "@offset(")?;
    604 			z += unparse::expr(out, indent, *expr)?;
    605 			z += fmt::fprint(out, ") ")?;
    606 		};
    607 
    608 		match (member.member) {
    609 		case let f: ast::struct_field =>
    610 			z += fmt::fprintf(out, "{}: ", f.name)?;
    611 			z += type_html(out, indent, *f._type, brief)?;
    612 		case let embed: ast::struct_embedded =>
    613 			z += type_html(out, indent, *embed, brief)?;
    614 		case let indent: ast::struct_alias =>
    615 			z += unparse::ident(out, indent)?;
    616 		};
    617 		z += fmt::fprint(out, ",")?;
    618 	};
    619 
    620 	indent -= 1;
    621 	z += newline(out, indent)?;
    622 	z += fmt::fprint(out, "}")?;
    623 
    624 	return z;
    625 };
    626 
    627 fn type_html(
    628 	out: io::handle,
    629 	indent: size,
    630 	_type: ast::_type,
    631 	brief: bool,
    632 ) (size | io::error) = {
    633 	if (brief) {
    634 		let buf = memio::dynamic();
    635 		defer io::close(&buf)!;
    636 		unparse::_type(&buf, indent, _type)?;
    637 		return html_escape(out, memio::string(&buf)!)?;
    638 	};
    639 
    640 	// TODO: More detailed formatter which can find aliases nested deeper in
    641 	// other types and highlight more keywords, like const
    642 	let z = 0z;
    643 
    644 	if (_type.flags & ast::type_flag::CONST != 0
    645 			&& !(_type.repr is ast::func_type)) {
    646 		z += fmt::fprint(out, "<span class='keyword'>const</span> ")?;
    647 	};
    648 
    649 	if (_type.flags & ast::type_flag::ERROR != 0) {
    650 		if (_type.repr is ast::builtin_type) {
    651 			z += fmt::fprint(out, "<span class='type'>!</span>")?;
    652 		} else {
    653 			z += fmt::fprint(out, "!")?;
    654 		};
    655 	};
    656 
    657 	match (_type.repr) {
    658 	case let a: ast::alias_type =>
    659 		if (a.unwrap) {
    660 			z += fmt::fprint(out, "...")?;
    661 		};
    662 		z += unparse::ident(out, a.ident)?;
    663 	case let t: ast::builtin_type =>
    664 		z += fmt::fprintf(out, "<span class='type'>{}</span>",
    665 			unparse::builtin_type(t))?;
    666 	case let t: ast::tagged_type =>
    667 		// rough estimate of current line length
    668 		let linelen: size = z + (indent + 1) * 8;
    669 		z = 0;
    670 		linelen += fmt::fprint(out, "(")?;
    671 		for (let i = 0z; i < len(t); i += 1) {
    672 			linelen += type_html(out, indent, *t[i], brief)?;
    673 			if (i + 1 == len(t)) break;
    674 			linelen += fmt::fprint(out, " |")?;
    675 			// use 72 instead of 80 to give a bit of leeway for long
    676 			// type names
    677 			if (linelen > 72) {
    678 				z += linelen;
    679 				linelen = (indent + 1) * 8;
    680 				z += fmt::fprintln(out)?;
    681 				for (let i = 0z; i < indent; i += 1) {
    682 					z += fmt::fprint(out, "\t")?;
    683 				};
    684 			} else {
    685 				linelen += fmt::fprint(out, " ")?;
    686 			};
    687 		};
    688 		z += linelen;
    689 		z += fmt::fprint(out, ")")?;
    690 	case let t: ast::tuple_type =>
    691 		// rough estimate of current line length
    692 		let linelen: size = z + (indent + 1) * 8;
    693 		z = 0;
    694 		linelen += fmt::fprint(out, "(")?;
    695 		for (let i = 0z; i < len(t); i += 1) {
    696 			linelen += type_html(out, indent, *t[i], brief)?;
    697 			if (i + 1 == len(t)) break;
    698 			linelen += fmt::fprint(out, ",")?;
    699 			// use 72 instead of 80 to give a bit of leeway for long
    700 			// type names
    701 			if (linelen > 72) {
    702 				z += linelen;
    703 				linelen = (indent + 1) * 8;
    704 				z += fmt::fprintln(out)?;
    705 				for (let i = 0z; i < indent; i += 1) {
    706 					z += fmt::fprint(out, "\t")?;
    707 				};
    708 			} else {
    709 				linelen += fmt::fprint(out, " ")?;
    710 			};
    711 		};
    712 		z += linelen;
    713 		z += fmt::fprint(out, ")")?;
    714 	case let t: ast::pointer_type =>
    715 		if (t.flags & ast::pointer_flag::NULLABLE != 0) {
    716 			z += fmt::fprint(out, "<span class='type'>nullable</span> ")?;
    717 		};
    718 		z += fmt::fprint(out, "*")?;
    719 		z += type_html(out, indent, *t.referent, brief)?;
    720 	case let t: ast::func_type =>
    721 		if (t.attrs & ast::func_attrs::NORETURN == ast::func_attrs::NORETURN) {
    722 			z += fmt::fprint(out, "@noreturn ")?;
    723 		};
    724 
    725 		z += fmt::fprint(out, "<span class='keyword'>fn</span>(")?;
    726 		for (let i = 0z; i < len(t.params); i += 1) {
    727 			const param = t.params[i];
    728 			z += fmt::fprintf(out, "{}: ",
    729 				if (len(param.name) == 0) "_" else param.name)?;
    730 			z += type_html(out, indent, *param._type, brief)?;
    731 
    732 			if (i + 1 == len(t.params)
    733 					&& t.variadism == ast::variadism::HARE) {
    734 				// TODO: Highlight that as well
    735 				z += fmt::fprint(out, "...")?;
    736 			};
    737 			if (i + 1 < len(t.params)) {
    738 				z += fmt::fprint(out, ", ")?;
    739 			};
    740 		};
    741 		if (t.variadism == ast::variadism::C) {
    742 			z += fmt::fprint(out, ", ...")?;
    743 		};
    744 		z += fmt::fprint(out, ") ")?;
    745 		z += type_html(out, indent, *t.result, brief)?;
    746 	case let t: ast::enum_type =>
    747 		z += enum_html(out, indent, t)?;
    748 	case let t: ast::list_type =>
    749 		z += fmt::fprint(out, "[")?;
    750 		match (t.length) {
    751 		case let expr: *ast::expr =>
    752 			z += unparse::expr(out, indent, *expr)?;
    753 		case ast::len_slice =>
    754 			z += 0;
    755 		case ast::len_unbounded =>
    756 			z += fmt::fprintf(out, "*")?;
    757 		case ast::len_contextual =>
    758 			z += fmt::fprintf(out, "_")?;
    759 		};
    760 		z += fmt::fprint(out, "]")?;
    761 
    762 		z += type_html(out, indent, *t.members, brief)?;
    763 	case let t: ast::struct_type =>
    764 		z += struct_union_html(out, indent, _type, brief)?;
    765 	case let t: ast::union_type =>
    766 		z += struct_union_html(out, indent, _type, brief)?;
    767 	};
    768 
    769 	return z;
    770 };
    771 
    772 fn prototype_html(
    773 	out: io::handle,
    774 	indent: size,
    775 	t: ast::func_type,
    776 	brief: bool,
    777 ) (size | io::error) = {
    778 	let n = 0z;
    779 	n += fmt::fprint(out, "(")?;
    780 
    781 	// estimate length of prototype to determine if it should span multiple
    782 	// lines
    783 	const linelen = if (len(t.params) == 0 || brief) {
    784 		yield 0z; // If no parameters or brief, only use one line.
    785 	} else {
    786 		let linelen = indent * 8 + 5;
    787 		linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0;
    788 		for (let i = 0z; i < len(t.params); i += 1) {
    789 			const param = t.params[i];
    790 			linelen += unparse::_type(io::empty, indent,
    791 				*param._type)?;
    792 			linelen += if (param.name == "") 1 else len(param.name);
    793 		};
    794 		switch (t.variadism) {
    795 		case variadism::NONE => void;
    796 		case variadism::HARE =>
    797 			linelen += 3;
    798 		case variadism::C =>
    799 			linelen += 5;
    800 		};
    801 		linelen += unparse::_type(io::empty, indent, *t.result)?;
    802 		yield linelen;
    803 	};
    804 
    805 	// use 72 instead of 80 to give a bit of leeway for preceding text
    806 	if (linelen > 72) {
    807 		indent += 1;
    808 		for (let i = 0z; i < len(t.params); i += 1) {
    809 			const param = t.params[i];
    810 			n += newline(out, indent)?;
    811 			n += fmt::fprintf(out, "{}: ",
    812 				if (param.name == "") "_" else param.name)?;
    813 			n += type_html(out, indent, *param._type, brief)?;
    814 			if (i + 1 == len(t.params)
    815 					&& t.variadism == variadism::HARE) {
    816 				n += fmt::fprint(out, "...")?;
    817 			} else {
    818 				n += fmt::fprint(out, ",")?;
    819 			};
    820 		};
    821 		if (t.variadism == variadism::C) {
    822 			n += newline(out, indent)?;
    823 			n += fmt::fprint(out, "...")?;
    824 		};
    825 		indent -= 1;
    826 		n += newline(out, indent)?;
    827 	} else for (let i = 0z; i < len(t.params); i += 1) {
    828 		const param = t.params[i];
    829 		if (!brief) {
    830 			n += fmt::fprintf(out, "{}: ",
    831 				if (param.name == "") "_" else param.name)?;
    832 		};
    833 		n += type_html(out, indent, *param._type, brief)?;
    834 		if (i + 1 == len(t.params)) {
    835 			switch (t.variadism) {
    836 			case variadism::NONE => void;
    837 			case variadism::HARE =>
    838 				n += fmt::fprint(out, "...")?;
    839 			case variadism::C =>
    840 				n += fmt::fprint(out, ", ...")?;
    841 			};
    842 		} else {
    843 			n += fmt::fprint(out, ", ")?;
    844 		};
    845 	};
    846 
    847 	n += fmt::fprint(out, ") ")?;
    848 	n += type_html(out, indent, *t.result, brief)?;
    849 	return n;
    850 };
    851 
    852 fn breadcrumb(ident: ast::ident) str = {
    853 	if (len(ident) == 0) {
    854 		return "";
    855 	};
    856 	let buf = memio::dynamic();
    857 	fmt::fprintf(&buf, "<a href='/'>stdlib</a> » ")!;
    858 	for (let i = 0z; i < len(ident) - 1; i += 1) {
    859 		let ipath = module::identpath(ident[..i+1]);
    860 		defer free(ipath);
    861 		fmt::fprintf(&buf, "<a href='/{}'>{}</a>::", ipath, ident[i])!;
    862 	};
    863 	fmt::fprint(&buf, ident[len(ident) - 1])!;
    864 	return memio::string(&buf)!;
    865 };
    866 
    867 const harriet_b64 = "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";
    868 
    869 fn head(ident: ast::ident) (void | error) = {
    870 	const id = unparse::identstr(ident);
    871 	defer free(id);
    872 
    873 	let breadcrumb = breadcrumb(ident);
    874 	defer free(breadcrumb);
    875 
    876 	const title =
    877 		if (len(id) == 0)
    878 			fmt::asprintf("Hare documentation")
    879 		else
    880 			fmt::asprintf("{} — Hare documentation", id);
    881 	defer free(title);
    882 
    883 	// TODO: Move bits to +embed?
    884 	fmt::printfln("<!doctype html>
    885 <html lang='en'>
    886 <meta charset='utf-8' />
    887 <meta name='viewport' content='width=device-width, initial-scale=1' />
    888 <title>{}</title>
    889 <link rel='icon' type='image/png' href='data:image/png;base64,{}'>", title, harriet_b64)?;
    890 	fmt::println("<style>
    891 body {
    892 	font-family: sans-serif;
    893 	line-height: 1.3;
    894 	margin: 0 auto;
    895 	padding: 0 1rem;
    896 }
    897 
    898 nav:not(#TableOfContents) {
    899 	max-width: calc(800px + 128px + 128px);
    900 	margin: 1rem auto 0;
    901 	display: grid;
    902 	grid-template-rows: auto auto 1fr;
    903 	grid-template-columns: auto 1fr;
    904 	grid-template-areas:
    905 		'logo header'
    906 		'logo nav'
    907 		'logo none';
    908 }
    909 
    910 nav:not(#TableOfContents) img {
    911 	grid-area: logo;
    912 }
    913 
    914 nav:not(#TableOfContents) h1 {
    915 	grid-area: header;
    916 	margin: 0;
    917 	padding: 0;
    918 }
    919 
    920 nav:not(#TableOfContents) ul {
    921 	grid-area: nav;
    922 	margin: 0.5rem 0 0 0;
    923 	padding: 0;
    924 	list-style: none;
    925 	display: flex;
    926 	flex-direction: row;
    927 	justify-content: left;
    928 	flex-wrap: wrap;
    929 }
    930 
    931 nav:not(#TableOfContents) li:not(:first-child) {
    932 	margin-left: 2rem;
    933 }
    934 
    935 #TableOfContents {
    936 	font-size: 1.1rem;
    937 }
    938 
    939 main {
    940 	padding: 0 128px;
    941 	max-width: 800px;
    942 	margin: 0 auto;
    943 
    944 }
    945 
    946 pre {
    947 	background-color: #eee;
    948 	padding: 0.25rem 1rem;
    949 	margin: 0 -1rem 1rem;
    950 	font-size: 1.2rem;
    951 	max-width: calc(100% + 1rem);
    952 	overflow-x: auto;
    953 }
    954 
    955 pre .keyword {
    956     color: #008;
    957 }
    958 
    959 pre .type {
    960 	color: #44F;
    961 }
    962 
    963 ol {
    964 	padding-left: 0;
    965 	list-style: none;
    966 }
    967 
    968 ol li {
    969 	padding-left: 0;
    970 }
    971 
    972 h2, h3, h4 {
    973 	display: flex;
    974 }
    975 
    976 h3 {
    977 	border-bottom: 1px solid #ccc;
    978 	padding-bottom: 0.25rem;
    979 }
    980 
    981 .invalid {
    982 	color: red;
    983 }
    984 
    985 .heading-body {
    986 	word-wrap: anywhere;
    987 }
    988 
    989 .heading-extra {
    990 	align-self: flex-end;
    991 	flex-grow: 1;
    992 	padding-left: 0.5rem;
    993 	text-align: right;
    994 	font-size: 0.8rem;
    995 	color: #444;
    996 }
    997 
    998 h4:target + pre {
    999 	background: #ddf;
   1000 }
   1001 
   1002 details {
   1003 	background: #eee;
   1004 	margin: 1rem -1rem 1rem;
   1005 }
   1006 
   1007 summary {
   1008 	cursor: pointer;
   1009 	padding: 0.5rem 1rem;
   1010 }
   1011 
   1012 details pre {
   1013 	margin: 0;
   1014 }
   1015 
   1016 .comment {
   1017 	color: #000;
   1018 	font-weight: bold;
   1019 }
   1020 
   1021 @media(max-width: 1000px) {
   1022 	main {
   1023 		padding: 0;
   1024 	}
   1025 }
   1026 
   1027 @media(prefers-color-scheme: dark) {
   1028 	body {
   1029 		background: #121415;
   1030 		color: #e1dfdc;
   1031 	}
   1032 
   1033 	img.mascot {
   1034 		filter: invert(.92);
   1035 	}
   1036 
   1037 	a {
   1038 		color: #78bef8;
   1039 	}
   1040 
   1041 	a:visited {
   1042 		color: #48a7f5;
   1043 	}
   1044 
   1045 	summary {
   1046 		background: #16191c;
   1047 	}
   1048 
   1049 	h3 {
   1050 		border-bottom: solid #16191c;
   1051 	}
   1052 
   1053 	h4:target + pre {
   1054 		background: #162329;
   1055 	}
   1056 
   1057 	pre {
   1058 		background-color: #16191c;
   1059 	}
   1060 
   1061 	pre .keyword {
   1062 		color: #69f;
   1063 	}
   1064 
   1065 	pre .type {
   1066 		color: #3cf;
   1067 	}
   1068 
   1069 	.comment {
   1070 		color: #fff;
   1071 	}
   1072 
   1073 	.heading-extra {
   1074 		color: #9b9997;
   1075 	}
   1076 }
   1077 </style>")?;
   1078 	fmt::printfln("<nav>
   1079 	<img src='data:image/png;base64,{}'
   1080 		class='mascot'
   1081 		alt='An inked drawing of the Hare mascot, a fuzzy rabbit'
   1082 		width='128' height='128' />
   1083 	<h1>Hare documentation</h1>
   1084 	<ul>
   1085 		<li>
   1086 			<a href='https://harelang.org'>Home</a>
   1087 		</li>", harriet_b64)?;
   1088 	fmt::printf("<li>{}</li>", breadcrumb)?;
   1089 	fmt::print("</ul>
   1090 </nav>
   1091 <main>")?;
   1092 	return;
   1093 };