Skip to content

Commit 471e639

Browse files
authored
Merge pull request #420 from ia3andy/gfm-alerts
Add GFM alerts extension
2 parents f40ff54 + 61200f9 commit 471e639

File tree

20 files changed

+1725
-0
lines changed

20 files changed

+1725
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
var extension = AlertsExtension.create();
23+
var parser = Parser.builder().extensions(List.of(extension)).build();
24+
var 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+
var extension = AlertsExtension.builder()
33+
.addCustomType("BUG", "Known Bug")
34+
.build();
35+
```
36+
37+
Custom types must be UPPERCASE. Standard type titles can also be overridden for localization.
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+
![Alerts](screenshots/alerts.png)
67+
68+
Icons can be added using GitHub's [Octicons](https://primer.style/octicons/):
69+
70+
![Alerts with icons](screenshots/alerts-with-icons.png)
71+
72+
## License
73+
74+
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>
20.1 KB
Loading
19.4 KB
Loading
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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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.HashMap;
17+
import java.util.Locale;
18+
import java.util.HashSet;
19+
import java.util.Map;
20+
import java.util.Set;
21+
22+
/**
23+
* Extension for GFM alerts using {@code [!TYPE]} syntax (GitHub Flavored Markdown).
24+
* <p>
25+
* Create with {@link #create()} or {@link #builder()} and configure on builders
26+
* ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)},
27+
* {@link HtmlRenderer.Builder#extensions(Iterable)}).
28+
* Parsed alerts become {@link Alert} blocks.
29+
*/
30+
public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
31+
MarkdownRenderer.MarkdownRendererExtension {
32+
33+
static final Set<String> STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION");
34+
35+
private final Map<String, String> customTypes;
36+
37+
private AlertsExtension(Builder builder) {
38+
this.customTypes = new HashMap<>(builder.customTypes);
39+
}
40+
41+
public static Extension create() {
42+
return builder().build();
43+
}
44+
45+
public static Builder builder() {
46+
return new Builder();
47+
}
48+
49+
@Override
50+
public void extend(Parser.Builder parserBuilder) {
51+
var allowedTypes = new HashSet<>(STANDARD_TYPES);
52+
allowedTypes.addAll(customTypes.keySet());
53+
parserBuilder.postProcessor(new AlertPostProcessor(allowedTypes));
54+
}
55+
56+
@Override
57+
public void extend(HtmlRenderer.Builder rendererBuilder) {
58+
rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() {
59+
@Override
60+
public NodeRenderer create(HtmlNodeRendererContext context) {
61+
return new AlertHtmlNodeRenderer(context, customTypes);
62+
}
63+
});
64+
}
65+
66+
@Override
67+
public void extend(MarkdownRenderer.Builder rendererBuilder) {
68+
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
69+
@Override
70+
public NodeRenderer create(MarkdownNodeRendererContext context) {
71+
return new AlertMarkdownNodeRenderer(context);
72+
}
73+
74+
@Override
75+
public Set<Character> getSpecialCharacters() {
76+
return Set.of();
77+
}
78+
});
79+
}
80+
81+
/**
82+
* Builder for configuring the alerts extension.
83+
*/
84+
public static class Builder {
85+
private final Map<String, String> customTypes = new HashMap<>();
86+
87+
/**
88+
* Adds a custom alert type with a display title.
89+
* <p>
90+
* This can also be used to override the display title of standard GFM types
91+
* (e.g., for localization).
92+
*
93+
* @param type the alert type (must be uppercase)
94+
* @param title the display title for this alert type
95+
* @return {@code this}
96+
*/
97+
public Builder addCustomType(String type, String title) {
98+
if (type == null || type.isEmpty()) {
99+
throw new IllegalArgumentException("Type must not be null or empty");
100+
}
101+
if (title == null || title.isEmpty()) {
102+
throw new IllegalArgumentException("Title must not be null or empty");
103+
}
104+
if (!type.equals(type.toUpperCase(Locale.ROOT))) {
105+
throw new IllegalArgumentException("Type must be uppercase: " + type);
106+
}
107+
customTypes.put(type, title);
108+
return this;
109+
}
110+
111+
/**
112+
* @return a configured {@link Extension}
113+
*/
114+
public Extension build() {
115+
return new AlertsExtension(this);
116+
}
117+
}
118+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
var type = alert.getType();
26+
var cssClass = type.toLowerCase();
27+
28+
htmlWriter.line();
29+
var attributes = new LinkedHashMap<String, String>();
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+
var customTypeTitle = customTypeTitles.get(type);
51+
if (customTypeTitle != null) {
52+
return customTypeTitle;
53+
}
54+
switch (type) {
55+
case "NOTE":
56+
return "Note";
57+
case "TIP":
58+
return "Tip";
59+
case "IMPORTANT":
60+
return "Important";
61+
case "WARNING":
62+
return "Warning";
63+
case "CAUTION":
64+
return "Caution";
65+
default:
66+
throw new IllegalStateException("Unknown alert type: " + type);
67+
}
68+
}
69+
70+
private void renderChildren(Node parent) {
71+
var node = parent.getFirstChild();
72+
while (node != null) {
73+
var next = node.getNext();
74+
context.render(node);
75+
node = next;
76+
}
77+
}
78+
}
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+
var node = parent.getFirstChild();
32+
while (node != null) {
33+
var next = node.getNext();
34+
context.render(node);
35+
node = next;
36+
}
37+
}
38+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.NodeRenderer;
6+
7+
import java.util.Set;
8+
9+
public abstract class AlertNodeRenderer implements NodeRenderer {
10+
11+
@Override
12+
public Set<Class<? extends Node>> getNodeTypes() {
13+
return Set.of(Alert.class);
14+
}
15+
16+
@Override
17+
public void render(Node node) {
18+
var alert = (Alert) node;
19+
renderAlert(alert);
20+
}
21+
22+
protected abstract void renderAlert(Alert alert);
23+
}

0 commit comments

Comments
 (0)