185 cell.addEventListener('click', function(e) { |
185 cell.addEventListener('click', function(e) { |
186 const date = this.dataset.date; |
186 const date = this.dataset.date; |
187 const summaries = JSON.parse(this.dataset.summaries); |
187 const summaries = JSON.parse(this.dataset.summaries); |
188 |
188 |
189 // Create popup content |
189 // Create popup content |
190 let content = `<span class="close-btn">×</span> |
190 let content = '<span class="close-btn">×</span>'; |
191 <h3>${date}: ${summaries.length} commit${summaries.length !== '1' ? 's' : ''}</h3>`; |
191 if (Array.isArray(summaries)) { |
192 content += '<ul>'; |
192 content += `<h3>${date}: ${summaries.length} commit${summaries.length !== 1 ? 's' : ''}</h3>`; |
193 summaries.forEach(summary => { |
193 content += '<ul>'; |
194 content += `<li>${summary}</li>`; |
194 summaries.forEach(summary => { |
195 }); |
195 content += `<li>${summary}</li>`; |
196 content += '</ul>'; |
196 }); |
|
197 content += '</ul>'; |
|
198 } else { |
|
199 const repos = Object.keys(summaries).sort(); |
|
200 let total_commits = 0; |
|
201 for (const repo of repos) total_commits += summaries[repo].length; |
|
202 content += `<h3>${date}: ${total_commits} commit${total_commits !== 1 ? 's' : ''}</h3>`; |
|
203 for (const repo of repos) { |
|
204 const commits = summaries[repo]; |
|
205 if (repos.length > 1) { |
|
206 content += `<h4>${repo} (${commits.length} commit${commits.length !== 1 ? 's' : ''})</h4>`; |
|
207 } else { |
|
208 content += `<h4>${repo}</h4>`; |
|
209 } |
|
210 content += '<ul>'; |
|
211 commits.forEach(commit => { |
|
212 content += `<li>${commit}</li>`; |
|
213 }); |
|
214 content += '</ul>'; |
|
215 } |
|
216 } |
197 popup.innerHTML = content; |
217 popup.innerHTML = content; |
198 |
218 |
199 // Position popup near the cell |
219 // Position popup near the cell |
200 const rect = this.getBoundingClientRect(); |
220 const rect = this.getBoundingClientRect(); |
201 popup.style.left = rect.left + window.scrollX + 'px'; |
221 popup.style.left = rect.left + window.scrollX + 'px'; |
324 void html::cell_out_of_range() { |
344 void html::cell_out_of_range() { |
325 indent(); |
345 indent(); |
326 puts("<td class=\"out-of-range\"></td>"); |
346 puts("<td class=\"out-of-range\"></td>"); |
327 } |
347 } |
328 |
348 |
329 void html::cell(year_month_day ymd, const fm::commits &commits) { |
349 void html::cell(year_month_day ymd, bool hide_repo_names, const fm::commits &commits) { |
330 const char *color_class; |
350 const char *color_class; |
331 if (commits.count() == 0) { |
351 if (commits.count() == 0) { |
332 color_class = "zero-commits"; |
352 color_class = "zero-commits"; |
333 } else if (commits.count() == 1) { |
353 } else if (commits.count() == 1) { |
334 color_class = "one-commit"; |
354 color_class = "one-commit"; |
349 weekdays[weekday(ymd).iso_encoding() - 1], |
369 weekdays[weekday(ymd).iso_encoding() - 1], |
350 static_cast<int>(ymd.year()), |
370 static_cast<int>(ymd.year()), |
351 static_cast<unsigned>(ymd.month()), |
371 static_cast<unsigned>(ymd.month()), |
352 static_cast<unsigned>(ymd.day())); |
372 static_cast<unsigned>(ymd.day())); |
353 |
373 |
354 // Build a JSON array of commit summaries |
374 // Build a JSON object of commit summaries |
355 // We have to iterate in reverse order to sort the summaries chronologically |
375 auto escape_json = [](std::string str) static { |
356 std::string summaries_json = "["; |
376 size_t pos = str.find('\"'); |
357 for (const auto &summary : commits.summaries | std::views::reverse) { |
377 if (pos == std::string::npos) return str; |
358 // Escape quotes in JSON |
378 std::string escaped = std::move(str); |
359 size_t pos = summary.find('\"'); |
379 do { |
360 if (pos == std::string::npos) { |
380 escaped.replace(pos, 1, "\\\""); |
361 summaries_json += "\"" + summary + "\","; |
381 pos += 2; |
362 } else { |
382 } while ((pos = escaped.find('\"', pos)) != std::string::npos); |
363 std::string escaped = summary; |
383 return escaped; |
364 do { |
384 }; |
365 escaped.replace(pos, 1, "\\\""); |
385 auto add_summaries = [escape_json](std::string &json, const std::vector<std::string> &summaries) { |
366 pos += 2; |
386 // We have to iterate in reverse order to sort the summaries chronologically |
367 } while ((pos = escaped.find('\"', pos)) != std::string::npos); |
387 for (const auto &summary : summaries | std::views::reverse) { |
368 summaries_json += "\"" + escaped + "\","; |
388 json += "\"" + escape_json(summary) + "\","; |
369 } |
389 } |
370 } |
390 json.pop_back(); |
371 summaries_json.pop_back(); |
391 }; |
372 summaries_json += "]"; |
392 std::string summaries_json; |
|
393 if (hide_repo_names) { |
|
394 summaries_json += '['; |
|
395 for (const auto &summaries: commits.summaries | std::views::values) { |
|
396 add_summaries(summaries_json, summaries); |
|
397 } |
|
398 summaries_json += ']'; |
|
399 } else { |
|
400 summaries_json += '{'; |
|
401 for (const auto &[repo, summaries] : commits.summaries) { |
|
402 summaries_json += "\"" + escape_json(repo) + "\":["; |
|
403 add_summaries(summaries_json, summaries); |
|
404 summaries_json += "],"; |
|
405 } |
|
406 summaries_json.pop_back(); |
|
407 summaries_json += '}'; |
|
408 } |
373 |
409 |
374 indent(); |
410 indent(); |
375 printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n", |
411 printf("<td class=\"%s\" title=\"%s: %u %s\" data-date=\"%s\" data-summaries=\"%s\"></td>\n", |
376 color_class, |
412 color_class, |
377 date_str, |
413 date_str, |