src/html.cpp

changeset 81
1ff88eb9555c
parent 76
110a234a3260
--- a/src/html.cpp	Fri Feb 27 14:38:01 2026 +0100
+++ b/src/html.cpp	Thu Mar 12 12:00:50 2026 +0100
@@ -27,18 +27,42 @@
 #include <ranges>
 #include <cstdio>
 #include <cassert>
+#include <iostream>
 
 using namespace std::chrono;
 
 namespace html {
     static constexpr const char* weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
 
+    struct output_reference {
+        FILE *file;
+        explicit output_reference(FILE *f) : file{f} {}
+        output_reference(output_reference const &) = delete;
+        output_reference(output_reference && other) noexcept : file(other.file) {
+            other.file = nullptr;
+        }
+        output_reference &operator=(output_reference const &) = delete;
+        output_reference &operator=(output_reference && other) noexcept {
+            using std::swap;
+            swap(other.file, file);
+            return *this;
+        }
+        ~output_reference() {
+            if (file != nullptr && file != stdout) {
+                fclose(file);
+                file = nullptr;
+            }
+        }
+    };
+
+    static output_reference output{stdout};
+
     static unsigned indentation;
     static const char *tabs = "                                                                ";
     static void indent(int change = 0) {
         indentation += change;
         assert(indentation <= max_indentation);
-        fwrite(tabs, 4, indentation, stdout);
+        fwrite(tabs, 4, indentation, output.file);
     }
 
     static std::string encode(const std::string &data) {
@@ -158,8 +182,21 @@
     }
 }
 
+int html::set_output(std::string const &path) {
+    if (path.empty()) {
+        output = output_reference{stdout};
+        return 0;
+    }
+    FILE * f = fopen(path.c_str(), "w");
+    if (f == nullptr) {
+        return -1;
+    }
+    output = output_reference{f};
+    return 0;
+}
+
 void html::styles_and_script() {
-    puts(R"(        <style>
+    fputs(R"(        <style>
             table.heatmap {
                 table-layout: fixed;
                 border-collapse: separate;
@@ -387,23 +424,27 @@
                     }
                 });
             });
-        </script>)");
+        </script>
+)", output.file);
 }
 
 void html::open(bool fragment, unsigned char fragment_indent) {
+    indentation = 0;
     if (fragment) {
         indent(fragment_indent);
-        puts("<div class=\"heatmap-content\">");
+        fputs("<div class=\"heatmap-content\">\n", output.file);
         indentation++;
     } else {
-        puts(R"(<!DOCTYPE html>
+        fputs(R"(<!DOCTYPE html>
 <html>
     <head>
-        <meta charset="UTF-8">)");
+        <meta charset="UTF-8">
+)", output.file);
         styles_and_script();
-        puts(R"(    </head>
+        fputs(R"(    </head>
     <body>
-        <div class="heatmap-content">)");
+        <div class="heatmap-content">
+)", output.file);
         indentation = 3;
     }
 }
@@ -411,32 +452,33 @@
 void html::close(bool fragment) {
     if (fragment) {
         indent(-1);
-        puts("</div>");
+        fputs("</div>\n", output.file);
     } else {
-        puts("        </div>\n    </body>\n</html>");
+        fputs("        </div>\n    </body>\n</html>\n", output.file);
     }
 }
 
 void html::chart_begin(const std::string& repo, const std::string& author) {
     indent();
-    printf("<div class=\"chart\" data-repo=\"%s\" data-author=\"%s\">\n",
+    fprintf(output.file,
+        "<div class=\"chart\" data-repo=\"%s\" data-author=\"%s\">\n",
         encode(repo).c_str(), encode(author).c_str());
     indentation++;
 }
 
 void html::chart_end() {
     indent(-1);
-    puts("</div>");
+    fputs("</div>\n", output.file);
 }
 
 void html::heading_repo(const std::string& repo) {
     indent();
-    printf("<h1 data-repo=\"%1$s\">%1$s</h1>\n", encode(repo).c_str());
+    fprintf(output.file, "<h1 data-repo=\"%1$s\">%1$s</h1>\n", encode(repo).c_str());
 }
 
 void html::heading_author(const std::string& author, unsigned int total_commits) {
     indent();
-    printf("<h2 title=\"Total commits: %u\">%s</h2>\n",
+    fprintf(output.file, "<h2 title=\"Total commits: %u\">%s</h2>\n",
         total_commits,
         encode(author).c_str());
 }
@@ -459,11 +501,11 @@
 
     // now render the table heading
     indent();
-    puts("<table class=\"heatmap\">");
+    fputs("<table class=\"heatmap\">\n", output.file);
     indent(1);
-    puts("<tr>");
+    fputs("<tr>\n", output.file);
     indent(1);
-    puts("<th></th>");
+    fputs("<th></th>\n", output.file);
     for (unsigned i = 0 ; i < 12 ; i++) {
         indent();
         const fm::commit_summary &summary = commits_per_month[i];
@@ -483,39 +525,41 @@
                 commit_summary = std::format("data-commits=\"{}\"", encode(commit_summary_json));
             }
             std::string tags = build_tag_summary(summary.tags_with_date, hide_repo_names);
-            printf("<th scope=\"col\" title=\"Total commits: %u\" colspan=\"%d\" data-total=\"%u\" %s data-tags=\"%s\">%s</th>\n",
+            fprintf(output.file,
+                "<th scope=\"col\" title=\"Total commits: %u\" colspan=\"%d\" data-total=\"%u\" %s data-tags=\"%s\">%s</th>\n",
                 total, colspans[i], total,
                 commit_summary.c_str(),
                 encode(tags).c_str(), months[i]);
         } else {
-            printf("<th scope=\"col\" class=\"zero-commits\" colspan=\"%d\">%s</th>\n",
+            fprintf(output.file,
+                "<th scope=\"col\" class=\"zero-commits\" colspan=\"%d\">%s</th>\n",
                 colspans[i], months[i]);
         }
     }
     indent(-1);
-    puts("</tr>");
+    fputs("</tr>\n", output.file);
 }
 
 void html::table_end() {
     indent(-1);
-    puts("</table>");
+    fputs("</table>\n", output.file);
 }
 
 void html::row_begin(unsigned int row) {
     indent();
-    puts("<tr>");
+    fputs("<tr>\n", output.file);
     indent(1);
-    printf("<th scope=\"row\">%s</th>\n", weekdays[row]);
+    fprintf(output.file, "<th scope=\"row\">%s</th>\n", weekdays[row]);
 }
 
 void html::row_end() {
     indent(-1);
-    puts("</tr>");
+    fputs("</tr>\n", output.file);
 }
 
 void html::cell_out_of_range() {
     indent();
-    puts("<td class=\"out-of-range\"></td>");
+    fputs("<td class=\"out-of-range\"></td>\n", output.file);
 }
 
 void html::cell(year_month_day ymd, bool hide_repo_names, const fm::commits &commits) {
@@ -573,7 +617,8 @@
         static_cast<unsigned>(ymd.day()));
     const unsigned total = commits.count();
     indent();
-    printf("<td class=\"%s\" title=\"%s: %u %s\" data-total=\"%u\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n",
+    fprintf(output.file,
+        "<td class=\"%s\" title=\"%s: %u %s\" data-total=\"%u\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n",
         color_class,
         date_str.c_str(),
         total,

mercurial