Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 80 additions & 37 deletions src/PRDigest.NET/HtmlGenereator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ public static string GenerateIndex(string archivesDir, string outputsDir)
statsHtml = $"""
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{analyzerResult.PullRequestCount}</div>
<div class="stat-value">{analyzerResult.PullRequestTotalCount}</div>
<div class="stat-label">マージされたPR</div>
</div>
<div class="stat-card">
<div class="stat-value">{analyzerResult.PullRequestCountForBot}</div>
<div class="stat-label">マージされたPR(Bot)</div>
</div>
<div class="stat-card">
<div class="stat-value">{analyzerResult.LabelCount}</div>
<div class="stat-label">ラベル種類</div>
Expand Down Expand Up @@ -120,6 +124,7 @@ public static string GenerateHtmlFromMarkdown(string startTargetDate, string mar
}

var analyzerResult = PullReqeustAnalayzer.Analayze(document);
var categoryViewHtml = GenerateCategorizedTocHtml(analyzerResult);
var labelViewHtml = GenerateLabelViewHtml(analyzerResult);

var content = $"""
Expand All @@ -128,11 +133,15 @@ public static string GenerateHtmlFromMarkdown(string startTargetDate, string mar
<hr>
<div class="view-tabs">
<button class="view-tab active" data-view="list">一覧</button>
<button class="view-tab" data-view="category">カテゴリ別</button>
<button class="view-tab" data-view="label">ラベル別</button>
</div>
<div id="list-view" class="view-panel">
{tocHtml}
</div>
<div id="category-view" class="view-panel" style="display:none">
{categoryViewHtml}
</div>
<div id="label-view" class="view-panel" style="display:none">
{labelViewHtml}
</div>
Expand Down Expand Up @@ -164,49 +173,83 @@ private static string GenerateLabelViewHtml(PullReqeustAnalayzer.AnalayzerResult

foreach (var heading in headingBlocks)
{
// Extract PR number and title from HeadingBlock inlines
var pullRequestNumber = "";
var titleText = "";
AppendHeadingListItem(ref builder, heading);
}

builder.AppendLiteral($" </ol>{Environment.NewLine}");
builder.AppendLiteral($"</details>{Environment.NewLine}");
}

return builder.ToStringAndClear();
}

private static string GenerateCategorizedTocHtml(PullReqeustAnalayzer.AnalayzerResult analyzerResult)
{
var builder = new DefaultInterpolatedStringHandler(0, 0);
builder.AppendLiteral($"<h3>カテゴリ別PR一覧</h3>{Environment.NewLine}");

// Community PRs (expanded)
var communityCount = analyzerResult.CommunityPullRequestHeadingSpan.Length;
builder.AppendLiteral($"<details class=\"label-group\">{Environment.NewLine}");
Comment thread
prozolic marked this conversation as resolved.
builder.AppendLiteral($" <summary class=\"label-group-summary\">Community PRs <span class=\"label-pr-count\">({communityCount} PRs)</span></summary>{Environment.NewLine}");
builder.AppendLiteral($" <ol class=\"label-pr-list\">{Environment.NewLine}");
foreach (var heading in analyzerResult.CommunityPullRequestHeadingSpan)
{
AppendHeadingListItem(ref builder, heading);
}
builder.AppendLiteral($" </ol>{Environment.NewLine}");
builder.AppendLiteral($"</details>{Environment.NewLine}");

// Bot PRs (collapsed)
var botCount = analyzerResult.BotPullRequestHeadings?.Count ?? 0;
builder.AppendLiteral($"<details class=\"label-group\">{Environment.NewLine}");
builder.AppendLiteral($" <summary class=\"label-group-summary\">Bot PRs <span class=\"label-pr-count\">({botCount} PRs)</span></summary>{Environment.NewLine}");
builder.AppendLiteral($" <ol class=\"label-pr-list\">{Environment.NewLine}");
foreach (var heading in analyzerResult.BotPullRequestHeadings ?? [])
{
AppendHeadingListItem(ref builder, heading);
}
builder.AppendLiteral($" </ol>{Environment.NewLine}");
builder.AppendLiteral($"</details>{Environment.NewLine}");

return builder.ToStringAndClear();
}

