--- a/src/html.cpp Sun Aug 10 11:59:03 2025 +0200 +++ b/src/html.cpp Sun Aug 10 15:22:25 2025 +0200 @@ -86,20 +86,21 @@ <style> table.heatmap { table-layout: fixed; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 4px; font-family: sans-serif; } table.heatmap td, table.heatmap th { text-align: center; - border: solid 1px lightgray; - height: 1.5em; + font-size: 0.75rem; + border: solid 2px transparent; + border-radius: 4px; + height: 1rem; } table.heatmap td { - border: solid 1px lightgray; - width: 1.5em; - height: 1.5em; + width: 1rem; } table.heatmap td:hover, table.heatmap td.popup-open { @@ -134,6 +135,10 @@ background-color: #008000; } + table.heatmap td[data-tags]:not([data-tags=""]) { + border-color: gold; + } + /* Popup styles */ .commit-popup { position: absolute; @@ -156,6 +161,15 @@ padding-bottom: 5px; } + .commit-popup h4 { + margin-top: .5em; + margin-bottom: .5em; + } + + .commit-popup h5 { + margin-top: 0; + } + .commit-popup ul { margin: 0; padding-left: 20px; @@ -191,6 +205,9 @@ let content = '<span class="close-btn">×</span>'; if (Array.isArray(summaries)) { content += `<h3>${date}: ${summaries.length} commit${summaries.length !== 1 ? 's' : ''}</h3>`; + if (this.dataset.tags) { + content += `<h5>Tags: ${this.dataset.tags}</h5>`; + } content += '<ul>'; summaries.forEach(summary => { content += `<li>${summary}</li>`; @@ -198,6 +215,7 @@ content += '</ul>'; } else { const repos = Object.keys(summaries).sort(); + const tags = this.dataset.tags ? JSON.parse(this.dataset.tags) : {}; let total_commits = 0; for (const repo of repos) total_commits += summaries[repo].length; content += `<h3>${date}: ${total_commits} commit${total_commits !== 1 ? 's' : ''}</h3>`; @@ -208,6 +226,9 @@ } else { content += `<h4>${repo}</h4>`; } + if (tags[repo]) { + content += `<h5>Tags: ${tags[repo]}</h5>`; + } content += '<ul>'; commits.forEach(commit => { content += `<li>${commit}</li>`; @@ -372,7 +393,7 @@ static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day())); - // Build a JSON object of commit summaries + // Utility function to escape strings in JSON auto escape_json = [](std::string str) static { size_t pos = str.find('\"'); if (pos == std::string::npos) return str; @@ -383,6 +404,8 @@ } while ((pos = escaped.find('\"', pos)) != std::string::npos); return escaped; }; + + // Build a JSON object of commit summaries auto add_summaries = [escape_json](std::string &json, const std::vector<std::string> &summaries) { // We have to iterate in reverse order to sort the summaries chronologically for (const auto &summary : summaries | std::views::reverse) { @@ -410,13 +433,47 @@ summaries_json += '}'; } + // Build a JSON object of tags + std::string tags_json; + if (hide_repo_names) { + for (const auto &tags_vector: commits.tags | std::views::values) { + for (const auto &tag: tags_vector) { + tags_json += escape_json(tag); + tags_json += ' '; + } + } + if (!tags_json.empty()) { + tags_json.pop_back(); + } + } else { + tags_json += '{'; + for (const auto &[repo, tags_vector] : commits.tags) { + tags_json += "\"" + escape_json(repo) + "\":\""; + for (const auto &tag: tags_vector) { + tags_json += escape_json(tag); + tags_json += ' '; + } + if (!tags_vector.empty()) { + tags_json.pop_back(); + } + tags_json += "\","; + } + // note: in contrast to summaries, we want an empty string here when there's nothing to report + tags_json.pop_back(); + if (!commits.tags.empty()) { + tags_json += '}'; + } + } + + // Output indent(); - printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n", + printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\" data-tags=\"%s\"></td>\n", color_class, date_str, commits.count(), commits.count() == 1 ? "commit" : "commits", date_str, - encode(summaries_json).c_str() + encode(summaries_json).c_str(), + encode(tags_json).c_str() ); }