Skip to content

Commit 45554d3

Browse files
committed
Improve changelog render for asciidoc (#3262)
* Improve changelog render asciidoc output * Remove definition lists in favour of subsections
1 parent d88a8ce commit 45554d3

13 files changed

Lines changed: 175 additions & 82 deletions

docs/cli/changelog/render.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ When `--file-type asciidoc` is specified, the command generates a single asciido
9898

9999
The asciidoc output uses attribute references for links (for example, `{repo-pull}NUMBER[#NUMBER]`).
100100

101+
AsciiDoc output ignores the `--dropdowns` flag and always uses a standardized format with the following characteristics:
102+
103+
- Multi-block entries (containing description, Impact, and Action sections) use proper list continuation markers (`+`) to maintain list structure
104+
- Strong text formatting uses idiomatic single asterisk syntax (`*Impact:*`, `*Action:*`) following AsciiDoc best practices
105+
- All content blocks are properly attached to their parent list items for correct rendering
106+
101107
### Multiple PR and issue links
102108

103109
Changelog entries can reference multiple pull requests and issues using the `prs` and `issues` array fields. When an entry has multiple links, all of them are rendered inline for that entry:

src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,40 +61,48 @@ private static void RenderEntryTitleAndLinks(StringBuilder sb, ChangelogEntry en
6161
}
6262

6363
/// <summary>
64-
/// Renders an entry's description with optional comment handling
64+
/// Renders an entry's description with optional comment handling and list continuation
6565
/// </summary>
66-
private static void RenderEntryDescription(StringBuilder sb, ChangelogEntry entry, bool shouldHide)
66+
private static void RenderEntryDescription(StringBuilder sb, ChangelogEntry entry, bool shouldHide, bool needsContinuation = true)
6767
{
6868
if (string.IsNullOrWhiteSpace(entry.Description))
6969
return;
7070

7171
_ = sb.AppendLine();
72-
var indented = ChangelogTextUtilities.Indent(entry.Description);
72+
73+
// Add list continuation marker for multi-block list items
74+
if (needsContinuation)
75+
{
76+
_ = sb.AppendLine("+");
77+
}
78+
7379
if (shouldHide)
7480
{
75-
var indentedLines = indented.Split('\n');
76-
foreach (var line in indentedLines)
81+
var descriptionLines = entry.Description.Split('\n');
82+
foreach (var line in descriptionLines)
7783
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"// {line}");
7884
}
7985
else
80-
_ = sb.AppendLine(indented);
86+
_ = sb.AppendLine(entry.Description);
8187
}
8288

8389
/// <summary>
84-
/// Renders Impact and Action fields for breaking changes, deprecations, and known issues
90+
/// Renders Impact and Action fields for breaking changes, deprecations, and known issues with list continuation
8591
/// </summary>
8692
private static void RenderImpactAndAction(StringBuilder sb, ChangelogEntry entry)
8793
{
8894
if (!string.IsNullOrWhiteSpace(entry.Impact))
8995
{
9096
_ = sb.AppendLine();
91-
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"**Impact:** {entry.Impact}");
97+
_ = sb.AppendLine("+");
98+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"*Impact:* {entry.Impact}");
9299
}
93100

94101
if (!string.IsNullOrWhiteSpace(entry.Action))
95102
{
96103
_ = sb.AppendLine();
97-
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"**Action:** {entry.Action}");
104+
_ = sb.AppendLine("+");
105+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"*Action:* {entry.Action}");
98106
}
99107
}
100108

