src/html.cpp

changeset 81
1ff88eb9555c
parent 76
110a234a3260
equal deleted inserted replaced
80:fa5f493adfb5 81:1ff88eb9555c
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);
156 } 180 }
157 return tags_json; 181 return tags_json;
158 } 182 }
159 } 183 }
160 184
185 int html::set_output(std::string const &path) {
186 if (path.empty()) {
187 output = output_reference{stdout};
188 return 0;
189 }
190 FILE * f = fopen(path.c_str(), "w");
191 if (f == nullptr) {
192 return -1;
193 }
194 output = output_reference{f};
195 return 0;
196 }
197
161 void html::styles_and_script() { 198 void html::styles_and_script() {
162 puts(R"( <style> 199 fputs(R"( <style>
163 table.heatmap { 200 table.heatmap {
164 table-layout: fixed; 201 table-layout: fixed;
165 border-collapse: separate; 202 border-collapse: separate;
166 border-spacing: 2px; 203 border-spacing: 2px;
167 font-family: sans-serif; 204 font-family: sans-serif;
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,

mercurial