Skip to content

Commit e73456b

Browse files
committed
Add Text Content Rendering Support for Alerts
1 parent b146e55 commit e73456b

9 files changed

Lines changed: 343 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ with the exception that 0.x versions can break between minor versions.
88

99
## [Unreleased]
1010
### Added
11-
- Support rendering GFM task list items to Markdown
12-
- Support rendering YAML front matter to Markdown
11+
- Support rendering GFM task list items to Markdown (#433)
12+
- Support rendering YAML front matter to Markdown (#434)
1313
- Alerts
14-
- Allow customizing HTML attributes for alert title `<p>` tag via `AttributeProvider`
14+
- Allow customizing HTML attributes for alert title `<p>` tag via `AttributeProvider` (#427)
1515
- New configuration for `AlertsExtension` to allow authors to provide custom
1616
titles per alert. See the
1717
[custom titles section of the alerts README](./commonmark-ext-gfm-alerts/README.md#custom-alert-titles)
18-
for more information.
18+
for more information. (#430)
1919
- New configuration for `AlertsExtension` to allow alerts to be nested within
2020
other blocks (including other alerts). See
2121
[this section of the alerts README](./commonmark-ext-gfm-alerts/README.md#nesting-alerts)
22-
for more information.
22+
for more information. (#430)
2323
- New configuration for `AlertsExtension` to allow the set of alert types
24-
(including standard GFM types) to be completely overwritten.
24+
(including standard GFM types) to be completely overwritten. (#435)
2525
```java
2626
var extension = AlertsExtension.builder()
2727
.setAllowedTypes(Map.ofEntries(
@@ -31,6 +31,8 @@ with the exception that 0.x versions can break between minor versions.
3131
))
3232
.build();
3333
```
34+
- Support rendering alerts to text (#437)
35+
3436

3537
## [0.28.0] - 2026-03-31
3638
### Added

commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,43 @@ public Alert(String type) {
1818
public String getType() {
1919
return type;
2020
}
21+
22+
/**
23+
* @return Whether this alert has any body content (not including the title).
24+
* <p>
25+
*
26+
* - Examples where this would be {@code true}:
27+
* <pre>{@code
28+
* > [!NOTE]
29+
* > Body text
30+
* }</pre>
31+
* <pre>{@code
32+
* > [!NOTE] Custom title
33+
* > Body text
34+
* }</pre>
35+
*
36+
* - Examples where this would be {@code false}:
37+
*
38+
* <pre>{@code
39+
* > [!NOTE]
40+
* }</pre>
41+
* <pre>{@code
42+
* > [!NOTE]
43+
* >
44+
* >
45+
* }</pre>
46+
* <pre>{@code
47+
* > [!NOTE] Custom title
48+
* }</pre>
49+
*/
50+
public boolean hasBody() {
51+
var first = this.getFirstChild();
52+
if (first instanceof AlertTitle) {
53+
// Body exists if there's a sibling after AlertTitle
54+
return first.getNext() != null;
55+
} else {
56+
// Body exists if there are any children
57+
return first != null;
58+
}
59+
}
2160
}

commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import org.commonmark.ext.gfm.alerts.internal.AlertBlockParser;
55
import org.commonmark.ext.gfm.alerts.internal.AlertHtmlNodeRenderer;
66
import org.commonmark.ext.gfm.alerts.internal.AlertMarkdownNodeRenderer;
7+
import org.commonmark.ext.gfm.alerts.internal.AlertTextContentNodeRenderer;
78
import org.commonmark.parser.Parser;
89
import org.commonmark.renderer.NodeRenderer;
910
import org.commonmark.renderer.html.HtmlRenderer;
1011
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
1112
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
1213
import org.commonmark.renderer.markdown.MarkdownRenderer;
14+
import org.commonmark.renderer.text.TextContentRenderer;
1315

1416
import java.util.HashMap;
1517
import java.util.Locale;
@@ -48,7 +50,7 @@
4850
* }</pre>
4951
*/
5052
public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
51-
MarkdownRenderer.MarkdownRendererExtension {
53+
TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {
5254

5355
/**
5456
* The standard GitHub Flavored Markdown (GFM) types that the extension
@@ -95,6 +97,11 @@ public void extend(HtmlRenderer.Builder rendererBuilder) {
9597
rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, allowedTypes));
9698
}
9799

100+
@Override
101+
public void extend(TextContentRenderer.Builder rendererBuilder) {
102+
rendererBuilder.nodeRendererFactory(context -> new AlertTextContentNodeRenderer(context, allowedTypes));
103+
}
104+
98105
@Override
99106
public void extend(MarkdownRenderer.Builder rendererBuilder) {
100107
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {

commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map<String, String
2424
@Override
2525
protected void renderAlert(Alert alert) {
2626
var type = alert.getType();
27+
if (!allowedTypes.containsKey(type)) {
28+
throw new IllegalStateException("Unknown alert type: " + type);
29+
}
2730
var cssClass = type.toLowerCase();
2831

2932
htmlWriter.line();
@@ -40,7 +43,7 @@ protected void renderAlert(Alert alert) {
4043
if (first instanceof AlertTitle) {
4144
renderChildren(first);
4245
} else {
43-
htmlWriter.text(getAlertTitle(type));
46+
htmlWriter.text(allowedTypes.get(type));
4447
}
4548
htmlWriter.tag("/p");
4649
htmlWriter.line();
@@ -52,24 +55,8 @@ protected void renderAlert(Alert alert) {
5255
htmlWriter.line();
5356
}
5457

55-
private String getAlertTitle(String type) {
56-
var typeTitle = allowedTypes.get(type);
57-
if (typeTitle == null) {
58-
throw new IllegalStateException("Unknown alert type: " + type);
59-
}
60-
return typeTitle;
61-
}
62-
63-
private void renderChildren(Node parent) {
64-
var node = parent.getFirstChild();
65-
while (node != null) {
66-
var next = node.getNext();
67-
68-
// AlertTitle is rendered separately from other nodes.
69-
if (!(node instanceof AlertTitle)) {
70-
context.render(node);
71-
}
72-
node = next;
73-
}
58+
@Override
59+
protected void renderNode(Node node) {
60+
context.render(node);
7461
}
7562
}

commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,17 @@ protected void renderAlert(Alert alert) {
3030
renderChildren(first);
3131
}
3232

33-
writer.line();
34-
renderChildren(alert);
33+
if (alert.hasBody()) {
34+
writer.line();
35+
renderChildren(alert);
36+
}
37+
3538
writer.popPrefix();
3639
writer.block();
3740
}
3841

39-
private void renderChildren(Node parent) {
40-
var node = parent.getFirstChild();
41-
while (node != null) {
42-
var next = node.getNext();
43-
44-
// AlertTitle is rendered separately from other nodes.
45-
if (!(node instanceof AlertTitle)) {
46-
context.render(node);
47-
}
48-
node = next;
49-
}
42+
@Override
43+
protected void renderNode(Node node) {
44+
context.render(node);
5045
}
5146
}

commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.commonmark.ext.gfm.alerts.internal;
22

33
import org.commonmark.ext.gfm.alerts.Alert;
4+
import org.commonmark.ext.gfm.alerts.AlertTitle;
45
import org.commonmark.node.Node;
56
import org.commonmark.renderer.NodeRenderer;
67

@@ -20,4 +21,31 @@ public void render(Node node) {
2021
}
2122

2223
protected abstract void renderAlert(Alert alert);
24+
25+
/**
26+
* Renders the children of a parent node, excluding AlertTitle nodes.
27+
* AlertTitle is rendered separately from other content.
28+
*
29+
* @param parent the parent node whose children should be rendered
30+
*/
31+
protected final void renderChildren(Node parent) {
32+
var node = parent.getFirstChild();
33+
while (node != null) {
34+
var next = node.getNext();
35+
36+
// AlertTitle is rendered separately from other nodes.
37+
if (!(node instanceof AlertTitle)) {
38+
renderNode(node);
39+
}
40+
node = next;
41+
}
42+
}
43+
44+
/**
45+
* Renders a single node. Subclasses must implement this to delegate
46+
* to their context's render method.
47+
*
48+
* @param node the node to render
49+
*/
50+
protected abstract void renderNode(Node node);
2351
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.commonmark.ext.gfm.alerts.internal;
2+
3+
import org.commonmark.ext.gfm.alerts.Alert;
4+
import org.commonmark.ext.gfm.alerts.AlertTitle;
5+
import org.commonmark.node.Node;
6+
import org.commonmark.renderer.text.LineBreakRendering;
7+
import org.commonmark.renderer.text.TextContentNodeRendererContext;
8+
import org.commonmark.renderer.text.TextContentWriter;
9+
10+
import java.util.Map;
11+
12+
public class AlertTextContentNodeRenderer extends AlertNodeRenderer {
13+
14+
private final TextContentNodeRendererContext context;
15+
private final TextContentWriter textContent;
16+
private final Map<String, String> allowedTypes;
17+
18+
public AlertTextContentNodeRenderer(TextContentNodeRendererContext context, Map<String, String> allowedTypes) {
19+
this.context = context;
20+
this.textContent = context.getWriter();
21+
this.allowedTypes = allowedTypes;
22+
}
23+
24+
@Override
25+
protected void renderAlert(Alert alert) {
26+
var type = alert.getType();
27+
if (!allowedTypes.containsKey(type)) {
28+
throw new IllegalStateException("Unknown alert type: " + type);
29+
}
30+
31+
var first = alert.getFirstChild();
32+
if (first instanceof AlertTitle) {
33+
renderChildren(first);
34+
} else {
35+
textContent.write(allowedTypes.get(type));
36+
}
37+
38+
if (alert.hasBody()) {
39+
if (context.lineBreakRendering() == LineBreakRendering.STRIP) {
40+
textContent.write(": ");
41+
} else {
42+
textContent.block();
43+
}
44+
renderChildren(alert);
45+
}
46+
47+
textContent.block();
48+
}
49+
50+
@Override
51+
protected void renderNode(Node node) {
52+
context.render(node);
53+
}
54+
}

commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ public void alertWithList() {
7878
assertRoundTrip(input);
7979
}
8080

81+
@Test
82+
public void alertNoBody() {
83+
var input = "> [!NOTE]\n";
84+
assertRoundTrip(input);
85+
}
86+
87+
@Test
88+
public void alertNoBodyOtherBlocks() {
89+
var input = "- List\n\n> [!NOTE]\n\n- List\n";
90+
assertRoundTrip(input);
91+
}
92+
8193
// Custom titles
8294

8395
@Test
@@ -99,6 +111,18 @@ public void customTitleWithMultipleBlocks() {
99111
assertThat(rendered).isEqualTo(expected);
100112
}
101113

114+
@Test
115+
public void customTitleNoBody() {
116+
var input = "> [!WARNING] Custom title\n";
117+
assertRoundTripCustomTitles(input);
118+
}
119+
120+
@Test
121+
public void customTitleNoBodyOtherBlocks() {
122+
var input = "1. Ordered list\n\n> [!WARNING] Custom title\n\n1. Ordered list\n";
123+
assertRoundTripCustomTitles(input);
124+
}
125+
102126
// Helpers
103127

104128
private void assertRoundTrip(String input, Parser parser, MarkdownRenderer renderer) {

0 commit comments

Comments
 (0)