Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ with the exception that 0.x versions can break between minor versions.
other blocks (including other alerts). See
[this section of the alerts README](./commonmark-ext-gfm-alerts/README.md#nesting-alerts)
for more information.
- New configuration for `AlertsExtension` to allow alert types (including standard
GFM types) to be removed (disallowed).
```java
var extension = AlertsExtension.builder().removeTypes("NOTE", "TIP").build();
```

## [0.28.0] - 2026-03-31
### Added
Expand Down
7 changes: 7 additions & 0 deletions commonmark-ext-gfm-alerts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ var extension = AlertsExtension.builder()

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

If any types (including the five standard GFM types) aren't desired, they can be
removed (disallowed):

```java
var extension = AlertsExtension.builder().removeTypes("NOTE", "TIP").build();
```

### Custom Alert Titles

Allow authors to provide custom titles per alert by adding text after the alert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,28 @@
public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
MarkdownRenderer.MarkdownRendererExtension {

static final Set<String> STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION");
/**
* The standard GitHub Flavored Markdown (GFM) types that the extension
* enables by default. They can be removed individually with
* {@link Builder#removeTypes(String...)}.
*/
public static final Map<String, String> STANDARD_TYPES = Map.ofEntries(
Map.entry("NOTE", "Note"),
Map.entry("TIP", "Tip"),
Map.entry("IMPORTANT", "Important"),
Map.entry("WARNING", "Warning"),
Map.entry("CAUTION", "Caution")
);

private final Map<String, String> customTypes;
/**
* A map of alert marker ({@code [!TYPE]}) to the default title for that marker.
*/
private final Map<String, String> allowedTypes;
private final boolean customTitlesAllowed;
private final boolean nestedAlertsAllowed;

private AlertsExtension(Builder builder) {
this.customTypes = new HashMap<>(builder.customTypes);
this.allowedTypes = new HashMap<>(builder.allowedTypes);
this.customTitlesAllowed = builder.customTitlesAllowed;
this.nestedAlertsAllowed = builder.nestedAlertsAllowed;
}
Expand All @@ -71,15 +85,14 @@ public static Builder builder() {

@Override
public void extend(Parser.Builder parserBuilder) {
var allowedTypes = new HashSet<>(STANDARD_TYPES);
allowedTypes.addAll(customTypes.keySet());
var allowedTypesSet = new HashSet<>(allowedTypes.keySet());
parserBuilder.customBlockParserFactory(
new AlertBlockParser.Factory(allowedTypes, customTitlesAllowed, nestedAlertsAllowed));
new AlertBlockParser.Factory(allowedTypesSet, customTitlesAllowed, nestedAlertsAllowed));
}

@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, customTypes));
rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, allowedTypes));
}

@Override
Expand All @@ -101,18 +114,18 @@ public Set<Character> getSpecialCharacters() {
* Builder for configuring the alerts extension.
*/
public static class Builder {
private final Map<String, String> customTypes = new HashMap<>();
private final Map<String, String> allowedTypes = new HashMap<>(STANDARD_TYPES);
private boolean customTitlesAllowed = false;
private boolean nestedAlertsAllowed = false;

/**
* Adds a custom alert type with a display title.
* Adds a custom alert type with a default title.
* <p>
* This can also be used to override the display title of standard GFM types
* This can also be used to override the default title of standard GFM types
* (e.g., for localization).
*
* @param type the alert type (must be uppercase)
* @param title the display title for this alert type
* @param title the default title for this alert type
* @return {@code this}
*/
public Builder addCustomType(String type, String title) {
Expand All @@ -125,7 +138,34 @@ public Builder addCustomType(String type, String title) {
if (!type.equals(type.toUpperCase(Locale.ROOT))) {
throw new IllegalArgumentException("Type must be uppercase: " + type);
}
customTypes.put(type, title);
allowedTypes.put(type, title);
return this;
}

/**
* Removes alert types from the allowed list.
*
* @param types the alert types to remove (must be uppercase)
* @return {@code this}
* @see AlertsExtension#STANDARD_TYPES
*/
public Builder removeTypes(String... types) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if it would be nicer if the API was just setAllowedTypes(Map<String, String>) instead, which would allow you to set the exact types you want (including custom ones). It would just override the whole map. With the current API in some cases you'd have to do remove+add. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! Agree that setAllowedTypes(Map<String, String>) would be better than removeTypes(String...). I think addCustomType would still be good to keep so users who just want to add 1 or 2 more types don't need to completely recreate the set, but I made two commits so you can see which you think works best:

  • d665735 - Replaces removeTypes with setAllowedTypes, but keeps addCustomType
  • 9c813f6 - Additionally removes addCustomType

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah yeah I think addCustomType can stay.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

if (types == null) {
throw new IllegalArgumentException("Types must not be null");
}

for (String type : types) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("Each type must not be null or empty");
}

if (!type.equals(type.toUpperCase(Locale.ROOT))) {
throw new IllegalArgumentException("Type must be uppercase: " + type);
}

allowedTypes.remove(type);
}

return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ public class AlertHtmlNodeRenderer extends AlertNodeRenderer {

private final HtmlWriter htmlWriter;
private final HtmlNodeRendererContext context;
private final Map<String, String> customTypeTitles;
private final Map<String, String> allowedTypes;

public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map<String, String> customTypeTitles) {
public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map<String, String> allowedTypes) {
this.htmlWriter = context.getWriter();
this.context = context;
this.customTypeTitles = customTypeTitles;
this.allowedTypes = allowedTypes;
}

@Override
Expand Down Expand Up @@ -53,24 +53,11 @@ protected void renderAlert(Alert alert) {
}

private String getAlertTitle(String type) {
var customTypeTitle = customTypeTitles.get(type);
if (customTypeTitle != null) {
return customTypeTitle;
}
switch (type) {
case "NOTE":
return "Note";
case "TIP":
return "Tip";
case "IMPORTANT":
return "Important";
case "WARNING":
return "Warning";
case "CAUTION":
return "Caution";
default:
throw new IllegalStateException("Unknown alert type: " + type);
var typeTitle = allowedTypes.get(type);
if (typeTitle == null) {
throw new IllegalStateException("Unknown alert type: " + type);
}
return typeTitle;
}

private void renderChildren(Node parent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,31 @@ public void customTypeTitleMustNotBeEmpty() {
AlertsExtension.builder().addCustomType("INFO", "").build());
}

@Test
public void removeStandardTypes() {
var extension = AlertsExtension.builder().removeTypes("NOTE", "TIP").build();
var parser = Parser.builder().extensions(Set.of(extension)).build();
var renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();

assertThat(renderer.render(parser.parse("> [!NOTE]\n> Regular block quote"))).isEqualTo(
"<blockquote>\n" +
"<p>[!NOTE]\n" +
"Regular block quote</p>\n" +
"</blockquote>\n");

assertThat(renderer.render(parser.parse("> [!TIP]\n> Regular block quote"))).isEqualTo(
"<blockquote>\n" +
"<p>[!TIP]\n" +
"Regular block quote</p>\n" +
"</blockquote>\n");

assertThat(renderer.render(parser.parse("> [!IMPORTANT]\n> Alert"))).isEqualTo(
"<div class=\"markdown-alert markdown-alert-important\" data-alert-type=\"important\">\n" +
"<p class=\"markdown-alert-title\">Important</p>\n" +
"<p>Alert</p>\n" +
"</div>\n");
}

// Custom titles

@Test
Expand Down
Loading