84 <head> |
84 <head> |
85 <meta charset="UTF-8"> |
85 <meta charset="UTF-8"> |
86 <style> |
86 <style> |
87 table.heatmap { |
87 table.heatmap { |
88 table-layout: fixed; |
88 table-layout: fixed; |
89 border-collapse: collapse; |
89 border-collapse: separate; |
|
90 border-spacing: 4px; |
90 font-family: sans-serif; |
91 font-family: sans-serif; |
91 } |
92 } |
92 |
93 |
93 table.heatmap td, table.heatmap th { |
94 table.heatmap td, table.heatmap th { |
94 text-align: center; |
95 text-align: center; |
95 border: solid 1px lightgray; |
96 font-size: 0.75rem; |
96 height: 1.5em; |
97 border: solid 2px transparent; |
|
98 border-radius: 4px; |
|
99 height: 1rem; |
97 } |
100 } |
98 |
101 |
99 table.heatmap td { |
102 table.heatmap td { |
100 border: solid 1px lightgray; |
103 width: 1rem; |
101 width: 1.5em; |
|
102 height: 1.5em; |
|
103 } |
104 } |
104 |
105 |
105 table.heatmap td:hover, table.heatmap td.popup-open { |
106 table.heatmap td:hover, table.heatmap td.popup-open { |
106 filter: hue-rotate(90deg); |
107 filter: hue-rotate(90deg); |
107 } |
108 } |
189 |
203 |
190 // Create popup content |
204 // Create popup content |
191 let content = '<span class="close-btn">×</span>'; |
205 let content = '<span class="close-btn">×</span>'; |
192 if (Array.isArray(summaries)) { |
206 if (Array.isArray(summaries)) { |
193 content += `<h3>${date}: ${summaries.length} commit${summaries.length !== 1 ? 's' : ''}</h3>`; |
207 content += `<h3>${date}: ${summaries.length} commit${summaries.length !== 1 ? 's' : ''}</h3>`; |
|
208 if (this.dataset.tags) { |
|
209 content += `<h5>Tags: ${this.dataset.tags}</h5>`; |
|
210 } |
194 content += '<ul>'; |
211 content += '<ul>'; |
195 summaries.forEach(summary => { |
212 summaries.forEach(summary => { |
196 content += `<li>${summary}</li>`; |
213 content += `<li>${summary}</li>`; |
197 }); |
214 }); |
198 content += '</ul>'; |
215 content += '</ul>'; |
199 } else { |
216 } else { |
200 const repos = Object.keys(summaries).sort(); |
217 const repos = Object.keys(summaries).sort(); |
|
218 const tags = this.dataset.tags ? JSON.parse(this.dataset.tags) : {}; |
201 let total_commits = 0; |
219 let total_commits = 0; |
202 for (const repo of repos) total_commits += summaries[repo].length; |
220 for (const repo of repos) total_commits += summaries[repo].length; |
203 content += `<h3>${date}: ${total_commits} commit${total_commits !== 1 ? 's' : ''}</h3>`; |
221 content += `<h3>${date}: ${total_commits} commit${total_commits !== 1 ? 's' : ''}</h3>`; |
204 for (const repo of repos) { |
222 for (const repo of repos) { |
205 const commits = summaries[repo]; |
223 const commits = summaries[repo]; |
206 if (repos.length > 1) { |
224 if (repos.length > 1) { |
207 content += `<h4>${repo} (${commits.length} commit${commits.length !== 1 ? 's' : ''})</h4>`; |
225 content += `<h4>${repo} (${commits.length} commit${commits.length !== 1 ? 's' : ''})</h4>`; |
208 } else { |
226 } else { |
209 content += `<h4>${repo}</h4>`; |
227 content += `<h4>${repo}</h4>`; |
|
228 } |
|
229 if (tags[repo]) { |
|
230 content += `<h5>Tags: ${tags[repo]}</h5>`; |
210 } |
231 } |
211 content += '<ul>'; |
232 content += '<ul>'; |
212 commits.forEach(commit => { |
233 commits.forEach(commit => { |
213 content += `<li>${commit}</li>`; |
234 content += `<li>${commit}</li>`; |
214 }); |
235 }); |
370 weekdays[weekday(ymd).iso_encoding() - 1], |
391 weekdays[weekday(ymd).iso_encoding() - 1], |
371 static_cast<int>(ymd.year()), |
392 static_cast<int>(ymd.year()), |
372 static_cast<unsigned>(ymd.month()), |
393 static_cast<unsigned>(ymd.month()), |
373 static_cast<unsigned>(ymd.day())); |
394 static_cast<unsigned>(ymd.day())); |
374 |
395 |
375 // Build a JSON object of commit summaries |
396 // Utility function to escape strings in JSON |
376 auto escape_json = [](std::string str) static { |
397 auto escape_json = [](std::string str) static { |
377 size_t pos = str.find('\"'); |
398 size_t pos = str.find('\"'); |
378 if (pos == std::string::npos) return str; |
399 if (pos == std::string::npos) return str; |
379 std::string escaped = std::move(str); |
400 std::string escaped = std::move(str); |
380 do { |
401 do { |
381 escaped.replace(pos, 1, "\\\""); |
402 escaped.replace(pos, 1, "\\\""); |
382 pos += 2; |
403 pos += 2; |
383 } while ((pos = escaped.find('\"', pos)) != std::string::npos); |
404 } while ((pos = escaped.find('\"', pos)) != std::string::npos); |
384 return escaped; |
405 return escaped; |
385 }; |
406 }; |
|
407 |
|
408 // Build a JSON object of commit summaries |
386 auto add_summaries = [escape_json](std::string &json, const std::vector<std::string> &summaries) { |
409 auto add_summaries = [escape_json](std::string &json, const std::vector<std::string> &summaries) { |
387 // We have to iterate in reverse order to sort the summaries chronologically |
410 // We have to iterate in reverse order to sort the summaries chronologically |
388 for (const auto &summary : summaries | std::views::reverse) { |
411 for (const auto &summary : summaries | std::views::reverse) { |
389 json += "\"" + escape_json(summary) + "\","; |
412 json += "\"" + escape_json(summary) + "\","; |
390 } |
413 } |
408 summaries_json.pop_back(); |
431 summaries_json.pop_back(); |
409 } |
432 } |
410 summaries_json += '}'; |
433 summaries_json += '}'; |
411 } |
434 } |
412 |
435 |
413 indent(); |
436 // Build a JSON object of tags |
414 printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n", |
437 std::string tags_json; |
|
438 if (hide_repo_names) { |
|
439 for (const auto &tags_vector: commits.tags | std::views::values) { |
|
440 for (const auto &tag: tags_vector) { |
|
441 tags_json += escape_json(tag); |
|
442 tags_json += ' '; |
|
443 } |
|
444 } |
|
445 if (!tags_json.empty()) { |
|
446 tags_json.pop_back(); |
|
447 } |
|
448 } else { |
|
449 tags_json += '{'; |
|
450 for (const auto &[repo, tags_vector] : commits.tags) { |
|
451 tags_json += "\"" + escape_json(repo) + "\":\""; |
|
452 for (const auto &tag: tags_vector) { |
|
453 tags_json += escape_json(tag); |
|
454 tags_json += ' '; |
|
455 } |
|
456 if (!tags_vector.empty()) { |
|
457 tags_json.pop_back(); |
|
458 } |
|
459 tags_json += "\","; |
|
460 } |
|
461 // note: in contrast to summaries, we want an empty string here when there's nothing to report |
|
462 tags_json.pop_back(); |
|
463 if (!commits.tags.empty()) { |
|
464 tags_json += '}'; |
|
465 } |
|
466 } |
|
467 |
|
468 // Output |
|
469 indent(); |
|
470 printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n", |
415 color_class, |
471 color_class, |
416 date_str, |
472 date_str, |
417 commits.count(), |
473 commits.count(), |
418 commits.count() == 1 ? "commit" : "commits", |
474 commits.count() == 1 ? "commit" : "commits", |
419 date_str, |
475 date_str, |
420 encode(summaries_json).c_str() |
476 encode(summaries_json).c_str(), |
|
477 encode(tags_json).c_str() |
421 ); |
478 ); |
422 } |
479 } |