Skip to content

Commit b146e55

Browse files
authored
Merge pull request #435 from rdestefa/issue-431-disallow-github-alert-types
Allow Removing Certain GFM Alert Types
2 parents cd6633f + fa45a94 commit b146e55

6 files changed

Lines changed: 184 additions & 55 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ with the exception that 0.x versions can break between minor versions.
2020
other blocks (including other alerts). See
2121
[this section of the alerts README](./commonmark-ext-gfm-alerts/README.md#nesting-alerts)
2222
for more information.
23+
- New configuration for `AlertsExtension` to allow the set of alert types
24+
(including standard GFM types) to be completely overwritten.
25+
```java
26+
var extension = AlertsExtension.builder()
27+
.setAllowedTypes(Map.ofEntries(
28+
Map.entry("IMPORTANT", "Important"),
29+
Map.entry("WARNING", "Warning")
30+
Map.entry("BUG", "Known Bug")
31+
))
32+
.build();
33+
```
2334

2435
## [0.28.0] - 2026-03-31
2536
### Added

commonmark-ext-gfm-alerts/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ var extension = AlertsExtension.builder()
3636

3737
Custom types must be UPPERCASE. Standard type titles can also be overridden for localization.
3838

39+
The allowed types (including the five standard GFM types) can also be completely overwritten:
40+
41+
```java
42+
var extension = AlertsExtension.builder()
43+
.setAllowedTypes(Map.ofEntries(
44+
Map.entry("IMPORTANT", "Important"),
45+
Map.entry("WARNING", "Warning")
46+
Map.entry("BUG", "Known Bug")
47+
))
48+
.build();
49+
```
50+
3951
### Custom Alert Titles
4052

4153
Allow authors to provide custom titles per alert by adding text after the alert

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

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Locale;
1616
import java.util.HashSet;
1717
import java.util.Map;
18+
import java.util.Objects;
1819
import java.util.Set;
1920

2021
/**
@@ -49,14 +50,27 @@
4950
public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
5051
MarkdownRenderer.MarkdownRendererExtension {
5152

52-
static final Set<String> STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION");
53+
/**
54+
* The standard GitHub Flavored Markdown (GFM) types that the extension
55+
* enables by default. These can be overwritten with {@link Builder#setAllowedTypes(Map)}.
56+
*/
57+
public static final Map<String, String> STANDARD_TYPES = Map.ofEntries(
58+
Map.entry("NOTE", "Note"),
59+
Map.entry("TIP", "Tip"),
60+
Map.entry("IMPORTANT", "Important"),
61+
Map.entry("WARNING", "Warning"),
62+
Map.entry("CAUTION", "Caution")
63+
);
5364

54-
private final Map<String, String> customTypes;
65+
/**
66+
* A map of alert marker ({@code [!TYPE]}) to the default title for that marker.
67+
*/
68+
private final Map<String, String> allowedTypes;
5569
private final boolean customTitlesAllowed;
5670
private final boolean nestedAlertsAllowed;
5771

5872
private AlertsExtension(Builder builder) {
59-
this.customTypes = new HashMap<>(builder.customTypes);
73+
this.allowedTypes = new HashMap<>(builder.allowedTypes);
6074
this.customTitlesAllowed = builder.customTitlesAllowed;
6175
this.nestedAlertsAllowed = builder.nestedAlertsAllowed;
6276
}
@@ -71,15 +85,14 @@ public static Builder builder() {
7185

7286
@Override
7387
public void extend(Parser.Builder parserBuilder) {
74-
var allowedTypes = new HashSet<>(STANDARD_TYPES);
75-
allowedTypes.addAll(customTypes.keySet());
88+
var allowedTypesSet = new HashSet<>(allowedTypes.keySet());
7689
parserBuilder.customBlockParserFactory(
77-
new AlertBlockParser.Factory(allowedTypes, customTitlesAllowed, nestedAlertsAllowed));
90+
new AlertBlockParser.Factory(allowedTypesSet, customTitlesAllowed, nestedAlertsAllowed));
7891
}
7992

8093
@Override
8194
public void extend(HtmlRenderer.Builder rendererBuilder) {
82-
rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, customTypes));
95+
rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, allowedTypes));
8396
}
8497

