hare

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

html.ha (18149B)


      1 // SPDX-License-Identifier: GPL-3.0-only
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 // Note: ast::ident should never have to be escaped
      5 use encoding::utf8;
      6 use fmt;
      7 use hare::ast;
      8 use hare::lex;
      9 use hare::parse::doc;
     10 use hare::unparse;
     11 use io;
     12 use memio;
     13 use net::ip;
     14 use net::uri;
     15 use path;
     16 use strings;
     17 
     18 // Prints a string to an output handle, escaping any of HTML's reserved
     19 // characters.
     20 fn html_escape(out: io::handle, in: str) (size | io::error) = {
     21 	let z = 0z;
     22 	let iter = strings::iter(in);
     23 	for (let rn => strings::next(&iter)) {
     24 		z += fmt::fprint(out, switch (rn) {
     25 		case '&' =>
     26 			yield "&amp;";
     27 		case '<' =>
     28 			yield "&lt;";
     29 		case '>' =>
     30 			yield "&gt;";
     31 		case '"' =>
     32 			yield "&quot;";
     33 		case '\'' =>
     34 			yield "&apos;";
     35 		case =>
     36 			yield strings::fromutf8(utf8::encoderune(rn))!;
     37 		})?;
     38 	};
     39 	return z;
     40 };
     41 
     42 @test fn html_escape() void = {
     43 	let sink = memio::dynamic();
     44 	defer io::close(&sink)!;
     45 	html_escape(&sink, "hello world!")!;
     46 	assert(memio::string(&sink)! == "hello world!");
     47 
     48 	let sink = memio::dynamic();
     49 	defer io::close(&sink)!;
     50 	html_escape(&sink, "\"hello world!\"")!;
     51 	assert(memio::string(&sink)! == "&quot;hello world!&quot;");
     52 
     53 	let sink = memio::dynamic();
     54 	defer io::close(&sink)!;
     55 	html_escape(&sink, "<hello & 'world'!>")!;
     56 	assert(memio::string(&sink)! == "&lt;hello &amp; &apos;world&apos;!&gt;");
     57 };
     58 
     59 // Formats output as HTML
     60 export fn emit_html(ctx: *context) (void | error) = {
     61 	const decls = ctx.summary;
     62 	const ident = unparse::identstr(ctx.ident);
     63 	defer free(ident);
     64 
     65 	if (ctx.template) {
     66 		head(ctx.ident)?;
     67 	};
     68 
     69 	if (len(ident) == 0) {
     70 		fmt::fprintf(ctx.out, "<h2>The Hare standard library <span class='heading-extra'>")?;
     71 	} else {
     72 		fmt::fprintf(ctx.out, "<h2><span class='heading-body'>{}</span><span class='heading-extra'>", ident)?;
     73 	};
     74 	for (let tag .. ctx.tags) {
     75 		fmt::fprintf(ctx.out, "+{} ", tag)?;
     76 	};
     77 	fmt::fprintln(ctx.out, "</span></h2>")?;
     78 
     79 	match (ctx.readme) {
     80 	case void => void;
     81 	case let f: io::file =>
     82 		fmt::fprintln(ctx.out, "<div class='readme'>")?;
     83 		markup_html(ctx, f, lex::location {
     84 			path = "README", // XXX: this is meh
     85 			line = 1,
     86 			col = 1,
     87 		})?;
     88 		fmt::fprintln(ctx.out, "</div>")?;
     89 	};
     90 
     91 	let identpath = strings::join("/", ctx.ident...);
     92 	defer free(identpath);
     93 
     94 	if (len(ctx.submods) != 0) {
     95 		if (len(ctx.ident) == 0) {
     96 			fmt::fprintln(ctx.out, "<h3>Modules</h3>")?;
     97 		} else {
     98 			fmt::fprintln(ctx.out, "<h3>Submodules</h3>")?;
     99 		};
    100 		fmt::fprintln(ctx.out, "<ul class='submodules'>")?;
    101 		for (let submodule .. ctx.submods) {
    102 			let path = path::init("/", identpath, submodule)!;
    103 
    104 			fmt::fprintf(ctx.out, "<li><a href='")?;
    105 			html_escape(ctx.out, path::string(&path))?;
    106 			fmt::fprintf(ctx.out, "'>")?;
    107 			html_escape(ctx.out, submodule)?;
    108 			fmt::fprintfln(ctx.out, "</a></li>")?;
    109 		};
    110 		fmt::fprintln(ctx.out, "</ul>")?;
    111 	};
    112 
    113 	if (len(decls.types) == 0
    114 			&& len(decls.errors) == 0
    115 			&& len(decls.constants) == 0
    116 			&& len(decls.globals) == 0
    117 			&& len(decls.funcs) == 0) {
    118 		return;
    119 	};
    120 
    121 	fmt::fprintln(ctx.out, "<h3>Index</h3>")?;
    122 	tocentries(ctx.out, decls.types, "Types", "types")?;
    123 	tocentries(ctx.out, decls.errors, "Errors", "Errors")?;
    124 	tocentries(ctx.out, decls.constants, "Constants", "constants")?;
    125 	tocentries(ctx.out, decls.globals, "Globals", "globals")?;
    126 	tocentries(ctx.out, decls.funcs, "Functions", "functions")?;
    127 
    128 	if (len(decls.types) != 0) {
    129 		fmt::fprintln(ctx.out, "<h3>Types</h3>")?;
    130 		for (let t &.. decls.types) {
    131 			details(ctx, t)?;
    132 		};
    133 	};
    134 
    135 	if (len(decls.errors) != 0) {
    136 		fmt::fprintln(ctx.out, "<h3>Errors</h3>")?;
    137 		for (let e &.. decls.errors) {
    138 			details(ctx, e)?;
    139 		};
    140 	};
    141 
    142 	if (len(decls.constants) != 0) {
    143 		fmt::fprintln(ctx.out, "<h3>Constants</h3>")?;
    144 		for (let c &.. decls.constants) {
    145 			details(ctx, c)?;
    146 		};
    147 	};
    148 
    149 	if (len(decls.globals) != 0) {
    150 		fmt::fprintln(ctx.out, "<h3>Globals</h3>")?;
    151 		for (let g &.. decls.globals) {
    152 			details(ctx, g)?;
    153 		};
    154 	};
    155 
    156 	if (len(decls.funcs) != 0) {
    157 		fmt::fprintln(ctx.out, "<h3>Functions</h3>")?;
    158 		for (let f &.. decls.funcs) {
    159 			details(ctx, f)?;
    160 		};
    161 	};
    162 };
    163 
    164 fn tocentries(
    165 	out: io::handle,
    166 	decls: []ast::decl,
    167 	name: str,
    168 	lname: str,
    169 ) (void | error) = {
    170 	if (len(decls) == 0) {
    171 		return;
    172 	};
    173 	fmt::fprintfln(out, "<h4>{}</h4>", name)?;
    174 	fmt::fprintln(out, "<pre>")?;
    175 	let undoc = false;
    176 	for (let i = 0z; i < len(decls); i += 1) {
    177 		if (!undoc && decls[i].docs == "") {
    178 			fmt::fprintfln(
    179 				out,
    180 				"{}<span class='comment'>// Undocumented {}:</span>",
    181 				if (i == 0) "" else "\n",
    182 				lname)?;
    183 			undoc = true;
    184 		};
    185 		unparse::decl(out, &syn_centry, &decls[i])?;
    186 		fmt::fprintln(out)?;
    187 	};
    188 	fmt::fprint(out, "</pre>")?;
    189 	return;
    190 };
    191 
    192 fn details(ctx: *context, decl: *ast::decl) (void | error) = {
    193 	fmt::fprintln(ctx.out, "<section class='member'>")?;
    194 	fmt::fprint(ctx.out, "<h4 id='")?;
    195 	unparse::ident(ctx.out, decl_ident(decl))?;
    196 	fmt::fprint(ctx.out, "'><span class='heading-body'>")?;
    197 	fmt::fprintf(ctx.out, "{} ", match (decl.decl) {
    198 		case ast::decl_func =>
    199 			yield "fn";
    200 		case []ast::decl_type =>
    201 			yield "type";
    202 		case []ast::decl_const =>
    203 			yield "def";
    204 		case []ast::decl_global =>
    205 			yield "let";
    206 		case ast::assert_expr => abort();
    207 		})?;
    208 	unparse::ident(ctx.out, decl_ident(decl))?;
    209 	// TODO: Add source URL
    210 	fmt::fprint(ctx.out, "</span><span class='heading-extra'><a href='#")?;
    211 	unparse::ident(ctx.out, decl_ident(decl))?;
    212 	fmt::fprint(ctx.out, "'>[link]</a>
    213 	</span>")?;
    214 	fmt::fprintln(ctx.out, "</h4>")?;
    215 
    216 	if (len(decl.docs) == 0) {
    217 		fmt::fprintln(ctx.out, "<details>")?;
    218 		fmt::fprintln(ctx.out, "<summary>Show undocumented member</summary>")?;
    219 	};
    220 
    221 	fmt::fprintln(ctx.out, "<pre class='decl'>")?;
    222 	unparse::decl(ctx.out, &syn_html, decl)?;
    223 	fmt::fprintln(ctx.out, "</pre>")?;
    224 
    225 	if (len(decl.docs) != 0) {
    226 		const trimmed = trim_comment(decl.docs);
    227 		defer free(trimmed);
    228 		const buf = strings::toutf8(trimmed);
    229 		markup_html(ctx, &memio::fixed(buf), decl.start)?;
    230 	} else {
    231 		fmt::fprintln(ctx.out, "</details>")?;
    232 	};
    233 
    234 	fmt::fprintln(ctx.out, "</section>")?;
    235 	return;
    236 };
    237 
    238 fn html_decl_ref(ctx: *context, ref: ast::ident) (void | error) = {
    239 	const ik =
    240 		match (resolve(ctx, ref)?) {
    241 		case let ik: (ast::ident, symkind) =>
    242 			yield ik;
    243 		case void =>
    244 			const ident = unparse::identstr(ref);
    245 			fmt::errorfln("Warning: Unresolved reference: {}", ident)?;
    246 			fmt::fprintf(ctx.out, "<a href='#' "
    247 				"class='ref invalid' "
    248 				"title='This reference could not be found'>{}</a>",
    249 				ident)?;
    250 			free(ident);
    251 			return;
    252 		};
    253 
    254 	// TODO: The reference is not necessarily in the stdlib
    255 	const kind = ik.1, id = ik.0;
    256 	const ident = unparse::identstr(id);
    257 	switch (kind) {
    258 	case symkind::LOCAL =>
    259 		fmt::fprintf(ctx.out, "<a href='#{0}' class='ref'>{0}</a>", ident)?;
    260 	case symkind::MODULE =>
    261 		let ipath = strings::join("/", id...);
    262 		defer free(ipath);
    263 		fmt::fprintf(ctx.out, "<a href='/{}' class='ref'>{}::</a>",
    264 			ipath, ident)?;
    265 	case symkind::SYMBOL =>
    266 		let ipath = strings::join("/", id[..len(id) - 1]...);
    267 		defer free(ipath);
    268 		fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>",
    269 			ipath, id[len(id) - 1], ident)?;
    270 	case symkind::ENUM_LOCAL =>
    271 		fmt::fprintf(ctx.out, "<a href='#{}' class='ref'>{}</a>",
    272 			id[len(id) - 2], ident)?;
    273 	case symkind::ENUM_REMOTE =>
    274 		let ipath = strings::join("/", id[..len(id) - 2]...);
    275 		defer free(ipath);
    276 		fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>",
    277 			ipath, id[len(id) - 2], ident)?;
    278 	};
    279 	free(ident);
    280 };
    281 
    282 fn html_mod_ref(ctx: *context, ref: ast::ident) (void | error) = {
    283 	const ident = unparse::identstr(ref);
    284 	defer free(ident);
    285 	let ipath = strings::join("/", ref...);
    286 	defer free(ipath);
    287 	fmt::fprintf(ctx.out, "<a href='/{}' class='ref'>{}::</a>",
    288 		ipath, ident)?;
    289 };
    290 
    291 
    292 fn html_paragraph(ctx: *context, p: doc::paragraph) (void | error) = {
    293 	for (let elem .. p) {
    294 		match (elem) {
    295 		case let s: str =>
    296 			match (uri::parse(s)) {
    297 			case let uri: uri::uri =>
    298 				defer uri::finish(&uri);
    299 				if (uri.host is ip::addr || len(uri.host as str) > 0) {
    300 					fmt::fprint(ctx.out, "<a rel='nofollow noopener' href='")?;
    301 					uri::fmt(ctx.out, &uri)?;
    302 					fmt::fprint(ctx.out, "'>")?;
    303 					html_escape(ctx.out, s)?;
    304 					fmt::fprint(ctx.out, "</a>")?;
    305 				} else {
    306 					html_escape(ctx.out, s)?;
    307 				};
    308 			case uri::invalid =>
    309 				html_escape(ctx.out, s)?;
    310 			};
    311 		case let d: doc::decl_ref =>
    312 			html_decl_ref(ctx, d)?;
    313 		case let m: doc::mod_ref =>
    314 			html_mod_ref(ctx, m)?;
    315 		};
    316 	};
    317 };
    318 
    319 fn markup_html(
    320 	ctx: *context,
    321 	in: io::handle,
    322 	loc: lex::location,
    323 ) (void | error) = {
    324 	const doc = match (doc::parse(in, loc)) {
    325 	case let doc: doc::doc =>
    326 		yield doc;
    327 	case let err: lex::syntax =>
    328 		const err = lex::strerror(err);
    329 		fmt::errorln("Warning:", err)?;
    330 		fmt::fprint(ctx.out, "<p class='ref invalid'>Can't parse docs: ")?;
    331 		html_escape(ctx.out, err)?;
    332 		fmt::fprintln(ctx.out)?;
    333 		return;
    334 	};
    335 	defer doc::freeall(doc);
    336 
    337 	for (let elem .. doc) {
    338 		match (elem) {
    339 		case let p: doc::paragraph =>
    340 			fmt::fprint(ctx.out, "<p>")?;
    341 			html_paragraph(ctx, p)?;
    342 			fmt::fprintln(ctx.out)?;
    343 		case let l: doc::list =>
    344 			fmt::fprintln(ctx.out, "<ul>")?;
    345 			for (let entry .. l) {
    346 				fmt::fprint(ctx.out, "<li>")?;
    347 				html_paragraph(ctx, entry)?;
    348 				fmt::fprintln(ctx.out)?;
    349 			};
    350 			fmt::fprintln(ctx.out, "</ul>")?;
    351 		case let c: doc::code_sample =>
    352 			fmt::fprint(ctx.out, "<pre class='sample'>")?;
    353 			html_escape(ctx.out, c)?;
    354 			fmt::fprintln(ctx.out, "</pre>")?;
    355 		};
    356 	};
    357 };
    358 
    359 fn syn_centry(
    360 	ctx: *unparse::context,
    361 	s: str,
    362 	kind: unparse::synkind,
    363 ) (size | io::error) = {
    364 	let z = 0z;
    365 	switch (kind) {
    366 	case unparse::synkind::CONSTANT,
    367 		unparse::synkind::FUNCTION,
    368 		unparse::synkind::GLOBAL,
    369 		unparse::synkind::TYPEDEF =>
    370 		z += fmt::fprint(ctx.out, "<a href='#")?;
    371 		z += html_escape(ctx.out, s)?;
    372 		z += fmt::fprint(ctx.out, "'>")?;
    373 		z += html_escape(ctx.out, s)?;
    374 		z += fmt::fprint(ctx.out, "</a>")?;
    375 		ctx.linelen += len(s);
    376 		return z;
    377 	case =>
    378 		return syn_html(ctx, s, kind);
    379 	};
    380 };
    381 
    382 fn syn_html(
    383 	ctx: *unparse::context,
    384 	s: str,
    385 	kind: unparse::synkind,
    386 ) (size | io::error) = {
    387 	let z = 0z;
    388 	const span = switch (kind) {
    389 	case unparse::synkind::COMMENT =>
    390 		const stack = ctx.stack as *unparse::stack;
    391 		if (stack.cur is *ast::decl) {
    392 			// doc comment is unparsed separately later
    393 			return 0z;
    394 		};
    395 		z += fmt::fprint(ctx.out, "<span class='comment'>")?;
    396 		yield true;
    397 	case unparse::synkind::KEYWORD =>
    398 		z += fmt::fprint(ctx.out, "<span class='keyword'>")?;
    399 		yield true;
    400 	case unparse::synkind::TYPE =>
    401 		z += fmt::fprint(ctx.out, "<span class='type'>")?;
    402 		yield true;
    403 	case =>
    404 		yield false;
    405 	};
    406 
    407 	z += html_escape(ctx.out, s)?;
    408 	ctx.linelen += len(s);
    409 
    410 	if (span) {
    411 		z += fmt::fprint(ctx.out, "</span>")?;
    412 	};
    413 	return z;
    414 };
    415 
    416 fn breadcrumb(ident: ast::ident) str = {
    417 	if (len(ident) == 0) {
    418 		return "";
    419 	};
    420 	let buf = memio::dynamic();
    421 	fmt::fprintf(&buf, "<a href='/'>stdlib</a> » ")!;
    422 	for (let i = 0z; i < len(ident) - 1; i += 1) {
    423 		let ipath = strings::join("/", ident[..i+1]...);
    424 		defer free(ipath);
    425 		fmt::fprintf(&buf, "<a href='/{}'>{}</a>::", ipath, ident[i])!;
    426 	};
    427 	fmt::fprint(&buf, ident[len(ident) - 1])!;
    428 	return memio::string(&buf)!;
    429 };
    430 
    431 const harriet_b64 = "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";
    432 
    433 fn head(ident: ast::ident) (void | error) = {
    434 	const id = unparse::identstr(ident);
    435 	defer free(id);
    436 
    437 	let breadcrumb = breadcrumb(ident);
    438 	defer free(breadcrumb);
    439 
    440 	const title =
    441 		if (len(id) == 0)
    442 			fmt::asprintf("Hare documentation")
    443 		else
    444 			fmt::asprintf("{} — Hare documentation", id);
    445 	defer free(title);
    446 
    447 	// TODO: Move bits to +embed?
    448 	fmt::printfln("<!doctype html>
    449 <html lang='en'>
    450 <meta charset='utf-8' />
    451 <meta name='viewport' content='width=device-width, initial-scale=1' />
    452 <title>{}</title>
    453 <link rel='icon' type='image/png' href='data:image/png;base64,{}'>", title, harriet_b64)?;
    454 	fmt::println("<style>
    455 body {
    456 	font-family: sans-serif;
    457 	line-height: 1.3;
    458 	margin: 0 auto;
    459 	padding: 0 1rem;
    460 }
    461 
    462 nav:not(#TableOfContents) {
    463 	max-width: calc(800px + 128px + 128px);
    464 	margin: 1rem auto 0;
    465 	display: grid;
    466 	grid-template-rows: auto auto 1fr;
    467 	grid-template-columns: auto 1fr;
    468 	grid-template-areas:
    469 		'logo header'
    470 		'logo nav'
    471 		'logo none';
    472 }
    473 
    474 nav:not(#TableOfContents) img {
    475 	grid-area: logo;
    476 }
    477 
    478 nav:not(#TableOfContents) h1 {
    479 	grid-area: header;
    480 	margin: 0;
    481 	padding: 0;
    482 }
    483 
    484 nav:not(#TableOfContents) ul {
    485 	grid-area: nav;
    486 	margin: 0.5rem 0 0 0;
    487 	padding: 0;
    488 	list-style: none;
    489 	display: flex;
    490 	flex-direction: row;
    491 	justify-content: left;
    492 	flex-wrap: wrap;
    493 }
    494 
    495 nav:not(#TableOfContents) li:not(:first-child) {
    496 	margin-left: 2rem;
    497 }
    498 
    499 #TableOfContents {
    500 	font-size: 1.1rem;
    501 }
    502 
    503 main {
    504 	padding: 0 128px;
    505 	max-width: 800px;
    506 	margin: 0 auto;
    507 
    508 }
    509 
    510 pre {
    511 	background-color: #eee;
    512 	padding: 0.25rem 1rem;
    513 	margin: 0 -1rem 1rem;
    514 	font-size: 1.2rem;
    515 	max-width: calc(100% + 1rem);
    516 	overflow-x: auto;
    517 }
    518 
    519 pre .keyword {
    520     color: #008;
    521 }
    522 
    523 pre .type {
    524 	color: #44F;
    525 }
    526 
    527 ol {
    528 	padding-left: 0;
    529 	list-style: none;
    530 }
    531 
    532 ol li {
    533 	padding-left: 0;
    534 }
    535 
    536 h2, h3, h4 {
    537 	display: flex;
    538 }
    539 
    540 h3 {
    541 	border-bottom: 1px solid #ccc;
    542 	padding-bottom: 0.25rem;
    543 }
    544 
    545 .invalid {
    546 	color: red;
    547 }
    548 
    549 .heading-body {
    550 	word-wrap: anywhere;
    551 }
    552 
    553 .heading-extra {
    554 	align-self: flex-end;
    555 	flex-grow: 1;
    556 	padding-left: 0.5rem;
    557 	text-align: right;
    558 	font-size: 0.8rem;
    559 	color: #444;
    560 }
    561 
    562 h4:target + pre {
    563 	background: #ddf;
    564 }
    565 
    566 details {
    567 	background: #eee;
    568 	margin: 1rem -1rem 1rem;
    569 }
    570 
    571 summary {
    572 	cursor: pointer;
    573 	padding: 0.5rem 1rem;
    574 }
    575 
    576 details pre {
    577 	margin: 0;
    578 }
    579 
    580 .comment {
    581 	color: #000;
    582 	font-weight: bold;
    583 }
    584 
    585 @media(max-width: 1000px) {
    586 	main {
    587 		padding: 0;
    588 	}
    589 }
    590 
    591 @media(prefers-color-scheme: dark) {
    592 	body {
    593 		background: #121415;
    594 		color: #e1dfdc;
    595 	}
    596 
    597 	img.mascot {
    598 		filter: invert(.92);
    599 	}
    600 
    601 	a {
    602 		color: #78bef8;
    603 	}
    604 
    605 	a:visited {
    606 		color: #48a7f5;
    607 	}
    608 
    609 	summary {
    610 		background: #16191c;
    611 	}
    612 
    613 	h3 {
    614 		border-bottom: solid #16191c;
    615 	}
    616 
    617 	h4:target + pre {
    618 		background: #162329;
    619 	}
    620 
    621 	pre {
    622 		background-color: #16191c;
    623 	}
    624 
    625 	pre .keyword {
    626 		color: #69f;
    627 	}
    628 
    629 	pre .type {
    630 		color: #3cf;
    631 	}
    632 
    633 	.comment {
    634 		color: #fff;
    635 	}
    636 
    637 	.heading-extra {
    638 		color: #9b9997;
    639 	}
    640 }
    641 </style>")?;
    642 	fmt::printfln("<nav>
    643 	<img src='data:image/png;base64,{}'
    644 		class='mascot'
    645 		alt='An inked drawing of the Hare mascot, a fuzzy rabbit'
    646 		width='128' height='128' />
    647 	<h1>Hare documentation</h1>
    648 	<ul>
    649 		<li>
    650 			<a href='https://harelang.org'>Home</a>
    651 		</li>", harriet_b64)?;
    652 	fmt::printf("<li>{}</li>", breadcrumb)?;
    653 	fmt::print("</ul>
    654 </nav>
    655 <main>")?;
    656 	return;
    657 };