src/html.cpp

changeset 54
586dcd606e47
parent 52
e9edc3bd0301
--- a/src/html.cpp	Fri Jun 20 17:15:18 2025 +0200
+++ b/src/html.cpp	Sat Jun 28 11:32:08 2025 +0200
@@ -24,6 +24,7 @@
 
 #include "html.h"
 
+#include <ranges>
 #include <cstdio>
 #include <cassert>
 
@@ -36,7 +37,6 @@
     static const char *tabs = "                                                                ";
     static void indent(int change = 0) {
         indentation += change;
-        assert(indentation >= 0);
         assert(indentation <= max_indentation);
         fwrite(tabs, 4, indentation, stdout);
     }
@@ -61,6 +61,9 @@
                 case '>':
                     buffer.append("&gt;");
                     break;
+                case '#':
+                    buffer.append("&#35;");
+                    break;
                 default:
                     buffer.append(&pos, 1);
                     break;
@@ -79,22 +82,30 @@
         puts(R"(<!DOCTYPE html>
 <html>
     <head>
+        <meta charset="UTF-8">
         <style>
             table.heatmap {
                 table-layout: fixed;
                 border-collapse: collapse;
                 font-family: sans-serif;
             }
+
             table.heatmap td, table.heatmap th {
                 text-align: center;
                 border: solid 1px lightgray;
                 height: 1.5em;
             }
+
             table.heatmap td {
                 border: solid 1px lightgray;
                 width: 1.5em;
                 height: 1.5em;
             }
+
+            table.heatmap td:hover, table.heatmap td.popup-open {
+                filter: hue-rotate(90deg);
+            }
+
             table.heatmap td.out-of-range {
                 background-color: gray;
             }
@@ -122,7 +133,105 @@
             table.heatmap td.commit-spam {
                 background-color: #008000;
             }
+
+            /* Popup styles */
+            .commit-popup {
+                position: absolute;
+                background-color: #fff;
+                border: 1px solid #ccc;
+                border-radius: 4px;
+                box-shadow: 0 2px 10px rgba(0,0,0,0.2);
+                padding: .2rem;
+                z-index: 1000;
+                width: 40ch;
+                font-family: sans-serif;
+                font-size: smaller;
+                display: none;
+            }
+
+            .commit-popup h3 {
+                margin-top: 0;
+                border-bottom: 1px solid #eee;
+                padding-bottom: 5px;
+            }
+
+            .commit-popup ul {
+                margin: 0;
+                padding-left: 20px;
+            }
+
+            .commit-popup li {
+                margin-bottom: 5px;
+            }
+
+            .commit-popup .close-btn {
+                position: absolute;
+                top: 5px;
+                right: 8px;
+                cursor: pointer;
+                font-weight: bold;
+            }
         </style>
+        <script>
+            document.addEventListener('DOMContentLoaded', function() {
+                // Create popup element
+                const popup = document.createElement('div');
+                popup.className = 'commit-popup';
+                document.body.appendChild(popup);
+
+                // Add click event listeners to all commit cells
+                const cells = document.querySelectorAll('table.heatmap td:not(.out-of-range, .zero-commits)');
+                cells.forEach(cell => {
+                    cell.addEventListener('click', function(e) {
+                        const date = this.dataset.date;
+                        const summaries = JSON.parse(this.dataset.summaries);
+
+                        // Create popup content
+                        let content = `<span class="close-btn">×</span>
+                                      <h3>${date}: ${summaries.length} commit${summaries.length !== '1' ? 's' : ''}</h3>`;
+                        content += '<ul>';
+                        summaries.forEach(summary => {
+                            content += `<li>${summary}</li>`;
+                        });
+                        content += '</ul>';
+                        popup.innerHTML = content;
+
+                        // Position popup near the cell
+                        const rect = this.getBoundingClientRect();
+                        popup.style.left = rect.left + window.scrollX + 'px';
+                        popup.style.top = (rect.bottom + window.scrollY + 5) + 'px';
+                        popup.style.display = 'block';
+
+                        // Highlight the cell to which the popup belongs
+                        document.querySelectorAll('table.heatmap td.popup-open').forEach(old_cell => {
+                            old_cell.classList.toggle("popup-open");
+                        });
+                        cell.classList.toggle("popup-open");
+
+                        // Add close button handler
+                        const closeBtn = popup.querySelector('.close-btn');
+                        if (closeBtn) {
+                            closeBtn.addEventListener('click', function() {
+                                popup.style.display = 'none';
+                                cell.classList.toggle("popup-open");
+                            });
+                        }
+
+                        e.stopPropagation();
+                    });
+                });
+
+                // Close popup when clicking elsewhere
+                document.addEventListener('click', function(e) {
+                    if (!popup.contains(e.target)) {
+                        popup.style.display = 'none';
+                        document.querySelectorAll('table.heatmap td.popup-open').forEach(cell => {
+                            cell.classList.toggle("popup-open");
+                        });
+                    }
+                });
+            });
+        </script>
     </head>
     <body>
         <div class="heatmap-content">)");
@@ -217,28 +326,58 @@
     puts("<td class=\"out-of-range\"></td>");
 }
 
-void html::cell(year_month_day ymd, unsigned commits) {
+void html::cell(year_month_day ymd, const fm::commits &commits) {
     const char *color_class;
-    if (commits == 0) {
+    if (commits.count() == 0) {
         color_class = "zero-commits";
-    } else if (commits == 1) {
+    } else if (commits.count() == 1) {
         color_class = "one-commit";
-    } else if (commits <= 5) {
+    } else if (commits.count() <= 5) {
         color_class = "up-to-5-commits";
-    } else if (commits <= 10) {
+    } else if (commits.count() <= 10) {
         color_class = "up-to-10-commits";
-    } else if (commits <= 20) {
+    } else if (commits.count() <= 20) {
         color_class = "up-to-20-commits";
     } else {
         color_class = "commit-spam";
     }
-    indent();
-    printf("<td class=\"%s\" title=\"%s, %d-%02u-%02u: %u %s\"></td>\n",
-        color_class,
+
+
+    // Format date for display
+    char date_str[32];
+    sprintf(date_str, "%s, %d-%02u-%02u",
         weekdays[weekday(ymd).iso_encoding() - 1],
         static_cast<int>(ymd.year()),
         static_cast<unsigned>(ymd.month()),
-        static_cast<unsigned>(ymd.day()),
-        commits,
-        commits == 1 ? "commit" : "commits");
+        static_cast<unsigned>(ymd.day()));
+
+    // Build a JSON array of commit summaries
+    // We have to iterate in reverse order to sort the summaries chronologically
+    std::string summaries_json = "[";
+    for (const auto &summary :  commits.summaries | std::views::reverse) {
+        // Escape quotes in JSON
+        size_t pos = summary.find('\"');
+        if (pos == std::string::npos) {
+            summaries_json += "\"" + summary + "\",";
+        } else {
+            std::string escaped = summary;
+            do {
+                escaped.replace(pos, 1, "\\\"");
+                pos += 2;
+            } while ((pos = escaped.find('\"', pos)) != std::string::npos);
+            summaries_json += "\"" + escaped + "\",";
+        }
+    }
+    summaries_json.pop_back();
+    summaries_json += "]";
+
+    indent();
+    printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n",
+        color_class,
+        date_str,
+        commits.count(),
+        commits.count() == 1 ? "commit" : "commits",
+        date_str,
+        encode(summaries_json).c_str()
+    );
 }

mercurial