+```
+
+Basic CSS example:
+
+```css
+.markdown-alert {
+ padding: 0.5rem 1rem;
+ margin-bottom: 1rem;
+ border-left: 4px solid;
+}
+
+.markdown-alert-note { border-color: #0969da; background-color: #ddf4ff; }
+.markdown-alert-tip { border-color: #1a7f37; background-color: #dcffe4; }
+.markdown-alert-important { border-color: #8250df; background-color: #f6f0ff; }
+.markdown-alert-warning { border-color: #9a6700; background-color: #fff8c5; }
+.markdown-alert-caution { border-color: #cf222e; background-color: #ffebe9; }
+```
+
+Icons can be added using CSS `::before` pseudo-elements with GitHub's [Octicons](https://primer.style/octicons/) (info, light-bulb, report, alert, stop icons).
+
+## License
+
+See the main commonmark-java project for license information.
diff --git a/commonmark-ext-gfm-alerts/pom.xml b/commonmark-ext-gfm-alerts/pom.xml
new file mode 100644
index 00000000..5235af6b
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/pom.xml
@@ -0,0 +1,27 @@
+
+
diff --git a/commonmark-ext-gfm-alerts/src/main/java/module-info.java b/commonmark-ext-gfm-alerts/src/main/java/module-info.java
new file mode 100644
index 00000000..e8b5aecb
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/module-info.java
@@ -0,0 +1,5 @@
+module org.commonmark.ext.gfm.alerts {
+ exports org.commonmark.ext.gfm.alerts;
+
+ requires transitive org.commonmark;
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java
new file mode 100644
index 00000000..90af2e22
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java
@@ -0,0 +1,27 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.node.CustomBlock;
+
+import java.util.Set;
+
+/**
+ * Alert block for highlighting important information using {@code [!TYPE]} syntax.
+ */
+public class Alert extends CustomBlock {
+
+ public static final Set
STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION");
+
+ private String type;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public boolean isStandardType() {
+ return STANDARD_TYPES.contains(type);
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java
new file mode 100644
index 00000000..96d57b36
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java
@@ -0,0 +1,123 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.Extension;
+import org.commonmark.ext.gfm.alerts.internal.AlertBlockParser;
+import org.commonmark.ext.gfm.alerts.internal.AlertHtmlNodeRenderer;
+import org.commonmark.ext.gfm.alerts.internal.AlertMarkdownNodeRenderer;
+import org.commonmark.ext.gfm.alerts.internal.AlertTextContentNodeRenderer;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.commonmark.renderer.text.TextContentRenderer;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Extension for GFM alerts using {@code [!TYPE]} syntax (GitHub Flavored Markdown).
+ *
+ * Create with {@link #create()} or {@link #builder()} and configure on builders
+ * ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)},
+ * {@link HtmlRenderer.Builder#extensions(Iterable)}).
+ * Parsed alerts become {@link Alert} blocks.
+ */
+public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
+ TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {
+
+ private final Map customTypes;
+
+ private AlertsExtension(Builder builder) {
+ this.customTypes = new LinkedHashMap<>(builder.customTypes);
+ }
+
+ public static Extension create() {
+ return new AlertsExtension(builder());
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public Map getCustomTypes() {
+ return Collections.unmodifiableMap(customTypes);
+ }
+
+ @Override
+ public void extend(Parser.Builder parserBuilder) {
+ parserBuilder.customBlockParserFactory(new AlertBlockParser.Factory(this));
+ }
+
+ @Override
+ public void extend(HtmlRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, customTypes));
+ }
+
+ @Override
+ public void extend(TextContentRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(AlertTextContentNodeRenderer::new);
+ }
+
+ @Override
+ public void extend(MarkdownRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
+ @Override
+ public NodeRenderer create(MarkdownNodeRendererContext context) {
+ return new AlertMarkdownNodeRenderer(context);
+ }
+
+ @Override
+ public Set getSpecialCharacters() {
+ return Set.of();
+ }
+ });
+ }
+
+ /**
+ * Builder for configuring the alerts extension.
+ */
+ public static class Builder {
+ private final Map customTypes = new LinkedHashMap<>();
+
+ /**
+ * Adds a custom alert type with a display title.
+ *
+ * Custom types must:
+ *
+ * - Be UPPERCASE (e.g., "INFO", "SUCCESS")
+ * - Not conflict with standard GFM types (NOTE, TIP, IMPORTANT, WARNING, CAUTION)
+ *
+ *
+ * @param type the alert type (must be uppercase)
+ * @param title the display title for this alert type
+ * @return {@code this}
+ */
+ public Builder addCustomType(String type, String title) {
+ if (type == null || type.isEmpty()) {
+ throw new IllegalArgumentException("Type must not be null or empty");
+ }
+ if (title == null || title.isEmpty()) {
+ throw new IllegalArgumentException("Title must not be null or empty");
+ }
+ if (!type.equals(type.toUpperCase())) {
+ throw new IllegalArgumentException("Type must be uppercase: " + type);
+ }
+ if (Alert.STANDARD_TYPES.contains(type)) {
+ throw new IllegalArgumentException("Cannot override standard GFM type: " + type);
+ }
+ customTypes.put(type, title);
+ return this;
+ }
+
+ /**
+ * @return a configured {@link Extension}
+ */
+ public Extension build() {
+ return new AlertsExtension(this);
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertBlockParser.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertBlockParser.java
new file mode 100644
index 00000000..30f417ef
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertBlockParser.java
@@ -0,0 +1,121 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.ext.gfm.alerts.AlertsExtension;
+import org.commonmark.node.Block;
+import org.commonmark.parser.block.*;
+import org.commonmark.text.Characters;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AlertBlockParser extends AbstractBlockParser {
+
+ private static final Pattern ALERT_PATTERN = Pattern.compile("^\\[!([A-Z]+)]$");
+
+ // Duplicates org.commonmark.internal.util.Parsing.CODE_BLOCK_INDENT which is not accessible
+ private static final int CODE_BLOCK_INDENT = 4;
+
+ private final Alert block = new Alert();
+
+ public AlertBlockParser(String type) {
+ if (type == null) {
+ throw new IllegalArgumentException("Alert type must not be null");
+ }
+ block.setType(type);
+ }
+
+ @Override
+ public boolean isContainer() {
+ return true;
+ }
+
+ @Override
+ public boolean canContain(Block block) {
+ return true;
+ }
+
+ @Override
+ public Block getBlock() {
+ return block;
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState state) {
+ int nextNonSpace = state.getNextNonSpaceIndex();
+ if (isMarker(state, nextNonSpace)) {
+ int newColumn = state.getColumn() + state.getIndent() + 1;
+ // optional following space or tab
+ if (Characters.isSpaceOrTab(state.getLine().getContent(), nextNonSpace + 1)) {
+ newColumn++;
+ }
+ return BlockContinue.atColumn(newColumn);
+ } else {
+ return BlockContinue.none();
+ }
+ }
+
+ /**
+ * Checks if the character at the given index is a blockquote marker ('>').
+ *
+ * @param state the parser state
+ * @param index the index to check
+ * @return true if the character is '>' and indentation is less than CODE_BLOCK_INDENT
+ */
+ private static boolean isMarker(ParserState state, int index) {
+ CharSequence line = state.getLine().getContent();
+ return state.getIndent() < CODE_BLOCK_INDENT && index < line.length() && line.charAt(index) == '>';
+ }
+
+ /**
+ * Factory for creating alert block parsers.
+ */
+ public static class Factory extends AbstractBlockParserFactory {
+ private final Set allowedTypes;
+
+ public Factory(AlertsExtension extension) {
+ // Combine standard GFM types with custom types
+ this.allowedTypes = new HashSet<>(Alert.STANDARD_TYPES);
+ this.allowedTypes.addAll(extension.getCustomTypes().keySet());
+ }
+
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
+ int nextNonSpace = state.getNextNonSpaceIndex();
+ if (!isMarker(state, nextNonSpace)) {
+ return BlockStart.none();
+ }
+
+ // Check if this is an alert by looking for [!TYPE] pattern
+ CharSequence line = state.getLine().getContent();
+ int contentStart = nextNonSpace + 1;
+ // optional following space or tab
+ if (Characters.isSpaceOrTab(line, contentStart)) {
+ contentStart++;
+ }
+
+ // Look for [!TYPE] pattern after trimming whitespace
+ if (contentStart < line.length()) {
+ String content = line.subSequence(contentStart, line.length()).toString().trim();
+ Matcher matcher = ALERT_PATTERN.matcher(content);
+ if (matcher.matches()) {
+ String type = matcher.group(1);
+
+ // Check if this type is allowed (standard or custom)
+ if (allowedTypes.contains(type)) {
+ // Skip the entire first line (the marker line) by advancing to the end.
+ // This is different from regular blockquotes which calculate column position,
+ // because we want to consume the [!TYPE] marker completely and not render it.
+ int newColumn = line.length();
+
+ return BlockStart.of(new AlertBlockParser(type)).atColumn(newColumn);
+ }
+ }
+ }
+
+ return BlockStart.none();
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java
new file mode 100644
index 00000000..f965d179
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java
@@ -0,0 +1,98 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.html.HtmlNodeRendererContext;
+import org.commonmark.renderer.html.HtmlWriter;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class AlertHtmlNodeRenderer extends AlertNodeRenderer {
+
+ private final HtmlWriter htmlWriter;
+ private final HtmlNodeRendererContext context;
+ private final Map customTypeTitles;
+
+ public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map customTypeTitles) {
+ this.htmlWriter = context.getWriter();
+ this.context = context;
+ this.customTypeTitles = customTypeTitles;
+ }
+
+ @Override
+ protected void renderAlert(Alert alert) {
+ String type = alert.getType();
+ String cssClass = type.toLowerCase();
+
+ htmlWriter.line();
+ Map attributes = new LinkedHashMap<>();
+ attributes.put("class", "markdown-alert markdown-alert-" + cssClass);
+ attributes.put("data-alert-type", cssClass);
+
+ htmlWriter.tag("div", context.extendAttributes(alert, "div", attributes));
+ htmlWriter.line();
+
+ // Render alert title
+ htmlWriter.tag("p", Map.of("class", "markdown-alert-title"));
+ htmlWriter.text(getAlertTitle(type));
+ htmlWriter.tag("/p");
+ htmlWriter.line();
+
+ // Render children (the alert content)
+ renderChildren(alert);
+
+ htmlWriter.tag("/div");
+ htmlWriter.line();
+ }
+
+ /**
+ * Gets the display title for an alert type.
+ *
+ * For standard GFM types, uses the predefined titles.
+ * For custom types, uses the configured title or capitalizes the type name.
+ *
+ *
+ * @param type the alert type
+ * @return the display title (e.g., "Note", "Important", or custom title)
+ */
+ private String getAlertTitle(String type) {
+ // Check if it's a custom type
+ if (customTypeTitles.containsKey(type)) {
+ return customTypeTitles.get(type);
+ }
+
+ // Standard GFM types
+ switch (type) {
+ case "NOTE":
+ return "Note";
+ case "TIP":
+ return "Tip";
+ case "IMPORTANT":
+ return "Important";
+ case "WARNING":
+ return "Warning";
+ case "CAUTION":
+ return "Caution";
+ default:
+ // Fallback: capitalize the type
+ return capitalize(type);
+ }
+ }
+
+ private String capitalize(String s) {
+ if (s == null || s.isEmpty()) {
+ return s;
+ }
+ return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java
new file mode 100644
index 00000000..ff71e79e
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java
@@ -0,0 +1,38 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+
+public class AlertMarkdownNodeRenderer extends AlertNodeRenderer {
+
+ private final MarkdownWriter writer;
+ private final MarkdownNodeRendererContext context;
+
+ public AlertMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+ this.writer = context.getWriter();
+ this.context = context;
+ }
+
+ @Override
+ protected void renderAlert(Alert alert) {
+ writer.line();
+ writer.raw("> [!" + alert.getType() + "]");
+ writer.line();
+
+ // Render children - they will be indented by the blockquote
+ renderChildren(alert);
+
+ writer.line();
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java
new file mode 100644
index 00000000..dcb1a25e
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java
@@ -0,0 +1,22 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.renderer.NodeRenderer;
+
+import java.util.Set;
+
+public abstract class AlertNodeRenderer implements NodeRenderer {
+
+ @Override
+ public Set> getNodeTypes() {
+ return Set.of(Alert.class);
+ }
+
+ @Override
+ public void render(org.commonmark.node.Node node) {
+ Alert alert = (Alert) node;
+ renderAlert(alert);
+ }
+
+ protected abstract void renderAlert(Alert alert);
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertTextContentNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertTextContentNodeRenderer.java
new file mode 100644
index 00000000..9c6c66c9
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertTextContentNodeRenderer.java
@@ -0,0 +1,28 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.text.TextContentNodeRendererContext;
+
+public class AlertTextContentNodeRenderer extends AlertNodeRenderer {
+
+ private final TextContentNodeRendererContext context;
+
+ public AlertTextContentNodeRenderer(TextContentNodeRendererContext context) {
+ this.context = context;
+ }
+
+ @Override
+ protected void renderAlert(Alert alert) {
+ renderChildren(alert);
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/javadoc/overview.html b/commonmark-ext-gfm-alerts/src/main/javadoc/overview.html
new file mode 100644
index 00000000..145232a8
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/javadoc/overview.html
@@ -0,0 +1,6 @@
+
+
+Extension for GitHub Flavored Markdown (GFM) alerts using blockquote syntax
+See {@link org.commonmark.ext.gfm.alerts.AlertsExtension}
+
+
diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsComplexContentTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsComplexContentTest.java
new file mode 100644
index 00000000..d9293ccb
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsComplexContentTest.java
@@ -0,0 +1,76 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.Extension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.RenderingTestCase;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+/**
+ * Tests for alerts containing complex content like code blocks, lists, etc.
+ */
+public class AlertsComplexContentTest extends RenderingTestCase {
+
+ private static final Set EXTENSIONS = Set.of(AlertsExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void alertWithCodeBlock() {
+ assertRendering("> [!TIP]\n> Here's some code:\n>\n> function() { }\n>\n> End of tip",
+ "\n" +
+ "
Tip
\n" +
+ "
Here's some code:
\n" +
+ "
function() { }\n
\n" +
+ "
End of tip
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithList() {
+ assertRendering("> [!IMPORTANT]\n> Items to remember:\n> - First item\n> - Second item",
+ "\n" +
+ "
Important
\n" +
+ "
Items to remember:
\n" +
+ "
\n" +
+ "- First item
\n" +
+ "- Second item
\n" +
+ "
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithLinks() {
+ assertRendering("> [!NOTE]\n> Check out [this link](https://example.com) for more info",
+ "\n" +
+ "
Note
\n" +
+ "
Check out this link for more info
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithSpecialCharacters() {
+ assertRendering("> [!WARNING]\n> Special chars: & < > \" ' / \\ ` * _",
+ "\n" +
+ "
Warning
\n" +
+ "
Special chars: & < > " ' / \\ ` * _
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithHeading() {
+ assertRendering("> [!IMPORTANT]\n> ## Heading\n> Content below heading",
+ "\n" +
+ "
Important
\n" +
+ "
Heading
\n" +
+ "
Content below heading
\n" +
+ "
\n");
+ }
+
+ @Override
+ protected String render(String source) {
+ return HTML_RENDERER.render(PARSER.parse(source));
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsCustomTypesTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsCustomTypesTest.java
new file mode 100644
index 00000000..3bdc7e31
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsCustomTypesTest.java
@@ -0,0 +1,143 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.RenderingTestCase;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for custom alert types functionality.
+ */
+public class AlertsCustomTypesTest extends RenderingTestCase {
+
+ @Test
+ public void customTypeWithConfiguration() {
+ Extension extension = AlertsExtension.builder()
+ .addCustomType("INFO", "Information")
+ .build();
+
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+ HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();
+
+ String html = renderer.render(parser.parse("> [!INFO]\n> Custom alert"));
+
+ assertThat(html).contains("markdown-alert-info");
+ assertThat(html).contains("Information");
+ }
+
+ @Test
+ public void multipleCustomTypes() {
+ Extension extension = AlertsExtension.builder()
+ .addCustomType("INFO", "Information")
+ .addCustomType("SUCCESS", "Success")
+ .addCustomType("DANGER", "Danger")
+ .build();
+
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+ HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();
+
+ String markdown = "> [!INFO]\n> Info\n\n> [!SUCCESS]\n> Success\n\n> [!DANGER]\n> Danger";
+ String html = renderer.render(parser.parse(markdown));
+
+ assertThat(html).contains("markdown-alert-info");
+ assertThat(html).contains("Information");
+ assertThat(html).contains("markdown-alert-success");
+ assertThat(html).contains("Success");
+ assertThat(html).contains("markdown-alert-danger");
+ assertThat(html).contains("Danger");
+ }
+
+ @Test
+ public void customTypeWithoutConfigurationIsBlockquote() {
+ Extension extension = AlertsExtension.create(); // No custom types
+
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+ HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();
+
+ String html = renderer.render(parser.parse("> [!INFO]\n> Should be blockquote"));
+
+ assertThat(html).contains("");
+ assertThat(html).doesNotContain("markdown-alert");
+ }
+
+ @Test
+ public void standardTypesStillWork() {
+ Extension extension = AlertsExtension.builder()
+ .addCustomType("INFO", "Information")
+ .build();
+
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+ HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();
+
+ String html = renderer.render(parser.parse("> [!NOTE]\n> Standard type"));
+
+ assertThat(html).contains("markdown-alert-note");
+ assertThat(html).contains("Note");
+ }
+
+ @Test
+ public void customTypeMustBeUppercase() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ AlertsExtension.builder()
+ .addCustomType("info", "Information")
+ .build();
+ });
+ }
+
+ @Test
+ public void customTypeCannotOverrideStandardType() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ AlertsExtension.builder()
+ .addCustomType("NOTE", "Custom Note")
+ .build();
+ });
+ }
+
+ @Test
+ public void customTypeMustNotBeEmpty() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ AlertsExtension.builder()
+ .addCustomType("", "Title")
+ .build();
+ });
+ }
+
+ @Test
+ public void customTypeTitleMustNotBeEmpty() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ AlertsExtension.builder()
+ .addCustomType("INFO", "")
+ .build();
+ });
+ }
+
+ @Test
+ public void customTypeIsNotStandardType() {
+ Extension extension = AlertsExtension.builder()
+ .addCustomType("INFO", "Information")
+ .build();
+
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+
+ Node document = parser.parse("> [!INFO]\n> Custom alert");
+ Alert alert = (Alert) document.getFirstChild();
+
+ assertThat(alert.isStandardType()).isFalse();
+ assertThat(alert.getType()).isEqualTo("INFO");
+ }
+
+ @Override
+ protected String render(String source) {
+ Extension extension = AlertsExtension.create();
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+ HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build();
+ return renderer.render(parser.parse(source));
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsEdgeCasesTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsEdgeCasesTest.java
new file mode 100644
index 00000000..8fc9d03d
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsEdgeCasesTest.java
@@ -0,0 +1,228 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.Extension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.RenderingTestCase;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for edge cases and invalid inputs.
+ */
+public class AlertsEdgeCasesTest extends RenderingTestCase {
+
+ private static final Set EXTENSIONS = Set.of(AlertsExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void notAnAlertWithTextAfterMarker() {
+ assertRendering("> [!NOTE] Some text",
+ "\n" +
+ "[!NOTE] Some text
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void notAnAlertWithInvalidType() {
+ assertRendering("> [!INVALID]\n> Some text",
+ "\n" +
+ "[!INVALID]\nSome text
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void emptyAlertWithNoContent() {
+ assertRendering("> [!NOTE]",
+ "\n");
+ }
+
+ @Test
+ public void alertWithOnlyWhitespaceContent() {
+ assertRendering("> [!TIP]\n> \n> ",
+ "\n");
+ }
+
+ @Test
+ public void lowercaseAlertTypeShouldNotMatch() {
+ assertRendering("> [!note]\n> This should be a blockquote",
+ "\n" +
+ "[!note]\nThis should be a blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void mixedCaseAlertTypeShouldNotMatch() {
+ assertRendering("> [!Note]\n> This should be a blockquote",
+ "\n" +
+ "[!Note]\nThis should be a blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void nestedAlertMarkersAreTreatedAsText() {
+ assertRendering("> [!NOTE]\n> This is a note\n> [!WARNING]\n> This is still part of the note",
+ "\n" +
+ "
Note
\n" +
+ "
This is a note\n[!WARNING]\nThis is still part of the note
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void emptyLineImmediatelyAfterMarkerEndsAlert() {
+ assertRendering("> [!NOTE]\n\nSome text",
+ "\n" +
+ "Some text
\n");
+ }
+
+ @Test
+ public void alertWithLeadingSpacesBeforeMarker() {
+ assertRendering(" > [!IMPORTANT]\n > Content",
+ "\n" +
+ "
Important
\n" +
+ "
Content
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithTrailingTabsAfterMarker() {
+ assertRendering("> [!WARNING]\t\t\n> Be careful",
+ "\n" +
+ "
Warning
\n" +
+ "
Be careful
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void adjacentAlerts() {
+ assertRendering("> [!NOTE]\n> First alert\n\n> [!WARNING]\n> Second alert",
+ "\n" +
+ "
Note
\n" +
+ "
First alert
\n" +
+ "
\n" +
+ "\n" +
+ "
Warning
\n" +
+ "
Second alert
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertInterruptedByNonBlockquoteLine() {
+ assertRendering("> [!CAUTION]\n> First line\nLazy continuation\n> Continues alert",
+ "\n" +
+ "
Caution
\n" +
+ "
First line\nLazy continuation\nContinues alert
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithLongContent() {
+ String longContent = "> [!CAUTION]\n> " + "This is a very long line of text. ".repeat(10).trim();
+ String result = render(longContent);
+ assertThat(result).startsWith(" [!TIP]\n> Last alert",
+ "
\n" +
+ "
Tip
\n" +
+ "
Last alert
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithEmptyLinesInMiddle() {
+ assertRendering("> [!NOTE]\n> First\n>\n>\n> After empty lines",
+ "
\n" +
+ "
Note
\n" +
+ "
First
\n" +
+ "
After empty lines
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithOnlyMarkerAndWhitespaceLine() {
+ assertRendering("> [!IMPORTANT]\n> ",
+ "
\n" +
+ "
Important
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void markerWithExtraSpacesInside() {
+ assertRendering("> [! NOTE]\n> Should be blockquote",
+ "
\n" +
+ "[! NOTE]\nShould be blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void markerWithMissingBrackets() {
+ assertRendering("> !NOTE\n> Should be blockquote",
+ "
\n" +
+ "!NOTE\nShould be blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void markerWithMissingExclamation() {
+ assertRendering("> [NOTE]\n> Should be blockquote",
+ "
\n" +
+ "[NOTE]\nShould be blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertTypeAfterRegularBlockquote() {
+ assertRendering("> Regular blockquote\n> [!NOTE]\n> More text",
+ "
\n" +
+ "Regular blockquote\n[!NOTE]\nMore text
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithLazyContinuation() {
+ assertRendering("> [!NOTE]\n> First line\nLazy continuation",
+ "
\n" +
+ "
Note
\n" +
+ "
First line\nLazy continuation
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertEndedByBlankLineThenContent() {
+ assertRendering("> [!WARNING]\n> Content\n\nNot part of alert",
+ "
\n" +
+ "
Warning
\n" +
+ "
Content
\n" +
+ "
\n" +
+ "
Not part of alert
\n");
+ }
+
+ @Test
+ public void alertEndedByIndentedCode() {
+ assertRendering("> [!TIP]\n> Content\n\n code",
+ "
\n" +
+ "
Tip
\n" +
+ "
Content
\n" +
+ "
\n" +
+ "
code\n
\n");
+ }
+
+ @Override
+ protected String render(String source) {
+ return HTML_RENDERER.render(PARSER.parse(source));
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java
new file mode 100644
index 00000000..db13541f
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java
@@ -0,0 +1,54 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for rendering alerts back to Markdown.
+ */
+public class AlertsMarkdownRendererTest {
+
+ private static final Set
EXTENSIONS = Set.of(AlertsExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final MarkdownRenderer MARKDOWN_RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void markdownRendererPreservesAlert() {
+ String markdown = "> [!WARNING]\n> Be careful";
+ Node document = PARSER.parse(markdown);
+ String rendered = MARKDOWN_RENDERER.render(document);
+ assertThat(rendered).contains("[!WARNING]");
+ }
+
+ @Test
+ public void markdownRendererPreservesAllTypes() {
+ String markdown = "> [!NOTE]\n> Note\n\n> [!TIP]\n> Tip\n\n> [!IMPORTANT]\n> Important";
+ Node document = PARSER.parse(markdown);
+ String rendered = MARKDOWN_RENDERER.render(document);
+ assertThat(rendered).contains("[!NOTE]");
+ assertThat(rendered).contains("[!TIP]");
+ assertThat(rendered).contains("[!IMPORTANT]");
+ }
+
+ @Test
+ public void markdownRendererPreservesCustomTypes() {
+ Extension extension = AlertsExtension.builder()
+ .addCustomType("INFO", "Information")
+ .build();
+
+ Parser parser = Parser.builder().extensions(Set.of(extension)).build();
+ MarkdownRenderer renderer = MarkdownRenderer.builder().extensions(Set.of(extension)).build();
+
+ String markdown = "> [!INFO]\n> Custom type";
+ Node document = parser.parse(markdown);
+ String rendered = renderer.render(document);
+ assertThat(rendered).contains("[!INFO]");
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsTest.java
new file mode 100644
index 00000000..8c3849f5
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsTest.java
@@ -0,0 +1,130 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.RenderingTestCase;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for basic alert functionality with standard GFM types.
+ */
+public class AlertsTest extends RenderingTestCase {
+
+ private static final Set EXTENSIONS = Set.of(AlertsExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void noteAlert() {
+ assertRendering("> [!NOTE]\n> This is a note",
+ "\n" +
+ "
Note
\n" +
+ "
This is a note
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void tipAlert() {
+ assertRendering("> [!TIP]\n> This is a tip",
+ "\n" +
+ "
Tip
\n" +
+ "
This is a tip
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void importantAlert() {
+ assertRendering("> [!IMPORTANT]\n> This is important",
+ "\n" +
+ "
Important
\n" +
+ "
This is important
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void warningAlert() {
+ assertRendering("> [!WARNING]\n> This is a warning",
+ "\n" +
+ "
Warning
\n" +
+ "
This is a warning
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void cautionAlert() {
+ assertRendering("> [!CAUTION]\n> This is a caution",
+ "\n" +
+ "
Caution
\n" +
+ "
This is a caution
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithMultipleParagraphs() {
+ assertRendering("> [!NOTE]\n> First paragraph\n>\n> Second paragraph",
+ "\n" +
+ "
Note
\n" +
+ "
First paragraph
\n" +
+ "
Second paragraph
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertWithInlineFormatting() {
+ assertRendering("> [!TIP]\n> This is **bold** and *italic*",
+ "\n" +
+ "
Tip
\n" +
+ "
This is bold and italic
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void regularBlockquoteStillWorks() {
+ assertRendering("> This is a regular blockquote",
+ "\n" +
+ "This is a regular blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertFollowedByBlockquote() {
+ assertRendering("> [!NOTE]\n> This is an alert\n\n> This is a blockquote",
+ "\n" +
+ "
Note
\n" +
+ "
This is an alert
\n" +
+ "
\n" +
+ "\n" +
+ "This is a blockquote
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void alertParsedAsAlertNode() {
+ Node document = PARSER.parse("> [!NOTE]\n> This is a note");
+ Node firstChild = document.getFirstChild();
+ assertThat(firstChild).isInstanceOf(Alert.class);
+ Alert alert = (Alert) firstChild;
+ assertThat(alert.getType()).isEqualTo("NOTE");
+ assertThat(alert.isStandardType()).isTrue();
+ }
+
+ @Test
+ public void alertWithTrailingSpacesAfterMarker() {
+ assertRendering("> [!NOTE] \n> This is a note",
+ "\n" +
+ "
Note
\n" +
+ "
This is a note
\n" +
+ "
\n");
+ }
+
+ @Override
+ protected String render(String source) {
+ return HTML_RENDERER.render(PARSER.parse(source));
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/examples/AlertsExample.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/examples/AlertsExample.java
new file mode 100644
index 00000000..dba1628c
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/examples/AlertsExample.java
@@ -0,0 +1,94 @@
+package org.commonmark.ext.gfm.alerts.examples;
+
+import org.commonmark.Extension;
+import org.commonmark.ext.gfm.alerts.AlertsExtension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+
+import java.util.List;
+
+/**
+ * Example demonstrating the use of the GFM Alerts extension.
+ */
+public class AlertsExample {
+
+ public static void main(String[] args) {
+ standardTypesExample();
+ System.out.println("\n" + "=".repeat(60) + "\n");
+ customTypesExample();
+ }
+
+ private static void standardTypesExample() {
+ System.out.println("STANDARD GFM ALERT TYPES");
+ System.out.println("=".repeat(60));
+
+ // Create the alerts extension with default settings
+ Extension extension = AlertsExtension.create();
+
+ Parser parser = Parser.builder()
+ .extensions(List.of(extension))
+ .build();
+
+ HtmlRenderer renderer = HtmlRenderer.builder()
+ .extensions(List.of(extension))
+ .build();
+
+ // Example markdown with all standard alert types
+ String markdown = "# GFM Alerts Demo\n\n" +
+ "> [!NOTE]\n" +
+ "> Highlights information that users should take into account.\n\n" +
+ "> [!TIP]\n" +
+ "> Helpful advice for doing things better.\n\n" +
+ "> [!IMPORTANT]\n" +
+ "> Key information users need to know.\n\n" +
+ "> [!WARNING]\n" +
+ "> Urgent info that needs immediate attention.\n\n" +
+ "> [!CAUTION]\n" +
+ "> Advises about risks or negative outcomes.\n";
+
+ String html = renderer.render(parser.parse(markdown));
+
+ System.out.println("Markdown Input:");
+ System.out.println(markdown);
+ System.out.println("\nHTML Output:");
+ System.out.println(html);
+ }
+
+ private static void customTypesExample() {
+ System.out.println("CUSTOM ALERT TYPES");
+ System.out.println("=".repeat(60));
+
+ // Create extension with custom types
+ Extension extension = AlertsExtension.builder()
+ .addCustomType("INFO", "Information")
+ .addCustomType("SUCCESS", "Success")
+ .addCustomType("DANGER", "Danger")
+ .build();
+
+ Parser parser = Parser.builder()
+ .extensions(List.of(extension))
+ .build();
+
+ HtmlRenderer renderer = HtmlRenderer.builder()
+ .extensions(List.of(extension))
+ .build();
+
+ // Example markdown with custom alert types
+ String markdown = "# Custom Alert Types\n\n" +
+ "> [!INFO]\n" +
+ "> This is a custom information alert.\n\n" +
+ "> [!SUCCESS]\n" +
+ "> Operation completed successfully!\n\n" +
+ "> [!DANGER]\n" +
+ "> This action is dangerous and irreversible.\n\n" +
+ "> [!NOTE]\n" +
+ "> Standard types still work alongside custom types.\n";
+
+ String html = renderer.render(parser.parse(markdown));
+
+ System.out.println("Markdown Input:");
+ System.out.println(markdown);
+ System.out.println("\nHTML Output:");
+ System.out.println(html);
+ }
+}
diff --git a/pom.xml b/pom.xml
index d55d1fd8..58c3ead2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
commonmark
commonmark-ext-autolink
commonmark-ext-footnotes
+ commonmark-ext-gfm-alerts
commonmark-ext-gfm-strikethrough
commonmark-ext-gfm-tables
commonmark-ext-heading-anchor