From 99e978b04aa7ad53cf1f6779e9a0105426fe980b Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Sun, 22 Feb 2026 21:57:17 +0900
Subject: [PATCH 1/7] rename file
---
src/PRDigest.NET/{HtmlGenereator.cs => HtmlGenerator.cs} | 0
.../{PullReqeustAnalayzer.cs => PullReqeustAnalyzer.cs} | 0
2 files changed, 0 insertions(+), 0 deletions(-)
rename src/PRDigest.NET/{HtmlGenereator.cs => HtmlGenerator.cs} (100%)
rename src/PRDigest.NET/{PullReqeustAnalayzer.cs => PullReqeustAnalyzer.cs} (100%)
diff --git a/src/PRDigest.NET/HtmlGenereator.cs b/src/PRDigest.NET/HtmlGenerator.cs
similarity index 100%
rename from src/PRDigest.NET/HtmlGenereator.cs
rename to src/PRDigest.NET/HtmlGenerator.cs
diff --git a/src/PRDigest.NET/PullReqeustAnalayzer.cs b/src/PRDigest.NET/PullReqeustAnalyzer.cs
similarity index 100%
rename from src/PRDigest.NET/PullReqeustAnalayzer.cs
rename to src/PRDigest.NET/PullReqeustAnalyzer.cs
From 37ed4df044f8548d83057065274c3dbef60006ac Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Sun, 22 Feb 2026 21:58:56 +0900
Subject: [PATCH 2/7] Add rss feed
---
src/PRDigest.NET/HtmlGenerator.cs | 198 ++++++++--------
src/PRDigest.NET/MarkdownOptions.cs | 12 +
src/PRDigest.NET/Program.cs | 31 ++-
src/PRDigest.NET/PullReqeustAnalyzer.cs | 287 ++++++++++++++++++------
src/PRDigest.NET/RssFeedGenerator.cs | 111 +++++++++
5 files changed, 470 insertions(+), 169 deletions(-)
create mode 100644 src/PRDigest.NET/MarkdownOptions.cs
create mode 100644 src/PRDigest.NET/RssFeedGenerator.cs
diff --git a/src/PRDigest.NET/HtmlGenerator.cs b/src/PRDigest.NET/HtmlGenerator.cs
index 9420af3..a9199cb 100644
--- a/src/PRDigest.NET/HtmlGenerator.cs
+++ b/src/PRDigest.NET/HtmlGenerator.cs
@@ -1,19 +1,14 @@
using Markdig;
-using Markdig.Extensions.AutoIdentifiers;
-using Markdig.Syntax;
-using Markdig.Syntax.Inlines;
+using System.Buffers;
using System.Globalization;
using System.Runtime.CompilerServices;
+using System.Text.Encodings.Web;
namespace PRDigest.NET;
-internal static class HtmlGenereator
+internal static class HtmlGenerator
{
private static readonly StringComparer NumericOrderingComparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
- private static MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
- .UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
- .UseAdvancedExtensions()
- .Build();
public static string GenerateIndex(string archivesDir, string outputsDir)
{
@@ -26,17 +21,40 @@ public static string GenerateIndex(string archivesDir, string outputsDir)
foreach (var monthDirss in Directory.GetDirectories(yearDirs).OrderDescending(comparer))
{
var month = Path.GetFileName(monthDirss);
- detailsBuilder.AppendLiteral($"{Environment.NewLine}");
- detailsBuilder.AppendLiteral($" {year}年{month}月
{Environment.NewLine}");
- detailsBuilder.AppendLiteral($" {Environment.NewLine}");
+ detailsBuilder.AppendLiteral("");
+ detailsBuilder.AppendLiteral(Environment.NewLine);
+ detailsBuilder.AppendLiteral(" ");
+ detailsBuilder.AppendLiteral(year);
+ detailsBuilder.AppendLiteral("年");
+ detailsBuilder.AppendLiteral(month);
+ detailsBuilder.AppendLiteral("月");
+ detailsBuilder.AppendLiteral("
");
+ detailsBuilder.AppendLiteral(Environment.NewLine);
+ detailsBuilder.AppendLiteral($" {Environment.NewLine}");
- detailsBuilder.AppendLiteral($" {Environment.NewLine}");
+ detailsBuilder.AppendLiteral("
");
+ detailsBuilder.AppendLiteral(Environment.NewLine);
+ detailsBuilder.AppendLiteral(" ");
+ detailsBuilder.AppendLiteral(Environment.NewLine);
}
}
@@ -57,8 +75,8 @@ public static string GenerateIndex(string archivesDir, string outputsDir)
if (File.Exists(latestMarkdownPath))
{
var markdownContent = File.ReadAllText(latestMarkdownPath);
- var document = Markdown.Parse(markdownContent, Pipeline);
- var analyzerResult = PullReqeustAnalayzer.Analayze(document);
+ var document = Markdown.Parse(markdownContent, MarkdownOptions.Pipeline);
+ var analyzerResult = PullRequestAnalyzer.Analyze(document);
statsHtml = $"""
@@ -91,8 +109,8 @@ public static string GenerateIndex(string archivesDir, string outputsDir)
public static string GenerateHtmlFromMarkdown(string startTargetDate, string markdownContent)
{
- var document = Markdown.Parse(markdownContent, Pipeline);
- var contentHtml = Markdown.ToHtml(document, Pipeline);
+ var document = Markdown.Parse(markdownContent, MarkdownOptions.Pipeline);
+ var contentHtml = Markdown.ToHtml(document, MarkdownOptions.Pipeline);
// Split contentHtml into TOC part and PR details part
// The TOC ends after , then a
separates it from PR details
@@ -123,7 +141,7 @@ public static string GenerateHtmlFromMarkdown(string startTargetDate, string mar
prDetailsHtml = "";
}
- var analyzerResult = PullReqeustAnalayzer.Analayze(document);
+ var analyzerResult = PullRequestAnalyzer.Analyze(document);
var categoryViewHtml = GenerateCategorizedTocHtml(analyzerResult);
var labelViewHtml = GenerateLabelViewHtml(analyzerResult);
@@ -151,114 +169,107 @@ public static string GenerateHtmlFromMarkdown(string startTargetDate, string mar
return GenerateTemplateHtml($"Pull Request on {startTargetDate}", "dotnet/runtimeにマージされたPull RequestをAIで日本語要約", content, includeViewScript: true);
}
- private static string GenerateLabelViewHtml(PullReqeustAnalayzer.AnalayzerResult analyzerResult)
+ private static string GenerateLabelViewHtml(PullRequestAnalyzer.AnalysisResults analyzerResult)
{
- if (analyzerResult.LabelInfo is null || analyzerResult.LabelCount == 0)
+ if (analyzerResult.LabelCount == 0)
return "
ラベル情報がありません。
";
var builder = new DefaultInterpolatedStringHandler(0, 0);
- builder.AppendLiteral($"
ラベル別PR一覧
{Environment.NewLine}");
+ builder.AppendLiteral("
ラベル別PR一覧
");
+ builder.AppendLiteral(Environment.NewLine);
- foreach (var (labelName, headingBlocks) in analyzerResult.LabelInfo.OrderByDescending(kv => kv.Value.Count))
+ foreach (var (labelName, metadataList) in analyzerResult.LabelMap.OrderByDescending(kv => kv.Value.Length))
{
- var colorStyle = "";
- if (analyzerResult.LabelColorMap is not null && analyzerResult.LabelColorMap.TryGetValue(labelName, out var color))
+ builder.AppendLiteral("
");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral($" ");
+ builder.AppendLiteral(labelName);
+ builder.AppendLiteral(" (");
+ builder.AppendFormatted(metadataList.Length);
+ builder.AppendLiteral(" PRs)
");
+ builder.AppendFormatted(Environment.NewLine);
- builder.AppendLiteral($"{Environment.NewLine}");
- builder.AppendLiteral($" {labelName} ({headingBlocks.Count} PRs)
{Environment.NewLine}");
- builder.AppendLiteral($" {Environment.NewLine}");
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
- foreach (var heading in headingBlocks)
+ foreach (var metadata in metadataList)
{
- AppendHeadingListItem(ref builder, heading);
+ AppendHeadingListItem(ref builder, metadata);
}
- builder.AppendLiteral($"
{Environment.NewLine}");
- builder.AppendLiteral($"
{Environment.NewLine}");
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
}
return builder.ToStringAndClear();
}
- private static string GenerateCategorizedTocHtml(PullReqeustAnalayzer.AnalayzerResult analyzerResult)
+ private static string GenerateCategorizedTocHtml(PullRequestAnalyzer.AnalysisResults analyzerResult)
{
var builder = new DefaultInterpolatedStringHandler(0, 0);
- builder.AppendLiteral($"
カテゴリ別PR一覧
{Environment.NewLine}");
-
+ builder.AppendLiteral($"
カテゴリ別PR一覧
");
+ builder.AppendLiteral(Environment.NewLine);
// Community PRs (expanded)
- var communityCount = analyzerResult.CommunityPullRequestHeadingSpan.Length;
- builder.AppendLiteral($"
{Environment.NewLine}");
- builder.AppendLiteral($" Community PRs ({communityCount} PRs)
{Environment.NewLine}");
- builder.AppendLiteral($" {Environment.NewLine}");
- foreach (var heading in analyzerResult.CommunityPullRequestHeadingSpan)
+ var communityCount = analyzerResult.CommunityPullRequestMetadataSpan.Length;
+ builder.AppendLiteral($"");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral($" Community PRs (");
+ builder.AppendFormatted(communityCount);
+ builder.AppendLiteral(" PRs)
");
+ builder.AppendFormatted(Environment.NewLine);
+
+ builder.AppendLiteral($" ");
+ builder.AppendLiteral(Environment.NewLine);
+
+ foreach (var heading in analyzerResult.CommunityPullRequestMetadataSpan)
{
AppendHeadingListItem(ref builder, heading);
}
- builder.AppendLiteral($"
{Environment.NewLine}");
- builder.AppendLiteral($" {Environment.NewLine}");
+ builder.AppendLiteral("
");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
// Bot PRs (collapsed)
- var botCount = analyzerResult.BotPullRequestHeadings?.Count ?? 0;
- builder.AppendLiteral($"
{Environment.NewLine}");
- builder.AppendLiteral($" Bot PRs ({botCount} PRs)
{Environment.NewLine}");
- builder.AppendLiteral($" {Environment.NewLine}");
- foreach (var heading in analyzerResult.BotPullRequestHeadings ?? [])
+ var botCount = analyzerResult.BotPullRequestMetadataSpan.Length;
+ builder.AppendLiteral("");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral($" Bot PRs (");
+ builder.AppendFormatted(botCount);
+ builder.AppendLiteral(" PRs)
");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
+ foreach (var heading in analyzerResult.BotPullRequestMetadataSpan)
{
AppendHeadingListItem(ref builder, heading);
}
- builder.AppendLiteral($"
{Environment.NewLine}");
- builder.AppendLiteral($" {Environment.NewLine}");
+ builder.AppendLiteral("
");
+ builder.AppendLiteral(Environment.NewLine);
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
return builder.ToStringAndClear();
}
- private static void AppendHeadingListItem(ref DefaultInterpolatedStringHandler builder, HeadingBlock heading)
+ private static void AppendHeadingListItem(ref DefaultInterpolatedStringHandler builder, PullRequestAnalyzer.Metadata metadata)
{
- var pullRequestNumber = "";
- var titleText = "";
-
- 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 (linkChild is LiteralInline lit)
- {
- pullRequestNumber = lit.Content.ToString();
- }
- linkChild = linkChild.NextSibling;
- }
- }
- else if (inline is LiteralInline literal)
- {
- titleText += literal.Content.ToString();
-
- if (literal.NextSibling is LinkDelimiterInline linkDelimiterInline)
- {
- titleText += linkDelimiterInline.ToLiteral();
- foreach (var linkChild in linkDelimiterInline.OfType
())
- {
- titleText += linkChild.Content.ToString();
- }
- }
- }
- else if (inline is CodeInline codeInline)
- {
- titleText += codeInline.Content;
- }
- inline = inline.NextSibling;
- }
-
- var anchorId = pullRequestNumber.TrimStart('#');
- var displayText = $"{pullRequestNumber} {titleText.Trim()}";
-
- builder.AppendLiteral($" {System.Net.WebUtility.HtmlEncode(displayText)}{Environment.NewLine}");
+ var text = HtmlEncoder.Default.Encode(metadata.TitleText);
+ builder.AppendLiteral($" ");
+ builder.AppendLiteral(text);
+ builder.AppendLiteral("");
+ builder.AppendLiteral(Environment.NewLine);
}
private static string GenerateTemplateHtml(string title, string subTitle, string content, bool includeViewScript = false)
@@ -302,6 +313,7 @@ private static string GenerateTemplateHtml(string title, string subTitle, string
+
diff --git a/src/PRDigest.NET/MarkdownOptions.cs b/src/PRDigest.NET/MarkdownOptions.cs
new file mode 100644
index 0000000..0fc28fb
--- /dev/null
+++ b/src/PRDigest.NET/MarkdownOptions.cs
@@ -0,0 +1,12 @@
+using Markdig;
+using Markdig.Extensions.AutoIdentifiers;
+
+namespace PRDigest.NET;
+
+internal static class MarkdownOptions
+{
+ public static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
+ .UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
+ .UseAdvancedExtensions()
+ .Build();
+}
diff --git a/src/PRDigest.NET/Program.cs b/src/PRDigest.NET/Program.cs
index dfc5e46..df61ffd 100644
--- a/src/PRDigest.NET/Program.cs
+++ b/src/PRDigest.NET/Program.cs
@@ -3,6 +3,7 @@
using Anthropic.Models.Messages;
using Octokit;
using PRDigest.NET;
+using System.Globalization;
using System.Text;
if (args.Length == 0) return;
@@ -20,6 +21,9 @@
// convert all markdown files to HTML
await CreateHtml(archivesDir, outputsDir);
+// create RSS feed for the first time if it doesn't exist
+await CreateRss(archivesDir, outputsDir);
+
// end
var endTime = TimeProvider.System.GetTimestamp();
Console.WriteLine($"Total elapsed time: {TimeProvider.System.GetElapsedTime(startTime, endTime).TotalSeconds} seconds.");
@@ -68,7 +72,7 @@ async ValueTask SummarizeCurrentPullRequestAndCreate(string archivesDir, string
if (string.IsNullOrEmpty(markdown)) return;
// Save markdown and HTML files.
- var html = HtmlGenereator.GenerateHtmlFromMarkdown($"{year}年{month}月{day}日", markdown);
+ var html = HtmlGenerator.GenerateHtmlFromMarkdown($"{year}年{month}月{day}日", markdown);
var markdownTask = File.WriteAllTextAsync(Path.Combine(archivesDir, year, month, $"{day}.md"), markdown);
var htmlTask = File.WriteAllTextAsync(Path.Combine(outputsDir, year, month, $"{day}.html"), html);
await Task.WhenAll(markdownTask, htmlTask);
@@ -260,6 +264,27 @@ async ValueTask SummarizePullRequestAsync(PullRequestInfo[] pullRequestI
return $"{tableOfContentsBuilder}{separator}{markdownlBuilder}";
}
+async ValueTask CreateRss(string archivesDir, string outputsDir)
+{
+ var comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
+ var yearDir = Directory.EnumerateDirectories(archivesDir).OrderDescending(comparer).FirstOrDefault();
+ if (yearDir == null) return;
+
+ var monthDir = Directory.EnumerateDirectories(yearDir).OrderDescending(comparer).FirstOrDefault();
+ if (monthDir == null) return;
+
+ var mdFilePath = Directory.EnumerateFiles(monthDir).OrderDescending(comparer).FirstOrDefault();
+ if (mdFilePath == null) return;
+
+ var markdown = await File.ReadAllTextAsync(mdFilePath);
+ var year = Path.GetFileName(yearDir);
+ var month = Path.GetFileName(monthDir);
+ var day = Path.GetFileNameWithoutExtension(mdFilePath);
+
+ var rssContent = RssFeedGenerator.Generate($"{year}/{month}/{day}", markdown);
+ await File.WriteAllTextAsync(Path.Combine(outputsDir, $"feed.xml"), rssContent);
+}
+
async ValueTask CreateHtml(string archivesDir, string outputsDir)
{
// set up archives directory
@@ -296,14 +321,14 @@ await Parallel.ForEachAsync(Directory.GetFiles(monthDirss, "*.md"), async (dayFi
var markdown = await File.ReadAllTextAsync(dayFiles);
// ./yyyy/mm/dd.html
- var html = HtmlGenereator.GenerateHtmlFromMarkdown($"{year}年{month}月{day}日", markdown);
+ var html = HtmlGenerator.GenerateHtmlFromMarkdown($"{year}年{month}月{day}日", markdown);
await File.WriteAllTextAsync(Path.Combine(outputsDir, year, month, $"{day}.html"), html);
});
}
}
// set up index.html
- await File.WriteAllTextAsync(Path.Combine(outputsDir, "index.html"), HtmlGenereator.GenerateIndex(archivesDir, outputsDir));
+ await File.WriteAllTextAsync(Path.Combine(outputsDir, "index.html"), HtmlGenerator.GenerateIndex(archivesDir, outputsDir));
}
internal sealed class PullRequestInfo
diff --git a/src/PRDigest.NET/PullReqeustAnalyzer.cs b/src/PRDigest.NET/PullReqeustAnalyzer.cs
index 403e0bb..62e567a 100644
--- a/src/PRDigest.NET/PullReqeustAnalyzer.cs
+++ b/src/PRDigest.NET/PullReqeustAnalyzer.cs
@@ -1,81 +1,81 @@
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PRDigest.NET;
-internal static class PullReqeustAnalayzer
+internal static class PullRequestAnalyzer
{
- public static AnalayzerResult Analayze(MarkdownDocument document)
+ private enum PullRequestPosition
{
+ None,
+ Title,
+ Metadata,
+ Overview,
+ FileChanged,
+ Performance,
+ RelatedIssue,
+ Other,
+ Unknown
+ }
+
+ public static AnalysisResults Analyze(MarkdownDocument document)
+ {
+ var currentPosition = PullRequestPosition.None;
var tableOfContents = false;
var pullRequestTotalCount = 0;
var pullRequestCountForBot = 0;
- HeadingBlock? nextPrNumber = null;
- HashSet? prNumberTable = null;
- Dictionary>? labelTable = new();
- Dictionary? labelColorMap = new();
- List botPullRequestHeadings = new();
- List communityPrHeadings = new();
+ HeadingBlock? nextPullRequestNumber = null;
+ HashSet? pullRequestNumberTable = null;
+ Dictionary> labelTable = [];
+ Dictionary labelColorMap = [];
+ List botPullRequestHeadings = [];
+ List communityPrHeadings = [];
+ Metadata currentMetadata = default;
+ Dictionary pullRequestInfoTable = [];
foreach (var block in document)
{
- if (tableOfContents && block is HeadingBlock headingBlock)
+ if (block is HeadingBlock headingBlock)
{
- var link = headingBlock.Inline?.Descendants().FirstOrDefault()?.FirstChild;
- if (prNumberTable!.TryGetValue(((link as LiteralInline)?.Content.ToString() ?? "Notfound"), out var prNumber))
+ if (tableOfContents && currentPosition != PullRequestPosition.Metadata)
{
- nextPrNumber = headingBlock;
+ var link = headingBlock.Inline?.Descendants().FirstOrDefault()?.FirstChild;
+ if (pullRequestNumberTable!.TryGetValue(((link as LiteralInline)?.Content.ToString() ?? "Notfound"), out var prNumber))
+ {
+ nextPullRequestNumber = headingBlock;
+ }
+ }
+ else if (currentPosition == PullRequestPosition.Metadata)
+ {
+ var content = headingBlock.Inline?.Descendants().FirstOrDefault()?.Content.ToString() ?? "";
+ if (content == "概要")
+ {
+ currentPosition = PullRequestPosition.Overview;
+ }
+ else
+ {
+ currentPosition = PullRequestPosition.Unknown;
+ }
}
}
else if (block is ListBlock listBlock)
{
- if (tableOfContents && nextPrNumber is not null)
+ if (tableOfContents && nextPullRequestNumber is not null)
{
- // pullRequestInfo is 4 items.
+ // metadataList is 4 items.
// 0: User
// 1: Created at
// 2: Merged at
// 3: Labels
- var pullRequestInfo = listBlock.Descendants().ToArray();
-
- var userBlock = pullRequestInfo[0];
- var user = userBlock?.Descendants().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) ||
- userName.IndexOf("@Copilot", StringComparison.OrdinalIgnoreCase) > -1)
- {
- pullRequestCountForBot++;
- botPullRequestHeadings.Add(nextPrNumber);
- }
- else
- {
- communityPrHeadings.Add(nextPrNumber);
- }
- }
- else
- {
- communityPrHeadings.Add(nextPrNumber);
- }
-
- var labelBlock = pullRequestInfo[3];
- var labels = labelBlock?.Descendants().Where(l => {
- var labelText = l.Content.ToString();
- return !string.IsNullOrWhiteSpace(labelText) && !labelText.Contains("ラベル");
- });
-
- foreach (var label in labels ?? [])
- {
- ref var prList = ref CollectionsMarshal.GetValueRefOrAddDefault(labelTable, label.ToString(), out var _);
- prList ??= new();
- prList.Add(nextPrNumber);
- }
+ currentPosition = PullRequestPosition.Metadata;
+ var metadataList = listBlock.Descendants().ToArray();
+ if (metadataList.Length < 4) throw new FormatException($"Expected metadata list length to be at least 4, but got {metadataList.Length}.");
+ var labelBlock = metadataList[3];
// Extract label colors from HtmlInline spans
if (labelBlock is not null)
{
@@ -115,7 +115,44 @@ public static AnalayzerResult Analayze(MarkdownDocument document)
}
}
- nextPrNumber = null;
+ var labels = labelBlock?.Descendants().Where(l =>
+ {
+ var labelText = l.Content.ToString();
+ return !string.IsNullOrWhiteSpace(labelText) && !labelText.Contains("ラベル");
+ });
+
+ currentMetadata = GetMetadata(nextPullRequestNumber, labels);
+
+ foreach (var label in labels ?? [])
+ {
+ ref var prList = ref CollectionsMarshal.GetValueRefOrAddDefault(labelTable, label.ToString(), out var _);
+ prList ??= new List(1);
+ prList.Add(currentMetadata);
+ }
+
+ var user = metadataList[0]?.Descendants().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) ||
+ userName.IndexOf("@Copilot", StringComparison.OrdinalIgnoreCase) > -1)
+ {
+ pullRequestCountForBot++;
+ botPullRequestHeadings.Add(currentMetadata);
+ }
+ else
+ {
+ communityPrHeadings.Add(currentMetadata);
+ }
+ }
+ else
+ {
+ communityPrHeadings.Add(currentMetadata);
+ }
+
+ nextPullRequestNumber = null;
}
else if (!tableOfContents)
{
@@ -125,37 +162,141 @@ public static AnalayzerResult Analayze(MarkdownDocument document)
var prNumber = listItemBlock.Descendants().FirstOrDefault();
if (prNumber is not null)
{
- prNumberTable ??= new HashSet();
- prNumberTable.Add(prNumber?.Url?.Trim() ?? "");
+ pullRequestNumberTable ??= new HashSet();
+ pullRequestNumberTable.Add(prNumber?.Url?.Trim() ?? "");
}
}
tableOfContents = true;
}
}
+ else if (block is ParagraphBlock paragraphBlock)
+ {
+ if (currentPosition == PullRequestPosition.Overview)
+ {
+ var overviewText = GetOverview(paragraphBlock.Inline);
+ pullRequestInfoTable.TryAdd(currentMetadata.PullRequestNumber, new Summary(overviewText));
+ }
+ currentPosition = PullRequestPosition.None;
+ }
+ }
+
+ return new AnalysisResults(
+ pullRequestTotalCount,
+ pullRequestCountForBot,
+ labelTable.ToFrozenDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray()),
+ labelColorMap.ToFrozenDictionary(),
+ botPullRequestHeadings,
+ communityPrHeadings,
+ pullRequestInfoTable.ToFrozenDictionary());
+ }
+
+ private static Metadata GetMetadata(HeadingBlock heading, IEnumerable? labels)
+ {
+ var pullRequestNumber = "";
+ var titleText = "";
+
+ 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 (linkChild is LiteralInline lit)
+ {
+ pullRequestNumber = lit.Content.ToString();
+ }
+ linkChild = linkChild.NextSibling;
+ }
+ }
+ else if (inline is LiteralInline literal)
+ {
+ titleText += literal.Content.ToString();
+
+ if (literal.NextSibling is LinkDelimiterInline linkDelimiterInline)
+ {
+ titleText += linkDelimiterInline.ToLiteral();
+ foreach (var linkChild in linkDelimiterInline.OfType())
+ {
+ titleText += linkChild.Content.ToString();
+ }
+ }
+ }
+ else if (inline is CodeInline codeInline)
+ {
+ titleText += codeInline.Content;
+ }
+ inline = inline.NextSibling;
}
+ var anchorId = pullRequestNumber.TrimStart('#');
+ var displayText = $"{pullRequestNumber} {titleText.Trim()}";
+
+ return new Metadata(anchorId, displayText, labels?.Select(l => l.ToString()).ToImmutableArray() ?? ImmutableArray.Empty);
+ }
+
+ private static string GetOverview(ContainerInline? inline)
+ {
+ if (inline is null) return "";
- return new AnalayzerResult
+ var builder = new DefaultInterpolatedStringHandler(0, 0);
+ var child = inline.FirstChild;
+ while (child is not null)
{
- PullRequestTotalCount = pullRequestTotalCount,
- PullRequestCountForBot = pullRequestCountForBot,
- LabelInfo = labelTable,
- LabelColorMap = labelColorMap,
- BotPullRequestHeadings = botPullRequestHeadings,
- CommunityPullRequestHeadings = communityPrHeadings
- };
+ switch (child)
+ {
+ case LiteralInline literal:
+ builder.AppendLiteral(literal.Content.ToString());
+ break;
+ case CodeInline codeInline:
+ builder.AppendLiteral(codeInline.Content);
+ break;
+ case LineBreakInline:
+ builder.AppendLiteral("\n");
+ break;
+ case LinkInline linkInline:
+ builder.AppendLiteral(GetOverview(linkInline));
+ break;
+ case EmphasisInline emphasisInline:
+ builder.AppendLiteral(GetOverview(emphasisInline));
+ break;
+ }
+ child = child.NextSibling;
+ }
+ return builder.ToStringAndClear();
}
- public ref struct AnalayzerResult
+ public sealed class AnalysisResults(
+ int pullRequestTotalCount,
+ int pullRequestCountForBot,
+ FrozenDictionary> labelMap,
+ FrozenDictionary labelColorMap,
+ List botPullRequestMetadata,
+ List communityPullRequestMetadata,
+ FrozenDictionary summaryMap)
{
- public int PullRequestTotalCount;
- public int PullRequestCountForBot;
- public Dictionary>? LabelInfo;
- public Dictionary? LabelColorMap;
- public List? BotPullRequestHeadings;
- public List CommunityPullRequestHeadings;
-
- public readonly int LabelCount => LabelInfo?.Count ?? 0;
- public ReadOnlySpan CommunityPullRequestHeadingSpan => CollectionsMarshal.AsSpan(CommunityPullRequestHeadings);
+ public int PullRequestTotalCount => pullRequestTotalCount;
+ public int PullRequestCountForBot => pullRequestCountForBot;
+ public FrozenDictionary> LabelMap => labelMap;
+ public FrozenDictionary LabelColorGroups => labelColorMap;
+ public int LabelCount => LabelMap.Count;
+ public ReadOnlySpan CommunityPullRequestMetadataSpan => CollectionsMarshal.AsSpan(communityPullRequestMetadata);
+ public ReadOnlySpan BotPullRequestMetadataSpan => CollectionsMarshal.AsSpan(botPullRequestMetadata);
+ public FrozenDictionary SummaryMap => summaryMap;
+ }
+
+ public readonly struct Summary(string overview)
+ {
+ public string Overview => overview;
+ }
+
+ public readonly struct Metadata(string pullRequestNumber, string titleText, ImmutableArray labels)
+ {
+ public string PullRequestNumber => pullRequestNumber;
+
+ public string TitleText => titleText;
+
+ public ImmutableArray Labels => labels;
}
}
\ No newline at end of file
diff --git a/src/PRDigest.NET/RssFeedGenerator.cs b/src/PRDigest.NET/RssFeedGenerator.cs
new file mode 100644
index 0000000..6d3298b
--- /dev/null
+++ b/src/PRDigest.NET/RssFeedGenerator.cs
@@ -0,0 +1,111 @@
+using Markdig;
+using System.Collections.Frozen;
+using System.Runtime.CompilerServices;
+using System.Text.Encodings.Web;
+using static PRDigest.NET.PullRequestAnalyzer;
+
+namespace PRDigest.NET;
+
+internal static class RssFeedGenerator
+{
+ public static string Generate(string target, string markdownContent)
+ {
+ var document = Markdown.Parse(markdownContent, MarkdownOptions.Pipeline);
+ var analyzerResult = PullRequestAnalyzer.Analyze(document);
+
+ var rss = $"""
+
+
+
+ PR Digest.NET
+ https://prozolic.github.io/PRDigest.NET/
+ dotnet/runtimeにマージされたPull RequestをAIで日本語要約
+ {TimeProvider.System.GetUtcNow():R}
+
+ ja
+
+ https://prozolic.github.io/PRDigest.NET/icon-512.png
+ PR Digest.NET
+ https://prozolic.github.io/PRDigest.NET/
+
+ Copyright © 2025 prozolic
+ {GenerateItems(target, analyzerResult)}
+
+
+ """;
+
+ return rss;
+ }
+
+ private static string GenerateItems(string target, PullRequestAnalyzer.AnalysisResults analysisResult)
+ {
+ var itemBuilder = new DefaultInterpolatedStringHandler(0, 0);
+ itemBuilder.AppendLiteral(Environment.NewLine);
+ foreach (var metadata in analysisResult.CommunityPullRequestMetadataSpan)
+ {
+ AppendItem(ref itemBuilder, target, analysisResult.SummaryMap, metadata);
+ }
+ foreach (var metadata in analysisResult.BotPullRequestMetadataSpan)
+ {
+ AppendItem(ref itemBuilder, target, analysisResult.SummaryMap, metadata);
+ }
+
+ return itemBuilder.ToStringAndClear();
+
+
+ static void AppendItem(ref DefaultInterpolatedStringHandler builder, string target, FrozenDictionary summaryGroups, PullRequestAnalyzer.Metadata metadata)
+ {
+ // Rss feed item format:
+ // -
+ //
+ //
+ //
+ //
+ //
+ //
+
+ builder.AppendLiteral(" - ");
+ builder.AppendLiteral(Environment.NewLine);
+
+ var header = metadata;
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
+
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral($"https://prozolic.github.io/PRDigest.NET/");
+ builder.AppendLiteral(target);
+ builder.AppendLiteral(".html");
+ builder.AppendLiteral("#");
+ builder.AppendLiteral(header.PullRequestNumber);
+ builder.AppendLiteral("");
+ builder.AppendLiteral(Environment.NewLine);
+
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral("https://github.com/dotnet/runtime/pull/");
+ builder.AppendLiteral(header.PullRequestNumber);
+ builder.AppendLiteral("");
+ builder.AppendLiteral(Environment.NewLine);
+
+ if (DateTimeOffset.TryParseExact(target, "yyyy/MM/dd", null, System.Globalization.DateTimeStyles.AssumeUniversal, out var date))
+ {
+ builder.AppendLiteral(" ");
+ builder.AppendFormatted(date, format: "R");
+ builder.AppendLiteral("");
+ builder.AppendLiteral(Environment.NewLine);
+ }
+
+ if (summaryGroups.TryGetValue(metadata.PullRequestNumber, out var info))
+ {
+ builder.AppendLiteral(" ");
+ builder.AppendLiteral(Environment.NewLine);
+ }
+
+ builder.AppendLiteral("
");
+ builder.AppendLiteral(Environment.NewLine);
+ }
+ }
+}
\ No newline at end of file
From 6c896f20184af55bd1d85dcdf1aa0758b6e87862 Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Sun, 22 Feb 2026 22:17:36 +0900
Subject: [PATCH 3/7] fix
---
src/PRDigest.NET/RssFeedGenerator.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/PRDigest.NET/RssFeedGenerator.cs b/src/PRDigest.NET/RssFeedGenerator.cs
index 6d3298b..1f955cf 100644
--- a/src/PRDigest.NET/RssFeedGenerator.cs
+++ b/src/PRDigest.NET/RssFeedGenerator.cs
@@ -69,7 +69,7 @@ static void AppendItem(ref DefaultInterpolatedStringHandler builder, string targ
var header = metadata;
builder.AppendLiteral(" ");
builder.AppendLiteral(Environment.NewLine);
@@ -88,7 +88,7 @@ static void AppendItem(ref DefaultInterpolatedStringHandler builder, string targ
builder.AppendLiteral("");
builder.AppendLiteral(Environment.NewLine);
- if (DateTimeOffset.TryParseExact(target, "yyyy/MM/dd", null, System.Globalization.DateTimeStyles.AssumeUniversal, out var date))
+ if (DateTimeOffset.TryParseExact(target, "yyyy/MM/dd".AsSpan(), null, System.Globalization.DateTimeStyles.AssumeUniversal, out var date))
{
builder.AppendLiteral(" ");
builder.AppendFormatted(date, format: "R");
From 0b2298a3e5324a93350d0151b06ccf6616a4e432 Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Sun, 22 Feb 2026 23:30:52 +0900
Subject: [PATCH 4/7] Update
---
src/PRDigest.NET/Program.cs | 39 ++++++++++++++++++----------
src/PRDigest.NET/RssFeedGenerator.cs | 24 ++++++++---------
2 files changed, 37 insertions(+), 26 deletions(-)
diff --git a/src/PRDigest.NET/Program.cs b/src/PRDigest.NET/Program.cs
index df61ffd..add8a10 100644
--- a/src/PRDigest.NET/Program.cs
+++ b/src/PRDigest.NET/Program.cs
@@ -4,6 +4,7 @@
using Octokit;
using PRDigest.NET;
using System.Globalization;
+using System.Runtime.InteropServices;
using System.Text;
if (args.Length == 0) return;
@@ -266,22 +267,32 @@ async ValueTask SummarizePullRequestAsync(PullRequestInfo[] pullRequestI
async ValueTask CreateRss(string archivesDir, string outputsDir)
{
+ const int MaxDays = 3;
var comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
- var yearDir = Directory.EnumerateDirectories(archivesDir).OrderDescending(comparer).FirstOrDefault();
- if (yearDir == null) return;
- var monthDir = Directory.EnumerateDirectories(yearDir).OrderDescending(comparer).FirstOrDefault();
- if (monthDir == null) return;
+ var items = new List<(string target, string markdownContent)>(MaxDays);
+ foreach (var yearDir in Directory.EnumerateDirectories(archivesDir).OrderDescending(comparer))
+ {
+ var year = Path.GetFileName(yearDir);
+
+ foreach (var monthDir in Directory.EnumerateDirectories(yearDir).OrderDescending(comparer))
+ {
+ var month = Path.GetFileName(monthDir);
- var mdFilePath = Directory.EnumerateFiles(monthDir).OrderDescending(comparer).FirstOrDefault();
- if (mdFilePath == null) return;
+ foreach (var mdFilePath in Directory.EnumerateFiles(monthDir, "*.md").OrderDescending(comparer))
+ {
+ if (items.Count >= MaxDays) goto END;
+ var day = Path.GetFileNameWithoutExtension(mdFilePath);
+ var markdown = await File.ReadAllTextAsync(mdFilePath);
+ items.Add(($"{year}/{month}/{day}", markdown));
+ }
+ }
+ }
- var markdown = await File.ReadAllTextAsync(mdFilePath);
- var year = Path.GetFileName(yearDir);
- var month = Path.GetFileName(monthDir);
- var day = Path.GetFileNameWithoutExtension(mdFilePath);
+ if (items.Count == 0) return;
- var rssContent = RssFeedGenerator.Generate($"{year}/{month}/{day}", markdown);
+END:
+ var rssContent = RssFeedGenerator.Generate(CollectionsMarshal.AsSpan(items));
await File.WriteAllTextAsync(Path.Combine(outputsDir, $"feed.xml"), rssContent);
}
@@ -299,7 +310,7 @@ async ValueTask CreateHtml(string archivesDir, string outputsDir)
Directory.CreateDirectory(outputsDir);
}
- foreach (var yearDirs in Directory.GetDirectories(archivesDir))
+ foreach (var yearDirs in Directory.EnumerateDirectories(archivesDir))
{
var year = Path.GetFileName(yearDirs);
if (!Directory.Exists(Path.Combine(outputsDir, year)))
@@ -307,7 +318,7 @@ async ValueTask CreateHtml(string archivesDir, string outputsDir)
Directory.CreateDirectory(Path.Combine(outputsDir, year));
}
- foreach (var monthDirss in Directory.GetDirectories(yearDirs))
+ foreach (var monthDirss in Directory.EnumerateDirectories(yearDirs))
{
var month = Path.GetFileName(monthDirss);
if (!Directory.Exists(Path.Combine(outputsDir, year, month)))
@@ -315,7 +326,7 @@ async ValueTask CreateHtml(string archivesDir, string outputsDir)
Directory.CreateDirectory(Path.Combine(outputsDir, year, month));
}
- await Parallel.ForEachAsync(Directory.GetFiles(monthDirss, "*.md"), async (dayFiles, _) =>
+ await Parallel.ForEachAsync(Directory.EnumerateFiles(monthDirss, "*.md"), async (dayFiles, _) =>
{
var day = Path.GetFileNameWithoutExtension(dayFiles);
var markdown = await File.ReadAllTextAsync(dayFiles);
diff --git a/src/PRDigest.NET/RssFeedGenerator.cs b/src/PRDigest.NET/RssFeedGenerator.cs
index 1f955cf..c203312 100644
--- a/src/PRDigest.NET/RssFeedGenerator.cs
+++ b/src/PRDigest.NET/RssFeedGenerator.cs
@@ -1,17 +1,21 @@
using Markdig;
using System.Collections.Frozen;
using System.Runtime.CompilerServices;
-using System.Text.Encodings.Web;
-using static PRDigest.NET.PullRequestAnalyzer;
namespace PRDigest.NET;
internal static class RssFeedGenerator
{
- public static string Generate(string target, string markdownContent)
+ public static string Generate(ReadOnlySpan<(string target, string markdownContent)> items)
{
- var document = Markdown.Parse(markdownContent, MarkdownOptions.Pipeline);
- var analyzerResult = PullRequestAnalyzer.Analyze(document);
+ var itemBuilder = new DefaultInterpolatedStringHandler(0, 0);
+ foreach(var (target, markdownContent) in items)
+ {
+ var document = Markdown.Parse(markdownContent, MarkdownOptions.Pipeline);
+ var analyzerResult = PullRequestAnalyzer.Analyze(document);
+ AppendItems(ref itemBuilder, target, analyzerResult);
+ }
+ var itemsText = itemBuilder.ToStringAndClear();
var rss = $"""
@@ -29,7 +33,7 @@ public static string Generate(string target, string markdownContent)
https://prozolic.github.io/PRDigest.NET/
Copyright © 2025 prozolic
- {GenerateItems(target, analyzerResult)}
+ {itemsText}
""";
@@ -37,9 +41,8 @@ public static string Generate(string target, string markdownContent)
return rss;
}
- private static string GenerateItems(string target, PullRequestAnalyzer.AnalysisResults analysisResult)
+ private static void AppendItems(ref DefaultInterpolatedStringHandler itemBuilder, string target, PullRequestAnalyzer.AnalysisResults analysisResult)
{
- var itemBuilder = new DefaultInterpolatedStringHandler(0, 0);
itemBuilder.AppendLiteral(Environment.NewLine);
foreach (var metadata in analysisResult.CommunityPullRequestMetadataSpan)
{
@@ -50,10 +53,7 @@ private static string GenerateItems(string target, PullRequestAnalyzer.AnalysisR
AppendItem(ref itemBuilder, target, analysisResult.SummaryMap, metadata);
}
- return itemBuilder.ToStringAndClear();
-
-
- static void AppendItem(ref DefaultInterpolatedStringHandler builder, string target, FrozenDictionary summaryGroups, PullRequestAnalyzer.Metadata metadata)
+ static void AppendItem(ref DefaultInterpolatedStringHandler builder, string target, FrozenDictionary summaryGroups, PullRequestAnalyzer.Metadata metadata)
{
// Rss feed item format:
// -
From cd181a7fc54b39fad1ad9882e23165f97e8b6dd0 Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Mon, 23 Feb 2026 00:43:36 +0900
Subject: [PATCH 5/7] Add license notice in READMD.md
---
README.md | 16 ++++++++++++++++
src/PRDigest.NET/HtmlGenerator.cs | 13 ++++++++++---
2 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 1fed891..81861f5 100644
--- a/README.md
+++ b/README.md
@@ -33,3 +33,19 @@ Claude Haiku 4.5
- [dotnet/runtime](https://github.com/dotnet/runtime)
- [Claude Docs - Models overview](https://platform.claude.com/docs/en/about-claude/models/overview)
+
+## Third-party Notices
+
+### Material Design Icons
+
+- Icon: rss
+- URL: https://pictogrammers.com/library/mdi/icon/rss/
+- Copyright (c) 2014 Austin Andrews (https://templarian.com/)
+- Licensed under the [Apache License, Version 2.0](https://github.com/Templarian/MaterialDesign/blob/master/LICENSE)
+
+### GitHub Octicons
+
+- Icon: mark-github
+- URL: https://github.com/primer/octicons/blob/main/icons/mark-github-24.svg
+- Copyright (c) 2012 GitHub, Inc.
+- Licensed under the [MIT License](https://github.com/primer/octicons/blob/main/LICENSE)
diff --git a/src/PRDigest.NET/HtmlGenerator.cs b/src/PRDigest.NET/HtmlGenerator.cs
index a9199cb..8ad1682 100644
--- a/src/PRDigest.NET/HtmlGenerator.cs
+++ b/src/PRDigest.NET/HtmlGenerator.cs
@@ -327,9 +327,16 @@ private static string GenerateTemplateHtml(string title, string subTitle, string
From 16834e8380e9e5adf0b4e36447565cb687c59f42 Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Mon, 23 Feb 2026 00:56:55 +0900
Subject: [PATCH 6/7] Update src/PRDigest.NET/Program.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/PRDigest.NET/Program.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PRDigest.NET/Program.cs b/src/PRDigest.NET/Program.cs
index add8a10..eea061d 100644
--- a/src/PRDigest.NET/Program.cs
+++ b/src/PRDigest.NET/Program.cs
@@ -22,7 +22,7 @@
// convert all markdown files to HTML
await CreateHtml(archivesDir, outputsDir);
-// create RSS feed for the first time if it doesn't exist
+// (Re)create RSS feed from archived markdown files
await CreateRss(archivesDir, outputsDir);
// end
From b971d0ad3e8fd4125996ad7231a9ac26ea72fc49 Mon Sep 17 00:00:00 2001
From: prozolic <42107886+prozolic@users.noreply.github.com>
Date: Mon, 23 Feb 2026 01:06:55 +0900
Subject: [PATCH 7/7] Fix
---
.../{PullReqeustAnalyzer.cs => PullRequestAnalyzer.cs} | 0
src/PRDigest.NET/RssFeedGenerator.cs | 3 ++-
2 files changed, 2 insertions(+), 1 deletion(-)
rename src/PRDigest.NET/{PullReqeustAnalyzer.cs => PullRequestAnalyzer.cs} (100%)
diff --git a/src/PRDigest.NET/PullReqeustAnalyzer.cs b/src/PRDigest.NET/PullRequestAnalyzer.cs
similarity index 100%
rename from src/PRDigest.NET/PullReqeustAnalyzer.cs
rename to src/PRDigest.NET/PullRequestAnalyzer.cs
diff --git a/src/PRDigest.NET/RssFeedGenerator.cs b/src/PRDigest.NET/RssFeedGenerator.cs
index c203312..2fe9414 100644
--- a/src/PRDigest.NET/RssFeedGenerator.cs
+++ b/src/PRDigest.NET/RssFeedGenerator.cs
@@ -1,5 +1,6 @@
using Markdig;
using System.Collections.Frozen;
+using System.Globalization;
using System.Runtime.CompilerServices;
namespace PRDigest.NET;
@@ -88,7 +89,7 @@ static void AppendItem(ref DefaultInterpolatedStringHandler builder, string targ
builder.AppendLiteral("");
builder.AppendLiteral(Environment.NewLine);
- if (DateTimeOffset.TryParseExact(target, "yyyy/MM/dd".AsSpan(), null, System.Globalization.DateTimeStyles.AssumeUniversal, out var date))
+ if (DateTimeOffset.TryParseExact(target, "yyyy/MM/dd".AsSpan(), CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var date))
{
builder.AppendLiteral(" ");
builder.AppendFormatted(date, format: "R");