8598
@Override
@@ -101,37 +114,57 @@ public Set<Character> getSpecialCharacters() {
101114
* Builder for configuring the alerts extension.
102115
*/
103116
public static class Builder {
104-
private final Map<String, String> customTypes = new HashMap<>();
117+
private Map<String, String> allowedTypes = new HashMap<>(STANDARD_TYPES);
105118
private boolean customTitlesAllowed = false;
106119
private boolean nestedAlertsAllowed = false;
107120

108121
/**
109-
* Adds a custom alert type with a display title.
122+
* Sets which alert types will be recognized and parsed into {@link Alert} blocks,
123+
* completely overwriting any previous configuration.
110124
* <p>
111-
* This can also be used to override the display title of standard GFM types
125+
* By default, {@link AlertsExtension#STANDARD_TYPES} are used.
126+
*
127+
* @param allowedTypes A map of alert type to the default title for that type.
128+
* Must not be null/empty or contain any null/empty keys or
129+
* values. Additionally, all alert types must be uppercase.
130+
* @return {@code this}
131+
* @see Builder#addCustomType(String, String)
132+
*/
133+
public Builder setAllowedTypes(Map<String, String> allowedTypes) {
134+
Objects.requireNonNull(allowedTypes, "allowedTypes must not be null");
135+
if (allowedTypes.isEmpty()) {
136+
throw new IllegalArgumentException("allowedTypes must not be empty");
137+
}
138+
139+
for (Map.Entry<String, String> entry : allowedTypes.entrySet()) {
140+
validateTypeAndTitle(entry.getKey(), entry.getValue());
141+
}
142+
143+
this.allowedTypes = new HashMap<>(allowedTypes);
144+
return this;
145+
}
146+
147+
/**
148+
* Adds a custom alert type with a default title.
149+
* <p>
150+
* This can also be used to override the default title of standard GFM types
112151
* (e.g., for localization).
113152
*
114153
* @param type the alert type (must be uppercase)
115-
* @param title the display title for this alert type
154+
* @param title the default title for this alert type
116155
* @return {@code this}
156+
* @see Builder#setAllowedTypes(Map)
117157
*/
118158
public Builder addCustomType(String type, String title) {
119-
if (type == null || type.isEmpty()) {
120-
throw new IllegalArgumentException("Type must not be null or empty");
121-
}
122-
if (title == null || title.isEmpty()) {
123-
throw new IllegalArgumentException("Title must not be null or empty");
124-
}
125-
if (!type.equals(type.toUpperCase(Locale.ROOT))) {
126-
throw new IllegalArgumentException("Type must be uppercase: " + type);
127-
}
128-
customTypes.put(type, title);
159+
validateTypeAndTitle(type, title);
160+
allowedTypes.put(type, title);
129161
return this;
130162
}
131163

132164
/**
133165
* Allows or disallows custom titles on alerts. Inline formatting is supported
134166
* within these titles.
167+
*
135168
* @param allow Whether to allow or disallow custom titles on alerts.
136169
* @return {@code this}
137170
* @see AlertTitle
@@ -149,6 +182,7 @@ public Builder allowCustomTitles(boolean allow) {
149182
* <p>
150183
* Note that even when this is allowed, {@link Parser.Builder#maxOpenBlockParsers(int)}
151184
* will be respected.
185+
*
152186
* @param allow Whether to allow or disallow parsing alerts within non-root blocks.
153187
* @return {@code this}
154188
*/
@@ -163,5 +197,29 @@ public Builder allowNestedAlerts(boolean allow) {
163197
public Extension build() {
164198
return new AlertsExtension(this);
165199
}
200+
201+
/**
202+
* Checks whether an alert type and default title are valid.
203+
*
204+
* @param type The type to validate:
205+
* <p>
206+
* - Must not be null or empty
207+
* - Must be uppercase
208+
* @param title The default title to validate. Must not be null or empty.
209+
*/
210+
private void validateTypeAndTitle(String type, String title) {
211+
Objects.requireNonNull(type, "Type must not be null");
212+
if (type.isEmpty()) {
213+
throw new IllegalArgumentException("Type must not be empty");
214+
}
215+
if (!type.equals(type.toUpperCase(Locale.ROOT))) {
216+
throw new IllegalArgumentException("Type must be uppercase: " + type);
217+
}
218+
219+
Objects.requireNonNull(title, "Default title must not be null: " + type);
220+
if (title.isEmpty()) {
221+
throw new IllegalArgumentException("Default title must not be empty: " + type);
222+
}
223+
}
166224
}
167225
}

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
@@ -13,12 +13,12 @@ public class AlertHtmlNodeRenderer extends AlertNodeRenderer {
1313

1414
private final HtmlWriter htmlWriter;
1515
private final HtmlNodeRendererContext context;
16-
private final Map<String, String> customTypeTitles;
16+
private final Map<String, String> allowedTypes;
1717

18-
public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map<String, String> customTypeTitles) {
18+
public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map<String, String> allowedTypes) {
1919
this.htmlWriter = context.getWriter();
2020
this.context = context;
21-
this.customTypeTitles = customTypeTitles;
21+
this.allowedTypes = allowedTypes;
2222
}
2323

2424
@Override
@@ -53,24 +53,11 @@ protected void renderAlert(Alert alert) {
5353
}
5454

5555
private String getAlertTitle(String type) {
56-
var customTypeTitle = customTypeTitles.get(type);
57-
if (customTypeTitle != null) {
58-
return customTypeTitle;
59-
}
60-
switch (type) {
61-
case "NOTE":
62-
return "Note";
63-
case "TIP":
64-
return "Tip";
65-
case "IMPORTANT":
66-
return "Important";
67-
case "WARNING":
68-
return "Warning";
69-
case "CAUTION":
70-
return "Caution";
71-
default:
72-
throw new IllegalStateException("Unknown alert type: " + type);
56+
var typeTitle = allowedTypes.get(type);
57+
if (typeTitle == null) {
58+
throw new IllegalStateException("Unknown alert type: " + type);
7359
}
60+
return typeTitle;
7461
}
7562

7663
private void renderChildren(Node parent) {

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,41 +40,41 @@ public void allStandardTypesRoundTrip() {
4040
@Test
4141
public void lowercaseTypeRendersAsUppercase() {
4242
// Lowercase input gets normalized to uppercase type
43-
String rendered = RENDERER.render(PARSER.parse("> [!note]\n> Content\n"));
43+
var rendered = RENDERER.render(PARSER.parse("> [!note]\n> Content\n"));
4444
assertThat(rendered).isEqualTo("> [!NOTE]\n> Content\n");
4545
}
4646

4747
@Test
4848
public void leadingAndTrailingLinesAreRemoved() {
49-
String rendered = RENDERER.render(PARSER.parse(">\n> \n>[!NOTE]\n> Content\n>\n> \n"));
49+
var rendered = RENDERER.render(PARSER.parse(">\n> \n>[!NOTE]\n> Content\n>\n> \n"));
5050
assertThat(rendered).isEqualTo("> [!NOTE]\n> Content\n");
5151
}
5252

5353
@Test
5454
public void alertWithMultipleParagraphs() {
55-
String input = "> [!NOTE]\n> First paragraph\n>\n> Second paragraph\n";
55+
var input = "> [!NOTE]\n> First paragraph\n>\n> Second paragraph\n";
5656
// MarkdownWriter always writes the prefix including trailing space
57-
String expected = "> [!NOTE]\n> First paragraph\n> \n> Second paragraph\n";
58-
String rendered = RENDERER.render(PARSER.parse(input));
57+
var expected = "> [!NOTE]\n> First paragraph\n> \n> Second paragraph\n";
58+
var rendered = RENDERER.render(PARSER.parse(input));
5959
assertThat(rendered).isEqualTo(expected);
6060
}
6161

6262
@Test
6363
public void customTypeRoundTrip() {
64-
Extension extension = AlertsExtension.builder()
64+
var extension = AlertsExtension.builder()
6565
.addCustomType("INFO", "Information")
6666
.build();
6767

68-
Parser parser = Parser.builder().extensions(Set.of(extension)).build();
69-
MarkdownRenderer renderer = MarkdownRenderer.builder().extensions(Set.of(extension)).build();
70-
String input = "> [!INFO]\n> Custom type\n";
68+
var parser = Parser.builder().extensions(Set.of(extension)).build();
69+
var renderer = MarkdownRenderer.builder().extensions(Set.of(extension)).build();
70+
var input = "> [!INFO]\n> Custom type\n";
7171

7272
assertRoundTrip(input, parser, renderer);
7373
}
7474

7575
@Test
7676
public void alertWithList() {
77-
String input = "> [!NOTE]\n> Items:\n> \n> - First\n> - Second\n";
77+
var input = "> [!NOTE]\n> Items:\n> \n> - First\n> - Second\n";
7878
assertRoundTrip(input);
7979
}
8080

@@ -92,17 +92,17 @@ public void customTitleWithFormattingRoundTrip() {
9292

9393
@Test
9494
public void customTitleWithMultipleBlocks() {
95-
String input = "> [!NOTE]Title\n> First paragraph\n>\n> Second paragraph\n>\n> - > Nested blocks\n";
95+
var input = "> [!NOTE]Title\n> First paragraph\n>\n> Second paragraph\n>\n> - > Nested blocks\n";
9696
// MarkdownWriter always writes the prefix including trailing space
97-
String expected = "> [!NOTE] Title\n> First paragraph\n> \n> Second paragraph\n> \n> - > Nested blocks\n";
98-
String rendered = RENDERER_CUSTOM_TITLES.render(PARSER_CUSTOM_TITLES.parse(input));
97+
var expected = "> [!NOTE] Title\n> First paragraph\n> \n> Second paragraph\n> \n> - > Nested blocks\n";
98+
var rendered = RENDERER_CUSTOM_TITLES.render(PARSER_CUSTOM_TITLES.parse(input));
9999
assertThat(rendered).isEqualTo(expected);
100100
}
101101

102102
// Helpers
103103

104104
private void assertRoundTrip(String input, Parser parser, MarkdownRenderer renderer) {
105-
String rendered = renderer.render(parser.parse(input));
105+
var rendered = renderer.render(parser.parse(input));
106106
assertThat(rendered).isEqualTo(input);
107107
}
108108

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.junit.jupiter.api.Test;
1313

1414
import java.util.List;
15+
import java.util.Map;
1516
import java.util.Set;
1617

1718
import static org.assertj.core.api.Assertions.assertThat;
@@ -137,6 +138,66 @@ public void customTypeTitleMustNotBeEmpty() {
137138
AlertsExtension.builder().addCustomType("INFO", "").build());
138139
}
139140

141+
// Overwriting types
142+
143+
@Test
144+
public void overwriteStandardTypes() {
145+
var allowedTypes = Map.ofEntries(Map.entry("IMPORTANT", "Important"));
146+
var extension = AlertsExtension.builder()
147+
.setAllowedTypes(allowedTypes)
148+
.addCustomType("BUG", "Known Bug")
149+
.build();
150+
var parser = Parser.builder().extensions(Set.of(extension)).build();
151+
var renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();
152+
153+
assertThat(renderer.render(parser.parse("> [!NOTE]\n> Regular block quote"))).isEqualTo(
154+
"<blockquote>\n" +
155+
"<p>[!NOTE]\n" +
156+
"Regular block quote</p>\n" +
157+
"</blockquote>\n");
158+
159+
assertThat(renderer.render(parser.parse("> [!TIP]\n> Regular block quote"))).isEqualTo(
160+
"<blockquote>\n" +
161+
"<p>[!TIP]\n" +
162+
"Regular block quote</p>\n" +
163+
"</blockquote>\n");
164+
165+
assertThat(renderer.render(parser.parse("> [!IMPORTANT]\n> Alert"))).isEqualTo(
166+
"<div class=\"markdown-alert markdown-alert-important\" data-alert-type=\"important\">\n" +
167+
"<p class=\"markdown-alert-title\">Important</p>\n" +
168+
"<p>Alert</p>\n" +
169+
"</div>\n");
170+
171+
assertThat(renderer.render(parser.parse("> [!BUG]\n> Alert"))).isEqualTo(
172+
"<div class=\"markdown-alert markdown-alert-bug\" data-alert-type=\"bug\">\n" +
173+
"<p class=\"markdown-alert-title\">Known Bug</p>\n" +
174+
"<p>Alert</p>\n" +
175+
"</div>\n");
176+
}
177+
178+
// Overwriting types validation
179+
180+
@Test
181+
public void overwriteTypesMustBeUppercase() {
182+
var allowedTypes = Map.ofEntries(Map.entry("info", "Info"));
183+
assertThrows(IllegalArgumentException.class, () ->
184+
AlertsExtension.builder().setAllowedTypes(allowedTypes).build());
185+
}
186+
187+
@Test
188+
public void overwriteTypesMustNotBeEmpty() {
189+
var allowedTypes = Map.ofEntries(Map.entry("", "Info"));
190+
assertThrows(IllegalArgumentException.class, () ->
191+
AlertsExtension.builder().setAllowedTypes(allowedTypes).build());
192+
}
193+
194+
@Test
195+
public void overwriteTypesTitleMustNotBeEmpty() {
196+
var allowedTypes = Map.ofEntries(Map.entry("INFO", ""));
197+
assertThrows(IllegalArgumentException.class, () ->
198+
AlertsExtension.builder().setAllowedTypes(allowedTypes).build());
199+
}
200+
140201
// Custom titles
141202

142203
@Test

0 commit comments

Comments
 (0)