private static void AppendHeadingListItem(ref DefaultInterpolatedStringHandler builder, HeadingBlock heading)
{
var pullRequestNumber = "";
var titleText = "";

var inline = heading.Inline?.FirstChild;
while (inline is not null)
var inline = heading.Inline?.FirstChild;
while (inline is not null)
{
if (inline is LinkInline linkInline)
{
var linkChild = linkInline.FirstChild;
while (linkChild is not null)
{
if (inline is LinkInline linkInline)
if (linkChild is LiteralInline lit)
{
// The link text contains the PR number like "#124237"
var linkChild = linkInline.FirstChild;
while (linkChild is not null)
{
if (linkChild is LiteralInline lit)
{
pullRequestNumber = lit.Content.ToString();
}
linkChild = linkChild.NextSibling;
}
pullRequestNumber = lit.Content.ToString();
}
else if (inline is LiteralInline literal)
{
titleText += literal.Content.ToString();
}
else if (inline is CodeInline codeInline)
{
titleText += codeInline.Content;
}
inline = inline.NextSibling;
linkChild = linkChild.NextSibling;
}

// Derive anchor ID from PR number (remove '#')
var anchorId = pullRequestNumber.TrimStart('#');
var displayText = $"{pullRequestNumber} {titleText.Trim()}";

builder.AppendLiteral($" <li><a href=\"#{anchorId}\">{System.Net.WebUtility.HtmlEncode(displayText)}</a></li>{Environment.NewLine}");
}

builder.AppendLiteral($" </ol>{Environment.NewLine}");
builder.AppendLiteral($"</details>{Environment.NewLine}");
else if (inline is LiteralInline literal)
{
titleText += literal.Content.ToString();
}
else if (inline is CodeInline codeInline)
{
titleText += codeInline.Content;
}
inline = inline.NextSibling;
}

return builder.ToStringAndClear();
var anchorId = pullRequestNumber.TrimStart('#');
var displayText = $"{pullRequestNumber} {titleText.Trim()}";

builder.AppendLiteral($" <li><a href=\"#{anchorId}\">{System.Net.WebUtility.HtmlEncode(displayText)}</a></li>{Environment.NewLine}");
}