@@ -105,7 +113,7 @@ protected void RenderBasicEntry(StringBuilder sb, ChangelogEntry entry, Changelo
105113
{
106114
var (entryRepo, _, hideLinks, shouldHide) = ChangelogRenderUtilities.GetEntryContext(entry, context);
107115
RenderEntryTitleAndLinks(sb, entry, entryRepo, hideLinks, shouldHide);
108-
RenderEntryDescription(sb, entry, shouldHide);
116+
RenderEntryDescription(sb, entry, shouldHide, needsContinuation: !string.IsNullOrWhiteSpace(entry.Description));
109117
_ = sb.AppendLine();
110118
}
111119

@@ -116,7 +124,11 @@ protected void RenderEntryWithImpactAction(StringBuilder sb, ChangelogEntry entr
116124
{
117125
var (entryRepo, _, hideLinks, shouldHide) = ChangelogRenderUtilities.GetEntryContext(entry, context);
118126
RenderEntryTitleAndLinks(sb, entry, entryRepo, hideLinks, shouldHide);
119-
RenderEntryDescription(sb, entry, shouldHide);
127+
128+
// Description needs continuation when it exists
129+
var hasDescription = !string.IsNullOrWhiteSpace(entry.Description);
130+
RenderEntryDescription(sb, entry, shouldHide, needsContinuation: hasDescription);
131+
120132
RenderImpactAndAction(sb, entry);
121133
_ = sb.AppendLine();
122134
}

src/services/Elastic.Changelog/Rendering/Asciidoc/BreakingChangesAsciidocRenderer.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Globalization;
56
using System.Text;
67
using Elastic.Documentation;
78
using Elastic.Documentation.ReleaseNotes;
@@ -30,8 +31,17 @@ public override void Render(IReadOnlyCollection<ChangelogEntry> entries, Changel
3031
if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key))
3132
{
3233
var header = ChangelogTextUtilities.FormatSubtypeHeader(group.Key);
33-
var headerLine = allEntriesHidden ? $"// **{header}**" : $"**{header}**";
34-
_ = sb.AppendLine(headerLine);
34+
35+
if (allEntriesHidden)
36+
{
37+
_ = sb.AppendLine("// [float]");
38+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {header}");
39+
}
40+
else
41+
{
42+
_ = sb.AppendLine("[float]");
43+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {header}");
44+
}
3545
_ = sb.AppendLine();
3646
}
3747

src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Globalization;
56
using System.Text;
67
using Elastic.Documentation.ReleaseNotes;
78

@@ -15,22 +16,37 @@ public class DeprecationsAsciidocRenderer(StringBuilder sb) : AsciidocRendererBa
1516
/// <inheritdoc />
1617
public override void Render(IReadOnlyCollection<ChangelogEntry> entries, ChangelogRenderContext context)
1718
{
18-
var groupedByArea = entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList();
19+
// Group by area if subsections is enabled, otherwise use single group
20+
var groupedEntries = context.Subsections
21+
? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList()
22+
: [entries.GroupBy(_ => string.Empty).First()];
1923

20-
foreach (var areaGroup in groupedByArea)
24+
foreach (var group in groupedEntries)
2125
{
22-
// Check if all entries in this area group are hidden
23-
var allEntriesHidden = areaGroup.All(entry =>
26+
// Check if all entries in this group are hidden
27+
var allEntriesHidden = group.All(entry =>
2428
ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context));
2529

26-
var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General";
27-
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
30+
// Add nested section header when subsections are enabled and group has a name
31+
if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key))
32+
{
33+
var componentName = group.Key != string.Empty ? group.Key : "General";
34+
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
2835

29-
var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::";
30-
_ = sb.AppendLine(headerLine);
31-
_ = sb.AppendLine();
36+
if (allEntriesHidden)
37+
{
38+
_ = sb.AppendLine("// [float]");
39+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}");
40+
}
41+
else
42+
{
43+
_ = sb.AppendLine("[float]");
44+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}");
45+
}
46+
_ = sb.AppendLine();
47+
}
3248

33-
foreach (var entry in areaGroup)
49+
foreach (var entry in group)
3450
RenderEntryWithImpactAction(sb, entry, context);
3551
}
3652
}

src/services/Elastic.Changelog/Rendering/Asciidoc/EntriesByAreaAsciidocRenderer.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Globalization;
56
using System.Text;
67
using Elastic.Documentation.ReleaseNotes;
78

@@ -15,24 +16,37 @@ public class EntriesByAreaAsciidocRenderer(StringBuilder sb) : AsciidocRendererB
1516
/// <inheritdoc />
1617
public override void Render(IReadOnlyCollection<ChangelogEntry> entries, ChangelogRenderContext context)
1718
{
18-
var groupedByArea = context.Subsections
19+
// Group by area if subsections is enabled, otherwise use single group
20+
var groupedEntries = context.Subsections
1921
? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList()
20-
: entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).ToList();
22+
: [entries.GroupBy(_ => string.Empty).First()];
2123

22-
foreach (var areaGroup in groupedByArea)
24+
foreach (var group in groupedEntries)
2325
{
24-
// Check if all entries in this area group are hidden
25-
var allEntriesHidden = areaGroup.All(entry =>
26+
// Check if all entries in this group are hidden
27+
var allEntriesHidden = group.All(entry =>
2628
ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context));
2729

28-
var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General";
29-
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
30+
// Add nested section header when subsections are enabled and group has a name
31+
if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key))
32+
{
33+
var componentName = group.Key != string.Empty ? group.Key : "General";
34+
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
3035

31-
var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::";
32-
_ = sb.AppendLine(headerLine);
33-
_ = sb.AppendLine();
36+
if (allEntriesHidden)
37+
{
38+
_ = sb.AppendLine("// [float]");
39+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}");
40+
}
41+
else
42+
{
43+
_ = sb.AppendLine("[float]");
44+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}");
45+
}
46+
_ = sb.AppendLine();
47+
}
3448

35-
foreach (var entry in areaGroup)
49+
foreach (var entry in group)
3650
RenderBasicEntry(sb, entry, context);
3751
}
3852
}

src/services/Elastic.Changelog/Rendering/Asciidoc/HighlightsAsciidocRenderer.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Globalization;
56
using System.Text;
67
using Elastic.Documentation.ReleaseNotes;
78

