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 "&"; 37 case '<' => 38 yield "<"; 39 case '>' => 40 yield ">"; 41 case '"' => 42 yield """; 43 case '\'' => 44 yield "'"; 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)! == ""hello world!""); 63 64 let sink = memio::dynamic(); 65 defer io::close(&sink)!; 66 html_escape(&sink, "<hello & 'world'!>")!; 67 assert(memio::string(&sink)! == "<hello & 'world'!>"); 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 };