From dba779650e929143e8cf9e4bae3c649d1705f5bf Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Wed, 5 Aug 2026 22:42:30 +0900
Subject: [PATCH] Embed only the CSS each HTML page needs
---
src/PRDigest.NET/HtmlGenerator.cs | 710 ++++++++++++++++++------------
1 file changed, 422 insertions(+), 288 deletions(-)
diff --git a/src/PRDigest.NET/HtmlGenerator.cs b/src/PRDigest.NET/HtmlGenerator.cs
index f2a2b07..a96be5f 100644
--- a/src/PRDigest.NET/HtmlGenerator.cs
+++ b/src/PRDigest.NET/HtmlGenerator.cs
@@ -8,6 +8,14 @@ namespace PRDigest.NET;
internal static class HtmlGenerator
{
+ private enum HtmlPageKind
+ {
+ Index, // outputs/index.html
+ Daily, // outputs/yyyy/MM/dd.html
+ LabelIndex, // outputs/labels/index.html
+ LabelPage, //outputs/labels/{label}/index.html
+ }
+
private static readonly SearchValues AllowedLabelPathChars =
SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-");
@@ -130,9 +138,10 @@ public static string GenerateIndex(string archivesDir, string outputsDir)
}
return GenerateTemplateHtml(
- title: $"PR Digest.NET",
- subTitle: "dotnet/runtimeにマージされたPull RequestをAIで日本語要約",
- content: latestPullRequestInfo + detailsBuilder.ToStringAndClear(),
+ pageKind: HtmlPageKind.Index,
+ title: $"PR Digest.NET",
+ subTitle: "dotnet/runtimeにマージされたPull RequestをAIで日本語要約",
+ content: latestPullRequestInfo + detailsBuilder.ToStringAndClear(),
viewScript: GenerateViewScript(),
floatingTocHtml: "",
floatingTocScript: "");
@@ -213,7 +222,8 @@ public static string GenerateHtmlFromMarkdown(string startTargetDate, string mar
""";
return GenerateTemplateHtml(
- title: $"Pull Request on {startTargetDate}",
+ pageKind: HtmlPageKind.Daily,
+ title: $"Pull Request on {startTargetDate}",
subTitle: "dotnet/runtimeにマージされたPull RequestをAIで日本語要約",
content: content,
viewScript: GenerateViewScript(),
@@ -355,7 +365,7 @@ public static string GenerateLabelIndexHtml(Dictionaryラベル情報がありません。
");
- return GenerateLabelPage("ラベル一覧", builder.ToStringAndClear());
+ return GenerateLabelPage(HtmlPageKind.LabelIndex, "ラベル一覧", builder.ToStringAndClear());
}
builder.AppendLiteral("");
@@ -391,7 +401,7 @@ public static string GenerateLabelIndexHtml(Dictionary");
builder.AppendLiteral(Environment.NewLine);
- return GenerateLabelPage("ラベル一覧", builder.ToStringAndClear(), GenerateLabelSortScript());
+ return GenerateLabelPage(HtmlPageKind.LabelIndex, "ラベル一覧", builder.ToStringAndClear(), GenerateLabelSortScript());
}
// Client-side column sorting for the labels/index.html table — no third-party library.
@@ -489,7 +499,7 @@ public static string GenerateLabelPageHtml(string label, LabelPullRequestInfo la
builder.AppendLiteral("");
builder.AppendLiteral(Environment.NewLine);
- return GenerateLabelPage($"ラベル: {HtmlEncoder.Default.Encode(label)}", builder.ToStringAndClear(), GenerateScrollToTopHtml());
+ return GenerateLabelPage(HtmlPageKind.LabelPage, $"ラベル: {HtmlEncoder.Default.Encode(label)}", builder.ToStringAndClear(), GenerateScrollToTopHtml());
}
public static string SanitizeLabelForPath(string label)
@@ -522,9 +532,10 @@ private static void AppendIndexBadgeStyle(ref DefaultInterpolatedStringHandler b
builder.AppendLiteral("; display: inline-block; padding: 0 7px; font-size: 12px; font-weight: 500; line-height: 1.5; border-radius: 0.2em; border: 1px solid transparent;\"");
}
- private static string GenerateLabelPage(string title, string content, string bodyEndHtml = "")
+ private static string GenerateLabelPage(HtmlPageKind pageKind, string title, string content, string bodyEndHtml = "")
{
return GenerateTemplateHtml(
+ pageKind: pageKind,
title: title,
subTitle: "dotnet/runtimeにマージされたPull RequestをAIで日本語要約",
content: content,
@@ -533,7 +544,7 @@ private static string GenerateLabelPage(string title, string content, string bod
floatingTocScript: "");
}
- private static string GenerateTemplateHtml(string title, string subTitle, string content, string viewScript, string floatingTocHtml, string floatingTocScript)
+ private static string GenerateTemplateHtml(HtmlPageKind pageKind, string title, string subTitle, string content, string viewScript, string floatingTocHtml, string floatingTocScript)
{
return $$"""
@@ -578,7 +589,7 @@ private static string GenerateTemplateHtml(string title, string subTitle, string
@@ -796,9 +807,34 @@ function closeToc() {
""";
}
- private static string GenerateCssStyle()
+ private static string GetCssStyle(HtmlPageKind pageKind) => pageKind switch
{
- return $$"""
+ HtmlPageKind.Index => IndexCss,
+ HtmlPageKind.Daily => DailyCss,
+ HtmlPageKind.LabelIndex => LabelIndexCss,
+ HtmlPageKind.LabelPage => LabelPageCss,
+ _ => BaseCss,
+ };
+
+ // outputs/index.html: month list, latest digest stats, scroll-to-top button.
+ private const string IndexCss =
+ BaseCss + DetailsCss + DayListCss + StatsCss + ScrollToTopCss;
+
+ // outputs/yyyy/MM/dd.html: markdown body (code/table/strong), view tabs, label groups, floating TOC.
+ private const string DailyCss =
+ BaseCss + DetailsCss + MarkdownContentCss + TableCss + TableOfContentsCss +
+ ViewTabsCss + LabelGroupCss + LabelPrCountCss + LabelPrListCss + FloatingTocCss + ScrollToTopCss;
+
+ // outputs/labels/index.html: sortable label table only (no , no scroll-to-top button).
+ private const string LabelIndexCss =
+ BaseCss + TableCss + LabelPrCountCss + LabelIndexTableCss;
+
+ // outputs/labels/{label}/index.html: a single PR list plus the scroll-to-top button.
+ private const string LabelPageCss =
+ BaseCss + LabelPrListCss + ScrollToTopCss;
+
+ // Layout shared by every page: navbar, header, content card, base typography and footer.
+ private const string BaseCss = """
* {
box-sizing: border-box;
}
@@ -806,7 +842,7 @@ private static string GenerateCssStyle()
:root {
interpolate-size: allow-keywords;
}
-
+
body {
margin: 0;
padding: 0;
@@ -844,7 +880,7 @@ private static string GenerateCssStyle()
padding-bottom: 40px;
margin-bottom: 0;
}
-
+
.container {
display: flex;
justify-content: space-between;
@@ -853,16 +889,16 @@ private static string GenerateCssStyle()
margin: 0 auto;
padding: 0 24px;
}
-
+
.page {
min-height: 100vh;
padding: 24px 16px;
}
-
+
.main {
margin: 0 auto;
}
-
+
.content {
background: #ffffff;
width: 100%;
@@ -879,7 +915,7 @@ private static string GenerateCssStyle()
padding-bottom: 16px;
color: #1a1a1a;
}
-
+
h2 {
font-size: 24px;
font-weight: 700;
@@ -889,13 +925,13 @@ private static string GenerateCssStyle()
padding-top: 16px;
border-top: 1px solid #e5e7eb;
}
-
+
h2:first-of-type {
margin-top: 0;
padding-top: 0;
border-top: none;
}
-
+
h3 {
font-size: 20px;
font-weight: 600;
@@ -911,14 +947,14 @@ private static string GenerateCssStyle()
overflow-wrap: break-word;
color: #374151;
}
-
+
a {
color: #2563eb;
border-radius: 24px;
line-height: 24px;
text-decoration: none;
}
-
+
a:hover {
color: #2563eb;
text-decoration: underline;
@@ -936,7 +972,7 @@ private static string GenerateCssStyle()
color: #9ca3af;
text-decoration: none;
}
-
+
ul {
margin: 0;
padding: 2px 2px;
@@ -948,77 +984,124 @@ ul p{
padding: 0;
}
- .daylist {
- list-style-type: none;
- display: grid;
- grid-auto-flow: column;
- grid-template-rows: repeat(12, auto);
- }
-
- .dayitem {
- list-style: none;
- display: flex;
- align-items: center;
- }
-
li {
margin: 0;
padding: 2px 2px;
color: #374151;
}
- code {
- font-family: 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
+ footer {
+ background: #151b23;
+ color: #9ca3af;
+ padding: 32px 24px;
+ margin-top: 48px;
+ text-align: center;
font-size: 14px;
- background: #f3f4f6;
- padding: 2px 6px;
- border-radius: 4px;
- color: #e11d48;
- }
-
- pre {
- margin: 24px 0;
- padding: 0;
- border-radius: 8px;
- overflow: hidden;
- background: #f9fafb;
- border: 1px solid #e5e7eb;
}
-
- pre code {
- display: block;
- padding: 16px 20px;
- overflow-x: auto;
- background: transparent;
- color: inherit;
- border-radius: 0;
+
+ footer p {
+ margin: 8px 0;
+ color: #9ca3af;
}
- table {
- width: 100%;
- border-collapse: collapse;
- margin: 16px 0;
- font-size: 14px;
- color: #374151;
+ @media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+
+ .content {
+ max-width: 1140px;
+ }
+
+ .main {
+ max-width: 1140px;
+ }
+
}
- th {
- background: #f3f4f6;
- color: #1a1a1a;
- font-weight: 600;
- text-align: left;
- padding: 10px 14px;
- border: 1px solid #e5e7eb;
- overflow-wrap: break-word;
+ @media (max-width: 768px) {
+ .page {
+ padding: 16px 8px;
+ }
+
+ .container {
+ max-width: 720px;
+ }
+
+
+ .main {
+ max-width: 720px;
+ }
+
+ .content {
+ max-width: 720px;
+ padding: 32px 24px;
+ }
+
+ h1 {
+ font-size: 28px;
+ }
+
+ h2 {
+ font-size: 22px;
+ }
+
+ h3 {
+ font-size: 18px;
+ }
+
+ body {
+ font-size: 15px;
+ }
+
+ li {
+ word-break: break-word;
+ overflow-wrap: anywhere;
+ }
+
}
- td {
- padding: 8px 14px;
- border: 1px solid #e5e7eb;
- overflow-wrap: anywhere;
- word-break: break-word;
+ @media (prefers-color-scheme: dark) {
+ body {
+ background-color: #111827;
+ color: #e5e7eb;
+ }
+
+ .content {
+ background: #1f2937;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ }
+
+ h1, h2, h3 {
+ color: #f9fafb;
+ }
+
+ h1 {
+ border-bottom-color: #374151;
+ }
+
+ h2 {
+ border-top-color: #374151;
+ }
+
+ p, li {
+ color: #d1d5db;
+ }
+
+ a {
+ color: #60a5fa;
+ }
+
+ a:hover {
+ color: #93c5fd;
+ }
}
+
+""";
+
+ // / blocks: the month list on index.html and the label groups on daily pages.
+ private const string DetailsCss = """
details {
margin: 0px 0px 8px 0px;
background-color: #f5f5f5;
@@ -1031,7 +1114,7 @@ pre code {
transition: height 0.1s ease, opacity 0.1s ease,
content-visibility 0.1s ease allow-discrete;
}
-
+
details[open]::details-content {
height: auto; /* for unsupported browser */
height: calc-size(auto, size);
@@ -1045,43 +1128,56 @@ pre code {
font-weight: bold;
cursor: pointer;
}
-
- strong {
- font-weight: 600;
- color: #1a1a1a;
- }
- #table-of-contents + ol li {
- padding: 0;
- margin: 0;
- font-weight: bold;
+ @media (max-width: 768px) {
+ summary {
+ padding: 1.2em 1em;
+ font-size: 16px;
+ }
}
- #table-of-contents + ol li a {
- color: #2563eb;
- font-weight: bold;
- text-decoration: underline;
+ @media (prefers-color-scheme: dark) {
+ details {
+ background-color: #1f2937;
+ }
+
+ summary {
+ background-color: #374151;
+ }
}
- #table-of-contents + ol li a:hover {
- color: #1d4ed8;
- text-decoration: underline;
+
+""";
+
+ // Day links on index.html, laid out as a multi-column grid.
+ private const string DayListCss = """
+ .daylist {
+ list-style-type: none;
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-rows: repeat(12, auto);
}
- footer {
- background: #151b23;
- color: #9ca3af;
- padding: 32px 24px;
- margin-top: 48px;
- text-align: center;
- font-size: 14px;
+ .dayitem {
+ list-style: none;
+ display: flex;
+ align-items: center;
}
- footer p {
- margin: 8px 0;
- color: #9ca3af;
+ @media (max-width: 768px) {
+ .daylist {
+ list-style-type: none;
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-rows: repeat(16, auto);
+ }
}
+
+""";
+
+ // PR count cards for the latest digest on index.html.
+ private const string StatsCss = """
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -1120,118 +1216,70 @@ footer p {
overflow-wrap: break-word;
}
- .view-tabs {
- display: flex;
- border-bottom: 2px solid #e5e7eb;
- margin: 24px 0 16px 0;
- }
-
- .view-tab {
- padding: 10px 24px;
- background: transparent;
- color: #6b7280;
- font-size: 16px;
- font-weight: 600;
- border: none;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
- cursor: pointer;
- transition: color 0.15s, border-bottom-color 0.15s;
- }
+ @media (max-width: 768px) {
+ .stats-grid,
+ .stats-label-row {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
- .view-tab:hover {
- color: #2563eb;
+ .stat-value {
+ font-size: 28px;
+ }
}
- .view-tab.active {
- color: #2563eb;
- border-bottom-color: #2563eb;
- }
+ @media (prefers-color-scheme: dark) {
+ .stat-card {
+ background: #374151;
+ border-color: #4b5563;
+ }
- .label-group {
- margin: 0 0 8px 0;
- }
+ .stat-value {
+ color: #f9fafb;
+ }
- .label-group-summary {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px;
+ .stat-label {
+ color: #9ca3af;
+ }
}
- .label-pr-count {
- font-size: 13px;
- color: #6b7280;
- font-weight: normal;
- }
- .label-pr-list {
- padding: 8px 16px 8px 32px;
- }
+""";
- .label-pr-list li {
- padding: 2px 0;
+ // Inline code, code blocks, emphasis and label badges rendered from the summarized markdown.
+ private const string MarkdownContentCss = """
+ code {
+ font-family: 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
+ font-size: 14px;
+ background: #f3f4f6;
+ padding: 2px 6px;
+ border-radius: 4px;
+ color: #e11d48;
}
- .label-pr-list li a {
- color: #2563eb;
- text-decoration: underline;
+ pre {
+ margin: 24px 0;
+ padding: 0;
+ border-radius: 8px;
+ overflow: hidden;
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
}
- .label-pr-list li a:hover {
- color: #1d4ed8;
+ pre code {
+ display: block;
+ padding: 16px 20px;
+ overflow-x: auto;
+ background: transparent;
+ color: inherit;
+ border-radius: 0;
}
- @media (min-width: 1200px) {
- .container {
- max-width: 1140px;
- }
-
- .content {
- max-width: 1140px;
- }
-
- .main {
- max-width: 1140px;
- }
-
+ strong {
+ font-weight: 600;
+ color: #1a1a1a;
}
@media (max-width: 768px) {
- .page {
- padding: 16px 8px;
- }
-
- .container {
- max-width: 720px;
- }
-
-
- .main {
- max-width: 720px;
- }
-
- .content {
- max-width: 720px;
- padding: 32px 24px;
- }
-
- h1 {
- font-size: 28px;
- }
-
- h2 {
- font-size: 22px;
- }
-
- h3 {
- font-size: 18px;
- }
-
- body {
- font-size: 15px;
- }
-
code {
word-break: break-all;
overflow-wrap: anywhere;
@@ -1243,105 +1291,59 @@ pre code {
overflow-wrap: anywhere;
}
- li {
- word-break: break-word;
- overflow-wrap: anywhere;
- }
-
li > span[style*="border-radius:2em"] {
white-space: normal !important;
overflow-wrap: anywhere;
}
-
- .daylist {
- list-style-type: none;
- display: grid;
- grid-auto-flow: column;
- grid-template-rows: repeat(16, auto);
- }
-
- summary {
- padding: 1.2em 1em;
- font-size: 16px;
- }
-
- .stats-grid,
- .stats-label-row {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- .stat-value {
- font-size: 28px;
- }
-
- .view-tab {
- padding: 8px 16px;
- font-size: 14px;
- }
-
- .label-pr-list {
- padding: 8px 8px 8px 24px;
- }
-
}
@media (prefers-color-scheme: dark) {
- body {
- background-color: #111827;
- color: #e5e7eb;
- }
-
- details {
- background-color: #1f2937;
- }
-
- summary {
- background-color: #374151;
- }
-
- .content {
- background: #1f2937;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- }
-
- h1, h2, h3, strong {
+ strong {
color: #f9fafb;
}
-
- h1 {
- border-bottom-color: #374151;
- }
-
- h2 {
- border-top-color: #374151;
- }
-
- p, li {
- color: #d1d5db;
- }
-
- a {
- color: #60a5fa;
- }
-
- a:hover {
- color: #93c5fd;
- }
- #table-of-contents + ol li a {
- color: #60a5fa;
- }
-
code {
background: #374151;
color: #fca5a5;
}
-
+
pre {
background: #111827;
border-color: #374151;
}
+ }
+
+
+""";
+
+ // Tables: markdown tables on daily pages and the label table on labels/index.html.
+ private const string TableCss = """
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 16px 0;
+ font-size: 14px;
+ color: #374151;
+ }
+
+ th {
+ background: #f3f4f6;
+ color: #1a1a1a;
+ font-weight: 600;
+ text-align: left;
+ padding: 10px 14px;
+ border: 1px solid #e5e7eb;
+ overflow-wrap: break-word;
+ }
+ td {
+ padding: 8px 14px;
+ border: 1px solid #e5e7eb;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ }
+
+ @media (prefers-color-scheme: dark) {
th {
background: #374151;
color: #f9fafb;
@@ -1352,20 +1354,77 @@ pre code {
border-color: #4b5563;
color: #d1d5db;
}
+ }
- .stat-card {
- background: #374151;
- border-color: #4b5563;
- }
- .stat-value {
- color: #f9fafb;
+""";
+
+ // The markdown-generated table of contents on daily pages.
+ private const string TableOfContentsCss = """
+ #table-of-contents + ol li {
+ padding: 0;
+ margin: 0;
+ font-weight: bold;
+ }
+
+ #table-of-contents + ol li a {
+ color: #2563eb;
+ font-weight: bold;
+ text-decoration: underline;
+ }
+
+ #table-of-contents + ol li a:hover {
+ color: #1d4ed8;
+ text-decoration: underline;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ #table-of-contents + ol li a {
+ color: #60a5fa;
}
+ }
- .stat-label {
- color: #9ca3af;
+
+""";
+
+ // List / category / label view switcher on daily pages.
+ private const string ViewTabsCss = """
+ .view-tabs {
+ display: flex;
+ border-bottom: 2px solid #e5e7eb;
+ margin: 24px 0 16px 0;
+ }
+
+ .view-tab {
+ padding: 10px 24px;
+ background: transparent;
+ color: #6b7280;
+ font-size: 16px;
+ font-weight: 600;
+ border: none;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ cursor: pointer;
+ transition: color 0.15s, border-bottom-color 0.15s;
+ }
+
+ .view-tab:hover {
+ color: #2563eb;
+ }
+
+ .view-tab.active {
+ color: #2563eb;
+ border-bottom-color: #2563eb;
+ }
+
+ @media (max-width: 768px) {
+ .view-tab {
+ padding: 8px 16px;
+ font-size: 14px;
}
+ }
+ @media (prefers-color-scheme: dark) {
.view-tabs {
border-bottom-color: #374151;
}
@@ -1379,7 +1438,25 @@ pre code {
color: #60a5fa;
border-bottom-color: #60a5fa;
}
+ }
+
+""";
+
+ // Collapsible category / label groups on daily pages.
+ private const string LabelGroupCss = """
+ .label-group {
+ margin: 0 0 8px 0;
+ }
+
+ .label-group-summary {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ }
+
+ @media (prefers-color-scheme: dark) {
.label-group {
background-color: #1f2937;
}
@@ -1387,11 +1464,54 @@ pre code {
.label-group-summary {
background-color: #374151;
}
+ }
+
+
+""";
+
+ // "(N PRs)" annotation used by daily pages and the label table.
+ private const string LabelPrCountCss = """
+ .label-pr-count {
+ font-size: 13px;
+ color: #6b7280;
+ font-weight: normal;
+ }
+ @media (prefers-color-scheme: dark) {
.label-pr-count {
color: #9ca3af;
}
+ }
+
+
+""";
+
+ // PR link lists inside label groups and on label pages.
+ private const string LabelPrListCss = """
+ .label-pr-list {
+ padding: 8px 16px 8px 32px;
+ }
+
+ .label-pr-list li {
+ padding: 2px 0;
+ }
+
+ .label-pr-list li a {
+ color: #2563eb;
+ text-decoration: underline;
+ }
+
+ .label-pr-list li a:hover {
+ color: #1d4ed8;
+ }
+
+ @media (max-width: 768px) {
+ .label-pr-list {
+ padding: 8px 8px 8px 24px;
+ }
+ }
+ @media (prefers-color-scheme: dark) {
.label-pr-list li a {
color: #60a5fa;
}
@@ -1401,6 +1521,11 @@ pre code {
}
}
+
+""";
+
+ // Sidebar table of contents shown on daily pages.
+ private const string FloatingTocCss = """
.floating-toc {
position: fixed;
top: 220px;
@@ -1512,6 +1637,11 @@ pre code {
.floating-toc-nav ol li:has(a.toc-active) { border-left-color: #60a5fa; }
}
+
+""";
+
+ // Floating "back to top" button.
+ private const string ScrollToTopCss = """
.scroll-to-top {
position: fixed;
bottom: 24px;
@@ -1557,6 +1687,11 @@ pre code {
}
}
+
+""";
+
+ // Sort indicators for the label table on labels/index.html.
+ private const string LabelIndexTableCss = """
.label-index-table th.sortable {
cursor: pointer;
user-select: none;
@@ -1580,5 +1715,4 @@ pre code {
opacity: 1;
}
""";
- }
}