@@ -15,24 +16,37 @@ public class HighlightsAsciidocRenderer(StringBuilder sb) : AsciidocRendererBase
1516
/// <inheritdoc />
1617
public override void Render(IReadOnlyCollection<ChangelogEntry> entries, ChangelogRenderContext context)
1718
{
18-
var groupedByArea = context.Subsections
19+
// Group by area if subsections is enabled, otherwise use single group
20+
var groupedEntries = context.Subsections
1921
? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList()
20-
: entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).ToList();
22+
: [entries.GroupBy(_ => string.Empty).First()];
2123

22-
foreach (var areaGroup in groupedByArea)
24+
foreach (var group in groupedEntries)
2325
{
24-
// Check if all entries in this area group are hidden
25-
var allEntriesHidden = areaGroup.All(entry =>
26+
// Check if all entries in this group are hidden
27+
var allEntriesHidden = group.All(entry =>
2628
ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context));
2729

28-
var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General";
29-
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
30+
// Add nested section header when subsections are enabled and group has a name
31+
if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key))
32+
{
33+
var componentName = group.Key != string.Empty ? group.Key : "General";
34+
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
3035

31-
var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::";
32-
_ = sb.AppendLine(headerLine);
33-
_ = sb.AppendLine();
36+
if (allEntriesHidden)
37+
{
38+
_ = sb.AppendLine("// [float]");
39+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}");
40+
}
41+
else
42+
{
43+
_ = sb.AppendLine("[float]");
44+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}");
45+
}
46+
_ = sb.AppendLine();
47+
}
3448

35-
foreach (var entry in areaGroup)
49+
foreach (var entry in group)
3650
RenderBasicEntry(sb, entry, context);
3751
}
3852
}

src/services/Elastic.Changelog/Rendering/Asciidoc/KnownIssuesAsciidocRenderer.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Globalization;
56
using System.Text;
67
using Elastic.Documentation.ReleaseNotes;
78

@@ -15,22 +16,37 @@ public class KnownIssuesAsciidocRenderer(StringBuilder sb) : AsciidocRendererBas
1516
/// <inheritdoc />
1617
public override void Render(IReadOnlyCollection<ChangelogEntry> entries, ChangelogRenderContext context)
1718
{
18-
var groupedByArea = entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList();
19+
// Group by area if subsections is enabled, otherwise use single group
20+
var groupedEntries = context.Subsections
21+
? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList()
22+
: [entries.GroupBy(_ => string.Empty).First()];
1923

20-
foreach (var areaGroup in groupedByArea)
24+
foreach (var group in groupedEntries)
2125
{
22-
// Check if all entries in this area group are hidden
23-
var allEntriesHidden = areaGroup.All(entry =>
26+
// Check if all entries in this group are hidden
27+
var allEntriesHidden = group.All(entry =>
2428
ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context));
2529

26-
var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General";
27-
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
30+
// Add nested section header when subsections are enabled and group has a name
31+
if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key))
32+
{
33+
var componentName = group.Key != string.Empty ? group.Key : "General";
34+
var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName);
2835

29-
var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::";
30-
_ = sb.AppendLine(headerLine);
31-
_ = sb.AppendLine();
36+
if (allEntriesHidden)
37+
{
38+
_ = sb.AppendLine("// [float]");
39+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}");
40+
}
41+
else
42+
{
43+
_ = sb.AppendLine("[float]");
44+
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}");
45+
}
46+
_ = sb.AppendLine();
47+
}
3248

33-
foreach (var entry in areaGroup)
49+
foreach (var entry in group)
3450
RenderEntryWithImpactAction(sb, entry, context);
3551
}
3652
}

src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct
7070
_ = sb.AppendLine(InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}");
7171
_ = sb.AppendLine(entry.Description ?? "% Describe the functionality that changed");
7272
_ = sb.AppendLine();
73-
RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks);
73+
RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks));
7474

7575
_ = sb.AppendLine(!string.IsNullOrWhiteSpace(entry.Impact)
7676
? "**Impact**<br>" + entry.Impact
@@ -99,7 +99,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct
9999
}
100100

101101
// PR/Issue links with "For more information" pattern - indented for list continuation
102-
RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true);
102+
RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks, IndentForListItem: true));
103103

104104
// Impact and Action sections - indented for list continuation
105105
if (!string.IsNullOrWhiteSpace(entry.Impact))

src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct
6767
_ = sb.AppendLine(InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}");
6868
_ = sb.AppendLine(entry.Description ?? "% Describe the functionality that was deprecated");
6969
_ = sb.AppendLine();
70-
RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks);
70+
RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks));
7171

7272
_ = sb.AppendLine(!string.IsNullOrWhiteSpace(entry.Impact)
7373
? "**Impact**<br>" + entry.Impact
@@ -96,7 +96,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct
9696
}
9797

9898
// PR/Issue links with "For more information" pattern - indented for list continuation
99-
RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true);
99+
RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks, IndentForListItem: true));
100100

101101
// Impact and Action sections - indented for list continuation
102102
if (!string.IsNullOrWhiteSpace(entry.Impact))

0 commit comments

Comments
 (0)