| 25 #include "html.h" |
25 #include "html.h" |
| 26 |
26 |
| 27 #include <ranges> |
27 #include <ranges> |
| 28 #include <cstdio> |
28 #include <cstdio> |
| 29 #include <cassert> |
29 #include <cassert> |
| |
30 #include <iostream> |
| 30 |
31 |
| 31 using namespace std::chrono; |
32 using namespace std::chrono; |
| 32 |
33 |
| 33 namespace html { |
34 namespace html { |
| 34 static constexpr const char* weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; |
35 static constexpr const char* weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; |
| |
36 |
| |
37 struct output_reference { |
| |
38 FILE *file; |
| |
39 explicit output_reference(FILE *f) : file{f} {} |
| |
40 output_reference(output_reference const &) = delete; |
| |
41 output_reference(output_reference && other) noexcept : file(other.file) { |
| |
42 other.file = nullptr; |
| |
43 } |
| |
44 output_reference &operator=(output_reference const &) = delete; |
| |
45 output_reference &operator=(output_reference && other) noexcept { |
| |
46 using std::swap; |
| |
47 swap(other.file, file); |
| |
48 return *this; |
| |
49 } |
| |
50 ~output_reference() { |
| |
51 if (file != nullptr && file != stdout) { |
| |
52 fclose(file); |
| |
53 file = nullptr; |
| |
54 } |
| |
55 } |
| |
56 }; |
| |
57 |
| |
58 static output_reference output{stdout}; |
| 35 |
59 |
| 36 static unsigned indentation; |
60 static unsigned indentation; |
| 37 static const char *tabs = " "; |
61 static const char *tabs = " "; |
| 38 static void indent(int change = 0) { |
62 static void indent(int change = 0) { |
| 39 indentation += change; |
63 indentation += change; |
| 40 assert(indentation <= max_indentation); |
64 assert(indentation <= max_indentation); |
| 41 fwrite(tabs, 4, indentation, stdout); |
65 fwrite(tabs, 4, indentation, output.file); |
| 42 } |
66 } |
| 43 |
67 |
| 44 static std::string encode(const std::string &data) { |
68 static std::string encode(const std::string &data) { |
| 45 std::string buffer; |
69 std::string buffer; |
| 46 buffer.reserve(data.size()+16); |
70 buffer.reserve(data.size()+16); |
| 385 cell.classList.toggle("popup-open"); |
422 cell.classList.toggle("popup-open"); |
| 386 }); |
423 }); |
| 387 } |
424 } |
| 388 }); |
425 }); |
| 389 }); |
426 }); |
| 390 </script>)"); |
427 </script> |
| |
428 )", output.file); |
| 391 } |
429 } |
| 392 |
430 |
| 393 void html::open(bool fragment, unsigned char fragment_indent) { |
431 void html::open(bool fragment, unsigned char fragment_indent) { |
| |
432 indentation = 0; |
| 394 if (fragment) { |
433 if (fragment) { |
| 395 indent(fragment_indent); |
434 indent(fragment_indent); |
| 396 puts("<div class=\"heatmap-content\">"); |
435 fputs("<div class=\"heatmap-content\">\n", output.file); |
| 397 indentation++; |
436 indentation++; |
| 398 } else { |
437 } else { |
| 399 puts(R"(<!DOCTYPE html> |
438 fputs(R"(<!DOCTYPE html> |
| 400 <html> |
439 <html> |
| 401 <head> |
440 <head> |
| 402 <meta charset="UTF-8">)"); |
441 <meta charset="UTF-8"> |
| |
442 )", output.file); |
| 403 styles_and_script(); |
443 styles_and_script(); |
| 404 puts(R"( </head> |
444 fputs(R"( </head> |
| 405 <body> |
445 <body> |
| 406 <div class="heatmap-content">)"); |
446 <div class="heatmap-content"> |
| |
447 )", output.file); |
| 407 indentation = 3; |
448 indentation = 3; |
| 408 } |
449 } |
| 409 } |
450 } |
| 410 |
451 |
| 411 void html::close(bool fragment) { |
452 void html::close(bool fragment) { |
| 412 if (fragment) { |
453 if (fragment) { |
| 413 indent(-1); |
454 indent(-1); |
| 414 puts("</div>"); |
455 fputs("</div>\n", output.file); |
| 415 } else { |
456 } else { |
| 416 puts(" </div>\n </body>\n</html>"); |
457 fputs(" </div>\n </body>\n</html>\n", output.file); |
| 417 } |
458 } |
| 418 } |
459 } |
| 419 |
460 |
| 420 void html::chart_begin(const std::string& repo, const std::string& author) { |
461 void html::chart_begin(const std::string& repo, const std::string& author) { |
| 421 indent(); |
462 indent(); |
| 422 printf("<div class=\"chart\" data-repo=\"%s\" data-author=\"%s\">\n", |
463 fprintf(output.file, |
| |
464 "<div class=\"chart\" data-repo=\"%s\" data-author=\"%s\">\n", |
| 423 encode(repo).c_str(), encode(author).c_str()); |
465 encode(repo).c_str(), encode(author).c_str()); |
| 424 indentation++; |
466 indentation++; |
| 425 } |
467 } |
| 426 |
468 |
| 427 void html::chart_end() { |
469 void html::chart_end() { |
| 428 indent(-1); |
470 indent(-1); |
| 429 puts("</div>"); |
471 fputs("</div>\n", output.file); |
| 430 } |
472 } |
| 431 |
473 |
| 432 void html::heading_repo(const std::string& repo) { |
474 void html::heading_repo(const std::string& repo) { |
| 433 indent(); |
475 indent(); |
| 434 printf("<h1 data-repo=\"%1$s\">%1$s</h1>\n", encode(repo).c_str()); |
476 fprintf(output.file, "<h1 data-repo=\"%1$s\">%1$s</h1>\n", encode(repo).c_str()); |
| 435 } |
477 } |
| 436 |
478 |
| 437 void html::heading_author(const std::string& author, unsigned int total_commits) { |
479 void html::heading_author(const std::string& author, unsigned int total_commits) { |
| 438 indent(); |
480 indent(); |
| 439 printf("<h2 title=\"Total commits: %u\">%s</h2>\n", |
481 fprintf(output.file, "<h2 title=\"Total commits: %u\">%s</h2>\n", |
| 440 total_commits, |
482 total_commits, |
| 441 encode(author).c_str()); |
483 encode(author).c_str()); |
| 442 } |
484 } |
| 443 |
485 |
| 444 void html::table_begin(year y, bool hide_repo_names, const std::array<fm::commit_summary, 12> &commits_per_month) { |
486 void html::table_begin(year y, bool hide_repo_names, const std::array<fm::commit_summary, 12> &commits_per_month) { |
| 457 } |
499 } |
| 458 } |
500 } |
| 459 |
501 |
| 460 // now render the table heading |
502 // now render the table heading |
| 461 indent(); |
503 indent(); |
| 462 puts("<table class=\"heatmap\">"); |
504 fputs("<table class=\"heatmap\">\n", output.file); |
| 463 indent(1); |
505 indent(1); |
| 464 puts("<tr>"); |
506 fputs("<tr>\n", output.file); |
| 465 indent(1); |
507 indent(1); |
| 466 puts("<th></th>"); |
508 fputs("<th></th>\n", output.file); |
| 467 for (unsigned i = 0 ; i < 12 ; i++) { |
509 for (unsigned i = 0 ; i < 12 ; i++) { |
| 468 indent(); |
510 indent(); |
| 469 const fm::commit_summary &summary = commits_per_month[i]; |
511 const fm::commit_summary &summary = commits_per_month[i]; |
| 470 const unsigned total = summary.count(); |
512 const unsigned total = summary.count(); |
| 471 if (total > 0) { |
513 if (total > 0) { |
| 481 } |
523 } |
| 482 commit_summary_json += '}'; |
524 commit_summary_json += '}'; |
| 483 commit_summary = std::format("data-commits=\"{}\"", encode(commit_summary_json)); |
525 commit_summary = std::format("data-commits=\"{}\"", encode(commit_summary_json)); |
| 484 } |
526 } |
| 485 std::string tags = build_tag_summary(summary.tags_with_date, hide_repo_names); |
527 std::string tags = build_tag_summary(summary.tags_with_date, hide_repo_names); |
| 486 printf("<th scope=\"col\" title=\"Total commits: %u\" colspan=\"%d\" data-total=\"%u\" %s data-tags=\"%s\">%s</th>\n", |
528 fprintf(output.file, |
| |
529 "<th scope=\"col\" title=\"Total commits: %u\" colspan=\"%d\" data-total=\"%u\" %s data-tags=\"%s\">%s</th>\n", |
| 487 total, colspans[i], total, |
530 total, colspans[i], total, |
| 488 commit_summary.c_str(), |
531 commit_summary.c_str(), |
| 489 encode(tags).c_str(), months[i]); |
532 encode(tags).c_str(), months[i]); |
| 490 } else { |
533 } else { |
| 491 printf("<th scope=\"col\" class=\"zero-commits\" colspan=\"%d\">%s</th>\n", |
534 fprintf(output.file, |
| |
535 "<th scope=\"col\" class=\"zero-commits\" colspan=\"%d\">%s</th>\n", |
| 492 colspans[i], months[i]); |
536 colspans[i], months[i]); |
| 493 } |
537 } |
| 494 } |
538 } |
| 495 indent(-1); |
539 indent(-1); |
| 496 puts("</tr>"); |
540 fputs("</tr>\n", output.file); |
| 497 } |
541 } |
| 498 |
542 |
| 499 void html::table_end() { |
543 void html::table_end() { |
| 500 indent(-1); |
544 indent(-1); |
| 501 puts("</table>"); |
545 fputs("</table>\n", output.file); |
| 502 } |
546 } |
| 503 |
547 |
| 504 void html::row_begin(unsigned int row) { |
548 void html::row_begin(unsigned int row) { |
| 505 indent(); |
549 indent(); |
| 506 puts("<tr>"); |
550 fputs("<tr>\n", output.file); |
| 507 indent(1); |
551 indent(1); |
| 508 printf("<th scope=\"row\">%s</th>\n", weekdays[row]); |
552 fprintf(output.file, "<th scope=\"row\">%s</th>\n", weekdays[row]); |
| 509 } |
553 } |
| 510 |
554 |
| 511 void html::row_end() { |
555 void html::row_end() { |
| 512 indent(-1); |
556 indent(-1); |
| 513 puts("</tr>"); |
557 fputs("</tr>\n", output.file); |
| 514 } |
558 } |
| 515 |
559 |
| 516 void html::cell_out_of_range() { |
560 void html::cell_out_of_range() { |
| 517 indent(); |
561 indent(); |
| 518 puts("<td class=\"out-of-range\"></td>"); |
562 fputs("<td class=\"out-of-range\"></td>\n", output.file); |
| 519 } |
563 } |
| 520 |
564 |
| 521 void html::cell(year_month_day ymd, bool hide_repo_names, const fm::commits &commits) { |
565 void html::cell(year_month_day ymd, bool hide_repo_names, const fm::commits &commits) { |
| 522 const char *color_class; |
566 const char *color_class; |
| 523 if (commits.count() == 0) { |
567 if (commits.count() == 0) { |
| 571 static_cast<int>(ymd.year()), |
615 static_cast<int>(ymd.year()), |
| 572 static_cast<unsigned>(ymd.month()), |
616 static_cast<unsigned>(ymd.month()), |
| 573 static_cast<unsigned>(ymd.day())); |
617 static_cast<unsigned>(ymd.day())); |
| 574 const unsigned total = commits.count(); |
618 const unsigned total = commits.count(); |
| 575 indent(); |
619 indent(); |
| 576 printf("<td class=\"%s\" title=\"%s: %u %s\" data-total=\"%u\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n", |
620 fprintf(output.file, |
| |
621 "<td class=\"%s\" title=\"%s: %u %s\" data-total=\"%u\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n", |
| 577 color_class, |
622 color_class, |
| 578 date_str.c_str(), |
623 date_str.c_str(), |
| 579 total, |
624 total, |
| 580 total == 1 ? "commit" : "commits", |
625 total == 1 ? "commit" : "commits", |
| 581 total, |
626 total, |