private static string GenerateTemplateHtml(string title, string subTitle, string content, bool includeViewScript = false)
Expand Down Expand Up @@ -576,7 +619,7 @@ footer p {

.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stats grid now has 3 columns on all screen sizes. On mobile devices (max-width: 768px), this may result in cramped cards. Consider adding a responsive style in the @media (max-width: 768px) section to make the grid single-column or wrap to 2 columns on mobile, for example: '.stats-grid { grid-template-columns: 1fr; }' or '.stats-grid { grid-template-columns: repeat(2, 1fr); }'.

Copilot uses AI. Check for mistakes.
gap: 16px;
margin: 16px 0 24px 0;
}
Expand Down
58 changes: 49 additions & 9 deletions src/PRDigest.NET/PullReqeustAnalayzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ internal static class PullReqeustAnalayzer
public static AnalayzerResult Analayze(MarkdownDocument document)
{
var tableOfContents = false;
var pullRequestCount = 0;
var pullRequestTotalCount = 0;
var pullRequestCountForBot = 0;
HeadingBlock? nextPrNumber = null;
HashSet<string>? prNumberTable = null;
Dictionary<string, List<HeadingBlock>>? labelTable = new();
Dictionary<string, string>? labelColorMap = new();
List<HeadingBlock> botPullRequestHeadings = new();
List<HeadingBlock> communityPrHeadings = new();
Comment thread
prozolic marked this conversation as resolved.

foreach (var block in document)
{
Expand All @@ -29,10 +32,39 @@ public static AnalayzerResult Analayze(MarkdownDocument document)
{
if (tableOfContents && nextPrNumber is not null)
{
var labelBlock = listBlock.Descendants<ListItemBlock>().Skip(3).FirstOrDefault();
var labels = labelBlock?.Descendants<LiteralInline>()
.Where(l =>
{
// pullRequestInfo is 4 items.
// 0: User
// 1: Created at
// 2: Merged at
// 3: Labels
var pullRequestInfo = listBlock.Descendants<ListItemBlock>().ToArray();

var userBlock = pullRequestInfo[0];
var user = userBlock?.Descendants<LiteralInline>().Skip(1).FirstOrDefault();

if (user is not null)
{
var userName = user.Content.ToString().Trim();

// check ..[bot].. or @Copilot to count bot PRs
if (userName.EndsWith("[bot]", StringComparison.OrdinalIgnoreCase) ||
Comment thread
prozolic marked this conversation as resolved.
userName.IndexOf("@Copilot", StringComparison.OrdinalIgnoreCase) > -1)
Comment thread
prozolic marked this conversation as resolved.
{
pullRequestCountForBot++;
botPullRequestHeadings.Add(nextPrNumber);
}
else
{
communityPrHeadings.Add(nextPrNumber);
}
}
else
{
communityPrHeadings.Add(nextPrNumber);
}

var labelBlock = pullRequestInfo[3];
Comment thread
prozolic marked this conversation as resolved.
var labels = labelBlock?.Descendants<LiteralInline>().Where(l => {
var labelText = l.Content.ToString();
return !string.IsNullOrWhiteSpace(labelText) && !labelText.Contains("ラベル");
});
Expand Down Expand Up @@ -89,7 +121,7 @@ public static AnalayzerResult Analayze(MarkdownDocument document)
{
foreach (var listItemBlock in listBlock.Descendants<ListItemBlock>())
{
pullRequestCount++;
pullRequestTotalCount++;
var prNumber = listItemBlock.Descendants<LinkInline>().FirstOrDefault();
if (prNumber is not null)
{
Expand All @@ -105,17 +137,25 @@ public static AnalayzerResult Analayze(MarkdownDocument document)

return new AnalayzerResult
{
PullRequestCount = pullRequestCount,
PullRequestTotalCount = pullRequestTotalCount,
PullRequestCountForBot = pullRequestCountForBot,
LabelInfo = labelTable,
LabelColorMap = labelColorMap
LabelColorMap = labelColorMap,
BotPullRequestHeadings = botPullRequestHeadings,
CommunityPullRequestHeadings = communityPrHeadings
};
}

public ref struct AnalayzerResult
{
public int PullRequestCount;
public int PullRequestTotalCount;
public int PullRequestCountForBot;
public Dictionary<string, List<HeadingBlock>>? LabelInfo;
public Dictionary<string, string>? LabelColorMap;
public List<HeadingBlock>? BotPullRequestHeadings;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency in the nullability of BotPullRequestHeadings (nullable) versus CommunityPullRequestHeadings (non-nullable). Both are initialized as new List instances and used in similar ways. Since CommunityPullRequestHeadings is also used to create a ReadOnlySpan via CollectionsMarshal.AsSpan (which requires a non-null list), it makes sense for it to be non-nullable. For consistency, BotPullRequestHeadings should also be non-nullable, and the null-coalescing operators on lines 204 and 208 in HtmlGenereator.cs can be removed.

Suggested change
public List<HeadingBlock>? BotPullRequestHeadings;
public List<HeadingBlock> BotPullRequestHeadings;

Copilot uses AI. Check for mistakes.
public List<HeadingBlock> CommunityPullRequestHeadings;

public readonly int LabelCount => LabelInfo?.Count ?? 0;
public ReadOnlySpan<HeadingBlock> CommunityPullRequestHeadingSpan => CollectionsMarshal.AsSpan(CommunityPullRequestHeadings);
}
}
Loading