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 "&"; 27 case '<' => 28 yield "<"; 29 case '>' => 30 yield ">"; 31 case '"' => 32 yield """; 33 case '\'' => 34 yield "'"; 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)! == ""hello world!""); 52 53 let sink = memio::dynamic(); 54 defer io::close(&sink)!; 55 html_escape(&sink, "<hello & 'world'!>")!; 56 assert(memio::string(&sink)! == "<hello & 'world'!>"); 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 };