Skip to content

Commit 057f13d

Browse files
ia3andyclaude
andcommitted
Add GFM alerts extension
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e62bca1 commit 057f13d

File tree

16 files changed

+1394
-0
lines changed

16 files changed

+1394
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# commonmark-ext-gfm-alerts
2+
3+
Extension for [commonmark-java](https://github.com/commonmark/commonmark-java) that adds support for [GitHub Flavored Markdown alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts).
4+
5+
Enables highlighting important information using blockquote syntax with five standard alert types: NOTE, TIP, IMPORTANT, WARNING, and CAUTION.
6+
7+
## Usage
8+
9+
#### Markdown Syntax
10+
11+
```markdown
12+
> [!NOTE]
13+
> Useful information
14+
15+
> [!WARNING]
16+
> Critical information
17+
```
18+
19+
#### Standard GFM Types
20+
21+
```java
22+
Extension extension = AlertsExtension.create();
23+
Parser parser = Parser.builder().extensions(List.of(extension)).build();
24+
HtmlRenderer renderer = HtmlRenderer.builder().extensions(List.of(extension)).build();
25+
```
26+
27+
#### Custom Alert Types
28+
29+
Add custom types beyond the five standard GFM types:
30+
31+
```java
32+
Extension extension = AlertsExtension.builder()
33+
.addCustomType("INFO", "Information")
34+
.build();
35+
```
36+
37+
Custom types must be UPPERCASE and cannot override standard types.
38+
39+
#### Styling
40+
41+
Alerts render as `<div>` elements with CSS classes:
42+
43+
```html
44+
<div class="markdown-alert markdown-alert-note" data-alert-type="note">
45+
<p class="markdown-alert-title">Note</p>
46+
<p>Content</p>
47+
</div>
48+
```
49+
50+
Basic CSS example:
51+
52+
```css
53+
.markdown-alert {
54+
padding: 0.5rem 1rem;
55+
margin-bottom: 1rem;
56+
border-left: 4px solid;
57+
}
58+
59+
.markdown-alert-note { border-color: #0969da; background-color: #ddf4ff; }
60+
.markdown-alert-tip { border-color: #1a7f37; background-color: #dcffe4; }
61+
.markdown-alert-important { border-color: #8250df; background-color: #f6f0ff; }
62+
.markdown-alert-warning { border-color: #9a6700; background-color: #fff8c5; }
63+
.markdown-alert-caution { border-color: #cf222e; background-color: #ffebe9; }
64+
```
65+
66+
Icons can be added using CSS `::before` pseudo-elements with GitHub's [Octicons](https://primer.style/octicons/) (info, light-bulb, report, alert, stop icons).
67+
68+
## License
69+
70+
See the main commonmark-java project for license information.

commonmark-ext-gfm-alerts/pom.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>org.commonmark</groupId>
6+
<artifactId>commonmark-parent</artifactId>
7+
<version>0.27.2-SNAPSHOT</version>
8+
</parent>
9+
10+
<artifactId>commonmark-ext-gfm-alerts</artifactId>
11+
<name>commonmark-java extension for alerts</name>
12+
<description>commonmark-java extension for GFM alerts (admonition blocks) using [!TYPE] syntax (GitHub Flavored Markdown)</description>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>org.commonmark</groupId>
17+
<artifactId>commonmark</artifactId>
18+
</dependency>
19+
20+
<dependency>
21+
<groupId>org.commonmark</groupId>
22+
<artifactId>commonmark-test-util</artifactId>
23+
<scope>test</scope>
24+
</dependency>
25+
</dependencies>
26+
27+
</project>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module org.commonmark.ext.gfm.alerts {
2+
exports org.commonmark.ext.gfm.alerts;
3+
4+
requires transitive org.commonmark;
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.commonmark.ext.gfm.alerts;
2+
3+
import org.commonmark.node.CustomBlock;
4+
5+
/**
6+
* Alert block for highlighting important information using {@code [!TYPE]} syntax.
7+
*/
8+
public class Alert extends CustomBlock {
9+
10+
private final String type;
11+
12+
public Alert(String type) {
13+
this.type = type;
14+
}
15+
16+
public String getType() {
17+
return type;
18+
}
19+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package org.commonmark.ext.gfm.alerts;
2+
3+
import org.commonmark.Extension;
4+
import org.commonmark.ext.gfm.alerts.internal.AlertPostProcessor;
5+
import org.commonmark.ext.gfm.alerts.internal.AlertHtmlNodeRenderer;
6+
import org.commonmark.ext.gfm.alerts.internal.AlertMarkdownNodeRenderer;
7+
import org.commonmark.parser.Parser;
8+
import org.commonmark.renderer.NodeRenderer;
9+
import org.commonmark.renderer.html.HtmlNodeRendererContext;
10+
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
11+
import org.commonmark.renderer.html.HtmlRenderer;
12+
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
13+
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
14+
import org.commonmark.renderer.markdown.MarkdownRenderer;
15+
16+
import java.util.HashSet;
17+
import java.util.LinkedHashMap;
18+
import java.util.Map;
19+
import java.util.Set;
20+
21+
/**
22+
* Extension for GFM alerts using {@code [!TYPE]} syntax (GitHub Flavored Markdown).
23+
* <p>
24+
* Create with {@link #create()} or {@link #builder()} and configure on builders
25+
* ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)},
26+
* {@link HtmlRenderer.Builder#extensions(Iterable)}).
27+
* Parsed alerts become {@link Alert} blocks.
28+
*/
29+
public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
30+
MarkdownRenderer.MarkdownRendererExtension {
31+
32+
static final Set<String> STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION");
33+
34+
private final Map<String, String> customTypes;
35+
36+
private AlertsExtension(Builder builder) {
37+
this.customTypes = new LinkedHashMap<>(builder.customTypes);
38+
}
39+
40+
public static Extension create() {
41+
return builder().build();
42+
}
43+
44+
public static Builder builder() {
45+
return new Builder();
46+
}
47+
48+
@Override
49+
public void extend(Parser.Builder parserBuilder) {
50+
Set<String> allowedTypes = new HashSet<>(STANDARD_TYPES);
51+
allowedTypes.addAll(customTypes.keySet());
52+
parserBuilder.postProcessor(new AlertPostProcessor(allowedTypes));
53+
}
54+
55+
@Override
56+
public void extend(HtmlRenderer.Builder rendererBuilder) {
57+
rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() {
58+
@Override
59+
public NodeRenderer create(HtmlNodeRendererContext context) {
60+
return new AlertHtmlNodeRenderer(context, customTypes);
61+
}
62+
});
63+
}
64+
65+
@Override
66+
public void extend(MarkdownRenderer.Builder rendererBuilder) {
67+
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
68+
@Override
69+
public NodeRenderer create(MarkdownNodeRendererContext context) {
70+
return new AlertMarkdownNodeRenderer(context);
71+
}
72+
73+
@Override
74+
public Set<Character> getSpecialCharacters() {
75+
return Set.of();
76+
}
77+
});
78+
}
79+
80+
/**
81+
* Builder for configuring the alerts extension.
82+
*/
83+
public static class Builder {
84+
private final Map<String, String> customTypes = new LinkedHashMap<>();
85+
86+
/**
87+
* Adds a custom alert type with a display title.
88+
* <p>
89+
* This can also be used to override the display title of standard GFM types
90+
* (e.g., for localization).
91+
*
92+
* @param type the alert type (must be uppercase)
93+
* @param title the display title for this alert type
94+
* @return {@code this}
95+
*/
96+
public Builder addCustomType(String type, String title) {
97+
if (type == null || type.isEmpty()) {
98+
throw new IllegalArgumentException("Type must not be null or empty");
99+
}
100+
if (title == null || title.isEmpty()) {
101+
throw new IllegalArgumentException("Title must not be null or empty");
102+
}
103+
if (!type.equals(type.toUpperCase())) {
104+
throw new IllegalArgumentException("Type must be uppercase: " + type);
105+
}
106+
customTypes.put(type, title);
107+
return this;
108+
}
109+
110+
/**
111+
* @return a configured {@link Extension}
112+
*/
113+
public Extension build() {
114+
return new AlertsExtension(this);
115+
}
116+
}
117+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.commonmark.ext.gfm.alerts.internal;
2+
3+
import org.commonmark.ext.gfm.alerts.Alert;
4+
import org.commonmark.node.Node;
5+
import org.commonmark.renderer.html.HtmlNodeRendererContext;
6+
import org.commonmark.renderer.html.HtmlWriter;
7+
8+
import java.util.LinkedHashMap;
9+
import java.util.Map;
10+
11+
public class AlertHtmlNodeRenderer extends AlertNodeRenderer {
12+
13+
private final HtmlWriter htmlWriter;
14+
private final HtmlNodeRendererContext context;
15+
private final Map<String, String> customTypeTitles;
16+
17+
public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map<String, String> customTypeTitles) {
18+
this.htmlWriter = context.getWriter();
19+
this.context = context;
20+
this.customTypeTitles = customTypeTitles;
21+
}
22+
23+
@Override
24+
protected void renderAlert(Alert alert) {
25+
String type = alert.getType();
26+
String cssClass = type.toLowerCase();
27+
28+
htmlWriter.line();
29+
Map<String, String> attributes = new LinkedHashMap<>();
30+
attributes.put("class", "markdown-alert markdown-alert-" + cssClass);
31+
attributes.put("data-alert-type", cssClass);
32+
33+
htmlWriter.tag("div", context.extendAttributes(alert, "div", attributes));
34+
htmlWriter.line();
35+
36+
// Render alert title
37+
htmlWriter.tag("p", Map.of("class", "markdown-alert-title"));
38+
htmlWriter.text(getAlertTitle(type));
39+
htmlWriter.tag("/p");
40+
htmlWriter.line();
41+
42+
// Render children (the alert content)
43+
renderChildren(alert);
44+
45+
htmlWriter.tag("/div");
46+
htmlWriter.line();
47+
}
48+
49+
private String getAlertTitle(String type) {
50+
if (customTypeTitles.containsKey(type)) {
51+
return customTypeTitles.get(type);
52+
}
53+
switch (type) {
54+
case "NOTE":
55+
return "Note";
56+
case "TIP":
57+
return "Tip";
58+
case "IMPORTANT":
59+
return "Important";
60+
case "WARNING":
61+
return "Warning";
62+
case "CAUTION":
63+
return "Caution";
64+
default:
65+
throw new IllegalStateException("Unknown alert type: " + type);
66+
}
67+
}
68+
69+
private void renderChildren(Node parent) {
70+
Node node = parent.getFirstChild();
71+
while (node != null) {
72+
Node next = node.getNext();
73+
context.render(node);
74+
node = next;
75+
}
76+
}
77+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.commonmark.ext.gfm.alerts.internal;
2+
3+
import org.commonmark.ext.gfm.alerts.Alert;
4+
import org.commonmark.node.Node;
5+
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
6+
import org.commonmark.renderer.markdown.MarkdownWriter;
7+
8+
public class AlertMarkdownNodeRenderer extends AlertNodeRenderer {
9+
10+
private final MarkdownWriter writer;
11+
private final MarkdownNodeRendererContext context;
12+
13+
public AlertMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
14+
this.writer = context.getWriter();
15+
this.context = context;
16+
}
17+
18+
@Override
19+
protected void renderAlert(Alert alert) {
20+
// First line: > [!TYPE]
21+
writer.writePrefix("> ");
22+
writer.pushPrefix("> ");
23+
writer.raw("[!" + alert.getType() + "]");
24+
writer.line();
25+
renderChildren(alert);
26+
writer.popPrefix();
27+
writer.block();
28+
}
29+
30+
private void renderChildren(Node parent) {
31+
Node node = parent.getFirstChild();
32+
while (node != null) {
33+
Node next = node.getNext();
34+
context.render(node);
35+
node = next;
36+
}
37+
}
38+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.commonmark.ext.gfm.alerts.internal;
2+
3+
import org.commonmark.ext.gfm.alerts.Alert;
4+
import org.commonmark.renderer.NodeRenderer;
5+
6+
import java.util.Set;
7+
8+
public abstract class AlertNodeRenderer implements NodeRenderer {
9+
10+
@Override
11+
public Set<Class<? extends org.commonmark.node.Node>> getNodeTypes() {
12+
return Set.of(Alert.class);
13+
}
14+
15+
@Override
16+
public void render(org.commonmark.node.Node node) {
17+
Alert alert = (Alert) node;
18+
renderAlert(alert);
19+
}
20+
21+
protected abstract void renderAlert(Alert alert);
22+
}

0 commit comments

Comments
 (0)