34 |
35 |
35 static unsigned indentation; |
36 static unsigned indentation; |
36 static const char *tabs = " "; |
37 static const char *tabs = " "; |
37 static void indent(int change = 0) { |
38 static void indent(int change = 0) { |
38 indentation += change; |
39 indentation += change; |
39 assert(indentation >= 0); |
|
40 assert(indentation <= max_indentation); |
40 assert(indentation <= max_indentation); |
41 fwrite(tabs, 4, indentation, stdout); |
41 fwrite(tabs, 4, indentation, stdout); |
42 } |
42 } |
43 |
43 |
44 static std::string encode(const std::string &data) { |
44 static std::string encode(const std::string &data) { |
77 indentation++; |
80 indentation++; |
78 } else { |
81 } else { |
79 puts(R"(<!DOCTYPE html> |
82 puts(R"(<!DOCTYPE html> |
80 <html> |
83 <html> |
81 <head> |
84 <head> |
|
85 <meta charset="UTF-8"> |
82 <style> |
86 <style> |
83 table.heatmap { |
87 table.heatmap { |
84 table-layout: fixed; |
88 table-layout: fixed; |
85 border-collapse: collapse; |
89 border-collapse: collapse; |
86 font-family: sans-serif; |
90 font-family: sans-serif; |
87 } |
91 } |
|
92 |
88 table.heatmap td, table.heatmap th { |
93 table.heatmap td, table.heatmap th { |
89 text-align: center; |
94 text-align: center; |
90 border: solid 1px lightgray; |
95 border: solid 1px lightgray; |
91 height: 1.5em; |
96 height: 1.5em; |
92 } |
97 } |
|
98 |
93 table.heatmap td { |
99 table.heatmap td { |
94 border: solid 1px lightgray; |
100 border: solid 1px lightgray; |
95 width: 1.5em; |
101 width: 1.5em; |
96 height: 1.5em; |
102 height: 1.5em; |
97 } |
103 } |
|
104 |
|
105 table.heatmap td:hover, table.heatmap td.popup-open { |
|
106 filter: hue-rotate(90deg); |
|
107 } |
|
108 |
98 table.heatmap td.out-of-range { |
109 table.heatmap td.out-of-range { |
99 background-color: gray; |
110 background-color: gray; |
100 } |
111 } |
101 |
112 |
102 table.heatmap td.zero-commits { |
113 table.heatmap td.zero-commits { |
120 } |
131 } |
121 |
132 |
122 table.heatmap td.commit-spam { |
133 table.heatmap td.commit-spam { |
123 background-color: #008000; |
134 background-color: #008000; |
124 } |
135 } |
|
136 |
|
137 /* Popup styles */ |
|
138 .commit-popup { |
|
139 position: absolute; |
|
140 background-color: #fff; |
|
141 border: 1px solid #ccc; |
|
142 border-radius: 4px; |
|
143 box-shadow: 0 2px 10px rgba(0,0,0,0.2); |
|
144 padding: .2rem; |
|
145 z-index: 1000; |
|
146 width: 40ch; |
|
147 font-family: sans-serif; |
|
148 font-size: smaller; |
|
149 display: none; |
|
150 } |
|
151 |
|
152 .commit-popup h3 { |
|
153 margin-top: 0; |
|
154 border-bottom: 1px solid #eee; |
|
155 padding-bottom: 5px; |
|
156 } |
|
157 |
|
158 .commit-popup ul { |
|
159 margin: 0; |
|
160 padding-left: 20px; |
|
161 } |
|
162 |
|
163 .commit-popup li { |
|
164 margin-bottom: 5px; |
|
165 } |
|
166 |
|
167 .commit-popup .close-btn { |
|
168 position: absolute; |
|
169 top: 5px; |
|
170 right: 8px; |
|
171 cursor: pointer; |
|
172 font-weight: bold; |
|
173 } |
125 </style> |
174 </style> |
|
175 <script> |
|
176 document.addEventListener('DOMContentLoaded', function() { |
|
177 // Create popup element |
|
178 const popup = document.createElement('div'); |
|
179 popup.className = 'commit-popup'; |
|
180 document.body.appendChild(popup); |
|
181 |
|
182 // Add click event listeners to all commit cells |
|
183 const cells = document.querySelectorAll('table.heatmap td:not(.out-of-range, .zero-commits)'); |
|
184 cells.forEach(cell => { |
|
185 cell.addEventListener('click', function(e) { |
|
186 const date = this.dataset.date; |
|
187 const summaries = JSON.parse(this.dataset.summaries); |
|
188 |
|
189 // Create popup content |
|
190 let content = `<span class="close-btn">×</span> |
|
191 <h3>${date}: ${summaries.length} commit${summaries.length !== '1' ? 's' : ''}</h3>`; |
|
192 content += '<ul>'; |
|
193 summaries.forEach(summary => { |
|
194 content += `<li>${summary}</li>`; |
|
195 }); |
|
196 content += '</ul>'; |
|
197 popup.innerHTML = content; |
|
198 |
|
199 // Position popup near the cell |
|
200 const rect = this.getBoundingClientRect(); |
|
201 popup.style.left = rect.left + window.scrollX + 'px'; |
|
202 popup.style.top = (rect.bottom + window.scrollY + 5) + 'px'; |
|
203 popup.style.display = 'block'; |
|
204 |
|
205 // Highlight the cell to which the popup belongs |
|
206 document.querySelectorAll('table.heatmap td.popup-open').forEach(old_cell => { |
|
207 old_cell.classList.toggle("popup-open"); |
|
208 }); |
|
209 cell.classList.toggle("popup-open"); |
|
210 |
|
211 // Add close button handler |
|
212 const closeBtn = popup.querySelector('.close-btn'); |
|
213 if (closeBtn) { |
|
214 closeBtn.addEventListener('click', function() { |
|
215 popup.style.display = 'none'; |
|
216 cell.classList.toggle("popup-open"); |
|
217 }); |
|
218 } |
|
219 |
|
220 e.stopPropagation(); |
|
221 }); |
|
222 }); |
|
223 |
|
224 // Close popup when clicking elsewhere |
|
225 document.addEventListener('click', function(e) { |
|
226 if (!popup.contains(e.target)) { |
|
227 popup.style.display = 'none'; |
|
228 document.querySelectorAll('table.heatmap td.popup-open').forEach(cell => { |
|
229 cell.classList.toggle("popup-open"); |
|
230 }); |
|
231 } |
|
232 }); |
|
233 }); |
|
234 </script> |
126 </head> |
235 </head> |
127 <body> |
236 <body> |
128 <div class="heatmap-content">)"); |
237 <div class="heatmap-content">)"); |
129 indentation = 3; |
238 indentation = 3; |
130 } |
239 } |
215 void html::cell_out_of_range() { |
324 void html::cell_out_of_range() { |
216 indent(); |
325 indent(); |
217 puts("<td class=\"out-of-range\"></td>"); |
326 puts("<td class=\"out-of-range\"></td>"); |
218 } |
327 } |
219 |
328 |
220 void html::cell(year_month_day ymd, unsigned commits) { |
329 void html::cell(year_month_day ymd, const fm::commits &commits) { |
221 const char *color_class; |
330 const char *color_class; |
222 if (commits == 0) { |
331 if (commits.count() == 0) { |
223 color_class = "zero-commits"; |
332 color_class = "zero-commits"; |
224 } else if (commits == 1) { |
333 } else if (commits.count() == 1) { |
225 color_class = "one-commit"; |
334 color_class = "one-commit"; |
226 } else if (commits <= 5) { |
335 } else if (commits.count() <= 5) { |
227 color_class = "up-to-5-commits"; |
336 color_class = "up-to-5-commits"; |
228 } else if (commits <= 10) { |
337 } else if (commits.count() <= 10) { |
229 color_class = "up-to-10-commits"; |
338 color_class = "up-to-10-commits"; |
230 } else if (commits <= 20) { |
339 } else if (commits.count() <= 20) { |
231 color_class = "up-to-20-commits"; |
340 color_class = "up-to-20-commits"; |
232 } else { |
341 } else { |
233 color_class = "commit-spam"; |
342 color_class = "commit-spam"; |
234 } |
343 } |
235 indent(); |
344 |
236 printf("<td class=\"%s\" title=\"%s, %d-%02u-%02u: %u %s\"></td>\n", |
345 |
237 color_class, |
346 // Format date for display |
|
347 char date_str[32]; |
|
348 sprintf(date_str, "%s, %d-%02u-%02u", |
238 weekdays[weekday(ymd).iso_encoding() - 1], |
349 weekdays[weekday(ymd).iso_encoding() - 1], |
239 static_cast<int>(ymd.year()), |
350 static_cast<int>(ymd.year()), |
240 static_cast<unsigned>(ymd.month()), |
351 static_cast<unsigned>(ymd.month()), |
241 static_cast<unsigned>(ymd.day()), |
352 static_cast<unsigned>(ymd.day())); |
242 commits, |
353 |
243 commits == 1 ? "commit" : "commits"); |
354 // Build a JSON array of commit summaries |
244 } |
355 // We have to iterate in reverse order to sort the summaries chronologically |
|
356 std::string summaries_json = "["; |
|
357 for (const auto &summary : commits.summaries | std::views::reverse) { |
|
358 // Escape quotes in JSON |
|
359 size_t pos = summary.find('\"'); |
|
360 if (pos == std::string::npos) { |
|
361 summaries_json += "\"" + summary + "\","; |
|
362 } else { |
|
363 std::string escaped = summary; |
|
364 do { |
|
365 escaped.replace(pos, 1, "\\\""); |
|
366 pos += 2; |
|
367 } while ((pos = escaped.find('\"', pos)) != std::string::npos); |
|
368 summaries_json += "\"" + escaped + "\","; |
|
369 } |
|
370 } |
|
371 summaries_json.pop_back(); |
|
372 summaries_json += "]"; |
|
373 |
|
374 indent(); |
|
375 printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n", |
|
376 color_class, |
|
377 date_str, |
|
378 commits.count(), |
|
379 commits.count() == 1 ? "commit" : "commits", |
|
380 date_str, |
|
381 encode(summaries_json).c_str() |
|
382 ); |
|
383 } |