src/html.cpp

changeset 61
d77763d2fdda
parent 60
9b1cbc665851
child 62
89b12ef5e190
equal deleted inserted replaced
60:9b1cbc665851 61:d77763d2fdda
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 }
130 background-color: #00A300; 131 background-color: #00A300;
131 } 132 }
132 133
133 table.heatmap td.commit-spam { 134 table.heatmap td.commit-spam {
134 background-color: #008000; 135 background-color: #008000;
136 }
137
138 table.heatmap td[data-tags]:not([data-tags=""]) {
139 border-color: gold;
135 } 140 }
136 141
137 /* Popup styles */ 142 /* Popup styles */
138 .commit-popup { 143 .commit-popup {
139 position: absolute; 144 position: absolute;
154 margin-top: 0; 159 margin-top: 0;
155 border-bottom: 1px solid #eee; 160 border-bottom: 1px solid #eee;
156 padding-bottom: 5px; 161 padding-bottom: 5px;
157 } 162 }
158 163
164 .commit-popup h4 {
165 margin-top: .5em;
166 margin-bottom: .5em;
167 }
168
169 .commit-popup h5 {
170 margin-top: 0;
171 }
172
159 .commit-popup ul { 173 .commit-popup ul {
160 margin: 0; 174 margin: 0;
161 padding-left: 20px; 175 padding-left: 20px;
162 } 176 }
163 177
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 }

mercurial