diff --git a/CHANGELOG.md b/CHANGELOG.md
index b973b700f..254826f18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,36 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
with the exception that 0.x versions can break between minor versions.
+## [Unreleased]
+### Added
+- Support rendering GFM task list items to Markdown
+- Support rendering YAML front matter to Markdown
+- Alerts
+ - Allow customizing HTML attributes for alert title `
` tag via `AttributeProvider`
+ - New configuration for `AlertsExtension` to allow authors to provide custom
+ titles per alert. See the
+ [custom titles section of the alerts README](./commonmark-ext-gfm-alerts/README.md#custom-alert-titles)
+ for more information.
+ - New configuration for `AlertsExtension` to allow alerts to be nested within
+ other blocks (including other alerts). See
+ [this section of the alerts README](./commonmark-ext-gfm-alerts/README.md#nesting-alerts)
+ for more information.
+
+## [0.28.0] - 2026-03-31
+### Added
+- New extension for alerts (aka callouts/admonitions)
+ - Syntax:
+ ```markdown
+ > [!NOTE]
+ > The text of the note.
+ ```
+ - As types you can use NOTE, TIP, IMPORTANT, WARNING, CAUTION; or configure the
+ extension to add additional ones.
+ - Use class `AlertsExtension` in artifact `commonmark-ext-gfm-alerts` (#420)
+- New option `maxOpenBlockParsers` for `Parser.Builder` to set an overall limit
+ for the depth of block parsing. If set, any nesting beyond the limit will be
+ parsed as paragraph text instead. The default remains unlimited.
+
## [0.27.1] - 2026-01-14
### Fixed
- Line(s) after a hard line break would sometimes also get an unwanted hard
@@ -83,9 +113,9 @@ with the exception that 0.x versions can break between minor versions.
### Added
- New extension for footnotes!
- Syntax:
- ```
+ ```markdown
Main text[^1]
-
+
[^1]: Additional text in a footnote
```
- Inline footnotes like `^[inline footnote]` are also supported when enabled
@@ -250,7 +280,7 @@ with the exception that 0.x versions can break between minor versions.
- Use class `ImageAttributesExtension` in artifact `commonmark-ext-image-attributes`
- Extension for task lists (GitHub-style), thanks @dohertyfjatl
- Syntax:
- ```
+ ```markdown
- [x] task #1
- [ ] task #2
```
@@ -518,6 +548,8 @@ API breaking changes (caused by changes in spec):
Initial release of commonmark-java, a port of commonmark.js with extensions
for autolinking URLs, GitHub flavored strikethrough and tables.
+[Unreleased]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.28.0...main
+[0.28.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.27.1...commonmark-parent-0.28.0
[0.27.1]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.27.0...commonmark-parent-0.27.1
[0.27.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.26.0...commonmark-parent-0.27.0
[0.26.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.25.1...commonmark-parent-0.26.0
diff --git a/README.md b/README.md
index a917167f6..1f00e0f01 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ Coordinates for core library (see all on [Maven Central]):
org.commonmarkcommonmark
- 0.27.1
+ 0.28.0
```
@@ -65,9 +65,9 @@ import org.commonmark.node.*;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
-Parser parser = Parser.builder().build();
-Node document = parser.parse("This is *Markdown*");
-HtmlRenderer renderer = HtmlRenderer.builder().build();
+var parser = Parser.builder().build();
+var document = parser.parse("This is *Markdown*");
+var renderer = HtmlRenderer.builder().build();
renderer.render(document); // "
This is Markdown
\n"
```
@@ -90,14 +90,16 @@ after this.
import org.commonmark.node.*;
import org.commonmark.renderer.markdown.MarkdownRenderer;
-MarkdownRenderer renderer = MarkdownRenderer.builder().build();
-Node document = new Document();
-Heading heading = new Heading();
+// Build document
+var heading = new Heading();
heading.setLevel(2);
-heading.appendChild(new Text("My title"));
+heading.appendChild(new Text("My heading"));
+var document = new Document();
document.appendChild(heading);
-renderer.render(document); // "## My title\n"
+// Render to Markdown
+var renderer = MarkdownRenderer.builder().build();
+renderer.render(document); // "## My heading\n"
```
For rendering to plain text with minimal markup, there's also `TextContentRenderer`.
@@ -291,7 +293,7 @@ First, add an additional dependency (see [Maven Central] for others):
org.commonmarkcommonmark-ext-gfm-tables
- 0.27.1
+ 0.28.0
```
@@ -333,12 +335,26 @@ Enables tables using pipes as in [GitHub Flavored Markdown][gfm-tables].
Use class `TablesExtension` in artifact `commonmark-ext-gfm-tables`.
+### Alerts
+
+Adds support for GitHub-style alerts (also known as callouts or admonitions) as described [here](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts), e.g.:
+
+```markdown
+> [!NOTE]
+> The text of the note.
+```
+
+As types you can use NOTE, TIP, IMPORTANT, WARNING, CAUTION; or configure the extension to add additional ones.
+
+Use class `AlertsExtension` in artifact `commonmark-ext-gfm-alerts`. See the
+[`AlertsExtension` README](./commonmark-ext-gfm-alerts/README.md) for more information.
+
### Footnotes
Enables footnotes like in [GitHub](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes)
or [Pandoc](https://pandoc.org/MANUAL.html#footnotes):
-```
+```markdown
Main text[^1]
[^1]: Additional text in a footnote
@@ -355,7 +371,7 @@ is based on the text of the heading.
`# Heading` will be rendered as:
-```
+```html
Heading
```
@@ -376,7 +392,7 @@ Use class `InsExtension` in artifact `commonmark-ext-ins`.
Adds support for metadata through a YAML front matter block. This extension only supports a subset of YAML syntax. Here's an example of what's supported:
-```
+```markdown
---
key: value
list:
@@ -399,11 +415,11 @@ Adds support for specifying attributes (specifically height and width) for image
The attribute elements are given as `key=value` pairs inside curly braces `{ }` after the image node to which they apply,
for example:
-```
+```markdown
{width=640 height=480}
```
will be rendered as:
-```
+```html
```
@@ -421,12 +437,12 @@ whitespace character or the letter `x` in lowercase or uppercase, then a right b
whitespace before any other content.
For example:
-```
+```markdown
- [ ] task #1
- [x] task #2
```
will be rendered as:
-```
+```html
task #1
task #2
diff --git a/commonmark-ext-autolink/pom.xml b/commonmark-ext-autolink/pom.xml
index a99fd0b8c..2cc4d53ca 100644
--- a/commonmark-ext-autolink/pom.xml
+++ b/commonmark-ext-autolink/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-autolink
diff --git a/commonmark-ext-footnotes/pom.xml b/commonmark-ext-footnotes/pom.xml
index 8bb88d74a..0d9e2f30c 100644
--- a/commonmark-ext-footnotes/pom.xml
+++ b/commonmark-ext-footnotes/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-footnotes
diff --git a/commonmark-ext-gfm-alerts/README.md b/commonmark-ext-gfm-alerts/README.md
new file mode 100644
index 000000000..026408a56
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/README.md
@@ -0,0 +1,112 @@
+# commonmark-ext-gfm-alerts
+
+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).
+
+Enables highlighting important information using blockquote syntax with five standard alert types: NOTE, TIP, IMPORTANT, WARNING, and CAUTION.
+
+## Usage
+
+### Markdown Syntax
+
+```markdown
+> [!NOTE]
+> Useful information
+
+> [!WARNING]
+> Critical information
+```
+
+### Standard GFM Types
+
+```java
+var extension = AlertsExtension.create();
+var parser = Parser.builder().extensions(List.of(extension)).build();
+var renderer = HtmlRenderer.builder().extensions(List.of(extension)).build();
+```
+
+### Custom Alert Types
+
+Add custom types beyond the five standard GFM types:
+
+```java
+var extension = AlertsExtension.builder()
+ .addCustomType("BUG", "Known Bug")
+ .build();
+```
+
+Custom types must be UPPERCASE. Standard type titles can also be overridden for localization.
+
+### Custom Alert Titles
+
+Allow authors to provide custom titles per alert by adding text after the alert
+marker on the same line:
+
+```java
+var extension = AlertsExtension.builder().allowCustomTitles(true).build();
+```
+
+```markdown
+> [!NOTE] Keep in mind
+> Useful information
+
+> [!WARNING] Be **very** careful
+> Critical information
+```
+
+### Nesting Alerts
+
+By default, alerts cannot be nested within other blocks. Alerts within other
+blocks are parsed as regular block quotes.
+
+```markdown
+
+> [!NOTE]
+> Useful information
+
+
+- > [!NOTE]
+ > Useful information
+```
+
+This behavior can be changed to allow nested alerts:
+
+```java
+var extension = AlertsExtension.builder().allowNestedAlerts(true).build();
+```
+
+### Styling
+
+Alerts render as `
` elements with CSS classes:
+
+```html
+
+
Note
+
Content
+
+```
+
+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 GitHub's [Octicons](https://primer.style/octicons/):
+
+
+
+## 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 000000000..02ecbf802
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+ org.commonmark
+ commonmark-parent
+ 0.28.1-SNAPSHOT
+
+
+ commonmark-ext-gfm-alerts
+ commonmark-java extension for alerts
+ commonmark-java extension for GFM alerts (admonition blocks) using [!TYPE] syntax (GitHub Flavored Markdown)
+
+
+
+ org.commonmark
+ commonmark
+
+
+
+ org.commonmark
+ commonmark-test-util
+ test
+
+
+
+
diff --git a/commonmark-ext-gfm-alerts/screenshots/alerts-with-icons.png b/commonmark-ext-gfm-alerts/screenshots/alerts-with-icons.png
new file mode 100644
index 000000000..47da9402b
Binary files /dev/null and b/commonmark-ext-gfm-alerts/screenshots/alerts-with-icons.png differ
diff --git a/commonmark-ext-gfm-alerts/screenshots/alerts.png b/commonmark-ext-gfm-alerts/screenshots/alerts.png
new file mode 100644
index 000000000..83d4009f0
Binary files /dev/null and b/commonmark-ext-gfm-alerts/screenshots/alerts.png differ
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 000000000..e8b5aecb7
--- /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 000000000..b95f697ed
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java
@@ -0,0 +1,21 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.node.CustomBlock;
+
+/**
+ * Alert block for highlighting important information using {@code [!TYPE]} syntax.
+ *
+ * @see AlertTitle
+ */
+public class Alert extends CustomBlock {
+
+ private final String type;
+
+ public Alert(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertTitle.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertTitle.java
new file mode 100644
index 000000000..c085af746
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertTitle.java
@@ -0,0 +1,24 @@
+package org.commonmark.ext.gfm.alerts;
+
+import org.commonmark.node.CustomNode;
+
+/**
+ * Inline content container for the optional custom title of an {@link Alert}.
+ *
+ *
+ * When present, an {@code AlertTitle} is always the first child of an {@link Alert}.
+ * Its own children are the parsed inline nodes of the title (i.e., the text after
+ * the {@code [!TYPE]} marker on the same line). For example, in
+ *
+ *
{@code
+ * > [!NOTE] Custom _title_
+ * > Body text
+ * }
+ * Create with {@link #create()} or {@link #builder()} and configure on builders
+ * ({@link Parser.Builder#extensions(Iterable)}, {@link HtmlRenderer.Builder#extensions(Iterable)}).
+ * Parsed alerts become {@link Alert} blocks. If custom alert titles are allowed
+ * via {@link Builder#allowCustomTitles(boolean)}, the inline formatting of those
+ * titles will be parsed into {@link AlertTitle} nodes.
+ *
+ * The {@link #create() default configuration} of this extension will match GFM
+ * exactly, with the following exceptions:
+ *
+ * - Alert markers take precedence over shortcut reference links.
+ * - Alerts with no content are allowed. Example:
+ *
+ *
+ * - Lazy continuation is not allowed between the marker and the body text. Example:
+ *
+ *
{@code
+ * > [!NOTE]
+ * Lazy body text will be parsed as a new paragraph
+ * }
+ */
+public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
+ MarkdownRenderer.MarkdownRendererExtension {
+
+ static final Set STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION");
+
+ private final Map customTypes;
+ private final boolean customTitlesAllowed;
+ private final boolean nestedAlertsAllowed;
+
+ private AlertsExtension(Builder builder) {
+ this.customTypes = new HashMap<>(builder.customTypes);
+ this.customTitlesAllowed = builder.customTitlesAllowed;
+ this.nestedAlertsAllowed = builder.nestedAlertsAllowed;
+ }
+
+ public static Extension create() {
+ return builder().build();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public void extend(Parser.Builder parserBuilder) {
+ var allowedTypes = new HashSet<>(STANDARD_TYPES);
+ allowedTypes.addAll(customTypes.keySet());
+ parserBuilder.customBlockParserFactory(
+ new AlertBlockParser.Factory(allowedTypes, customTitlesAllowed, nestedAlertsAllowed));
+ }
+
+ @Override
+ public void extend(HtmlRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(context -> new AlertHtmlNodeRenderer(context, customTypes));
+ }
+
+ @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 HashMap<>();
+ private boolean customTitlesAllowed = false;
+ private boolean nestedAlertsAllowed = false;
+
+ /**
+ * Adds a custom alert type with a display title.
+ *
+ * This can also be used to override the display 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
+ * @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(Locale.ROOT))) {
+ throw new IllegalArgumentException("Type must be uppercase: " + type);
+ }
+ customTypes.put(type, title);
+ return this;
+ }
+
+ /**
+ * Allows or disallows custom titles on alerts. Inline formatting is supported
+ * within these titles.
+ * @param allow Whether to allow or disallow custom titles on alerts.
+ * @return {@code this}
+ * @see AlertTitle
+ */
+ public Builder allowCustomTitles(boolean allow) {
+ customTitlesAllowed = allow;
+ return this;
+ }
+
+ /**
+ * Allows or disallows parsing alerts within non-root blocks ({@code Document}).
+ *
+ * When disallowed, if an alert appears within another block, it will be parsed as
+ * a regular {@code BlockQuote}.
+ *
+ * Note that even when this is allowed, {@link Parser.Builder#maxOpenBlockParsers(int)}
+ * will be respected.
+ * @param allow Whether to allow or disallow parsing alerts within non-root blocks.
+ * @return {@code this}
+ */
+ public Builder allowNestedAlerts(boolean allow) {
+ nestedAlertsAllowed = allow;
+ 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 000000000..e7a9f402e
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertBlockParser.java
@@ -0,0 +1,226 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.ext.gfm.alerts.AlertTitle;
+import org.commonmark.node.Block;
+import org.commonmark.node.BlockQuote;
+import org.commonmark.node.Document;
+import org.commonmark.parser.InlineParser;
+import org.commonmark.parser.SourceLine;
+import org.commonmark.parser.SourceLines;
+import org.commonmark.parser.block.AbstractBlockParser;
+import org.commonmark.parser.block.AbstractBlockParserFactory;
+import org.commonmark.parser.block.BlockContinue;
+import org.commonmark.parser.block.BlockStart;
+import org.commonmark.parser.block.MatchedBlockParser;
+import org.commonmark.parser.block.ParserState;
+import org.commonmark.text.Characters;
+
+public class AlertBlockParser extends AbstractBlockParser {
+
+ private static final Pattern ALERT_PATTERN_NO_CUSTOM_TITLE = Pattern.compile("^\\[!([a-zA-Z]+)]\\s*$");
+ private static final Pattern ALERT_PATTERN_CUSTOM_TITLE = Pattern.compile("^\\[!([a-zA-Z]+)](.*)$");
+
+ private final Alert block;
+ private final SourceLine titleLine;
+
+ private AlertBlockParser(String type, SourceLine titleLine) {
+ this.block = new Alert(type);
+ this.titleLine = titleLine;
+ }
+
+ @Override
+ public Block getBlock() {
+ return block;
+ }
+
+ @Override
+ public boolean isContainer() {
+ return true;
+ }
+
+ @Override
+ public boolean canContain(Block childBlock) {
+ return true;
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState state) {
+ /*
+ * Same continuation rule as a block quote: line must start with '>'
+ * (with up to 3 leading spaces, optional space after '>')
+ */
+ var line = state.getLine().getContent();
+ var nextNonSpace = state.getNextNonSpaceIndex();
+ if (state.getIndent() >= 4 // Parsing.CODE_BLOCK_INDENT
+ || nextNonSpace >= line.length()
+ || line.charAt(nextNonSpace) != '>') {
+ return BlockContinue.none();
+ }
+
+ var newColumn = state.getColumn() + state.getIndent() + 1;
+ if (Characters.isSpaceOrTab(line, nextNonSpace + 1)) {
+ newColumn++;
+ }
+
+ return BlockContinue.atColumn(newColumn);
+ }
+
+ @Override
+ public void parseInlines(InlineParser inlineParser) {
+ if (titleLine == null || titleLine.getContent().length() == 0) {
+ return;
+ }
+
+ /*
+ * Inline-parse the title in its own scope so delimiters are isolated
+ * from the body text. For example:
+ *
+ * > [!NOTE] 2*2 = 4
+ * > But 3*3 = 9
+ */
+ var titleNode = new AlertTitle();
+ inlineParser.parse(SourceLines.of(titleLine), titleNode);
+
+ // Set source spans on the title node from the source line
+ var sourceSpan = titleLine.getSourceSpan();
+ if (sourceSpan != null) {
+ titleNode.setSourceSpans(List.of(sourceSpan));
+ }
+
+ // Body blocks were attached as children during block parsing. Prepend the title.
+ block.prependChild(titleNode);
+ }
+
+ public static class Factory extends AbstractBlockParserFactory {
+
+ private final Set allowedTypes;
+ private final boolean customTitlesAllowed;
+ private final boolean nestedAlertsAllowed;
+
+ public Factory(Set allowedTypes, boolean customTitlesAllowed, boolean nestedAlertsAllowed) {
+ this.allowedTypes = allowedTypes;
+ this.customTitlesAllowed = customTitlesAllowed;
+ this.nestedAlertsAllowed = nestedAlertsAllowed;
+ }
+
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
+ // Parsing.CODE_BLOCK_INDENT
+ if (state.getIndent() >= 4) {
+ return BlockStart.none();
+ }
+
+ if (!nestedAlertsAllowed && !isAtRoot(matchedBlockParser.getMatchedBlockParser().getBlock())) {
+ return BlockStart.none();
+ }
+
+ var line = state.getLine().getContent();
+ var nextNonSpace = state.getNextNonSpaceIndex();
+
+ // Case A: Fresh start. Line begins with '>'.
+ if (nextNonSpace < line.length() && line.charAt(nextNonSpace) == '>') {
+ return tryStartFresh(line, nextNonSpace, state);
+ }
+
+ /*
+ * Case B: Promotion. We're already inside a BlockQuote whose body so far is
+ * empty (only blank '>' lines), and the current line (already stripped of
+ * its '>' prefix by BlockQuoteParser.tryContinue) is the marker. Replace the
+ * active block quote with an alert.
+ */
+ var matched = matchedBlockParser.getMatchedBlockParser().getBlock();
+ if (matched instanceof BlockQuote && matched.getFirstChild() == null) {
+ // Null if not a marker. null.replaceActiveBlockParser() would NPE, so guard.
+ return tryStartFresh(line, nextNonSpace, state);
+ }
+
+ return BlockStart.none();
+ }
+
+ private static boolean isAtRoot(Block matched) {
+ if (matched instanceof Document) {
+ return true;
+ }
+
+ /*
+ * Case B: Promotion. The matched block is a top-level (Document-parented)
+ * BlockQuote that's still empty.
+ */
+ if (matched instanceof BlockQuote
+ && matched.getFirstChild() == null
+ && matched.getParent() instanceof Document) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private BlockStart tryStartFresh(CharSequence line, int nextNonSpace, ParserState state) {
+ int afterGt;
+ if (nextNonSpace < line.length() && line.charAt(nextNonSpace) == '>') {
+ afterGt = nextNonSpace + 1;
+ if (Characters.isSpaceOrTab(line, afterGt)) {
+ afterGt++;
+ }
+ } else {
+ /*
+ * Promotion path: the '>' has already been consumed by the active
+ * block quote's tryContinue, so state.getIndex() points past it.
+ */
+ afterGt = state.getIndex();
+ }
+
+ var pattern = customTitlesAllowed ? ALERT_PATTERN_CUSTOM_TITLE : ALERT_PATTERN_NO_CUSTOM_TITLE;
+ var matcher = pattern.matcher(line.subSequence(afterGt, line.length()));
+
+ if (!matcher.matches()) {
+ return BlockStart.none();
+ }
+
+ var type = matcher.group(1).toUpperCase(Locale.ROOT);
+ if (!allowedTypes.contains(type)) {
+ return BlockStart.none();
+ }
+
+ SourceLine titleLine = null;
+ if (customTitlesAllowed) {
+ var fullSourceLine = state.getLine();
+ var fullContent = fullSourceLine.getContent();
+
+ var groupStart = matcher.start(2);
+ var groupEnd = matcher.end(2);
+ var absStart = afterGt + groupStart;
+ var absEnd = afterGt + groupEnd;
+
+ // Trim leading spaces/tabs
+ while (absStart < absEnd && Characters.isSpaceOrTab(fullContent, absStart)) {
+ absStart++;
+ }
+ // Trim trailing spaces/tabs
+ while (absEnd > absStart && Characters.isSpaceOrTab(fullContent, absEnd - 1)) {
+ absEnd--;
+ }
+
+ titleLine = fullSourceLine.substring(absStart, absEnd);
+ }
+
+ // Consume the rest of the first line.
+ var start = BlockStart.of(new AlertBlockParser(type, titleLine)).atIndex(line.length());
+
+ // If we got here via the promotion path, replace the empty BlockQuote.
+ var matched = state.getActiveBlockParser().getBlock();
+ if (matched instanceof BlockQuote && matched.getFirstChild() == null
+ && (nextNonSpace >= line.length() || line.charAt(nextNonSpace) != '>')) {
+ start = start.replaceActiveBlockParser();
+ }
+
+ return start;
+ }
+ }
+}
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 000000000..af9fff704
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java
@@ -0,0 +1,88 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.ext.gfm.alerts.AlertTitle;
+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) {
+ var type = alert.getType();
+ var cssClass = type.toLowerCase();
+
+ htmlWriter.line();
+ var 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", context.extendAttributes(alert, "p", Map.of("class", "markdown-alert-title")));
+ var first = alert.getFirstChild();
+ if (first instanceof AlertTitle) {
+ renderChildren(first);
+ } else {
+ htmlWriter.text(getAlertTitle(type));
+ }
+ htmlWriter.tag("/p");
+ htmlWriter.line();
+
+ // Render children (the alert content)
+ renderChildren(alert);
+
+ htmlWriter.tag("/div");
+ htmlWriter.line();
+ }
+
+ 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);
+ }
+ }
+
+ private void renderChildren(Node parent) {
+ var node = parent.getFirstChild();
+ while (node != null) {
+ var next = node.getNext();
+
+ // AlertTitle is rendered separately from other nodes.
+ if (!(node instanceof AlertTitle)) {
+ 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 000000000..ab2504cef
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java
@@ -0,0 +1,51 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.ext.gfm.alerts.AlertTitle;
+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) {
+ // First line: > [!TYPE]
+ writer.writePrefix("> ");
+ writer.pushPrefix("> ");
+ writer.raw("[!" + alert.getType() + "]");
+
+ // Custom title if present, also on the first line.
+ var first = alert.getFirstChild();
+ if (first instanceof AlertTitle) {
+ writer.raw(" ");
+ renderChildren(first);
+ }
+
+ writer.line();
+ renderChildren(alert);
+ writer.popPrefix();
+ writer.block();
+ }
+
+ private void renderChildren(Node parent) {
+ var node = parent.getFirstChild();
+ while (node != null) {
+ var next = node.getNext();
+
+ // AlertTitle is rendered separately from other nodes.
+ if (!(node instanceof AlertTitle)) {
+ 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 000000000..45b34bb46
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java
@@ -0,0 +1,23 @@
+package org.commonmark.ext.gfm.alerts.internal;
+
+import org.commonmark.ext.gfm.alerts.Alert;
+import org.commonmark.node.Node;
+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(Node node) {
+ var alert = (Alert) node;
+ renderAlert(alert);
+ }
+
+ protected abstract void renderAlert(Alert alert);
+}
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 000000000..145232a87
--- /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}
\n");
+ }
+
+ @Test
+ public void customTitleNoSpace() {
+ assertRenderingCustomTitles("> [!NOTE]Custom title\n> Note with a custom title",
+ "
\n" +
+ "
Custom title
\n" +
+ "
Note with a custom title
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void customTitleExtraSpace() {
+ assertRenderingCustomTitles("> [!NOTE] Custom title \n> Note with a custom title",
+ "
\n" +
+ "
Custom title
\n" +
+ "
Note with a custom title
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void customTitleNoHardLineBreak() {
+ assertRenderingCustomTitles("> [!NOTE] Custom title\\\n> Note with a custom title",
+ "
\n" +
+ "
Custom title\\
\n" +
+ "
Note with a custom title
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void customTitleWithComment() {
+ assertRenderingCustomTitles("> [!NOTE] Custom title \n> Note with a custom title",
+ "
\n" +
+ "
Custom title
\n" +
+ "
Note with a custom title
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void customTitleWithInlineFormatting() {
+ assertRenderingCustomTitles("> [!NOTE] Custom _title with **formatting**_\n> Note with a custom title",
+ "
\n" +
+ "
Custom title with formatting
\n" +
+ "
Note with a custom title
\n" +
+ "
\n");
+ }
+
+ @Test
+ public void customTitleWithLinkAndCode() {
+ assertRenderingCustomTitles("> [!IMPORTANT] See [docs](https://example.com) or `run()`\n> Note with a custom title",
+ "
+````````````````````````````````
+
+## Alert content
+
+Marker alone in first paragraph, blank line, then content:
+
+```````````````````````````````` example alert
+> [!NOTE]
+>
+> Content
+.
+
+
Note
+
Content
+
+````````````````````````````````
+
+Multiple paragraphs:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First paragraph
+>
+> Second paragraph
+.
+
+
Note
+
First paragraph
+
Second paragraph
+
+````````````````````````````````
+
+Inline formatting:
+
+```````````````````````````````` example alert
+> [!TIP]
+> This is **bold** and *italic*
+.
+
+````````````````````````````````
+
+List inside alert:
+
+```````````````````````````````` example alert
+> [!IMPORTANT]
+> Items:
+> - First item
+> - Second item
+.
+
+
Important
+
Items:
+
+
First item
+
Second item
+
+
+````````````````````````````````
+
+Links inside alert:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> Check out [this link](https://example.com) for more info
+.
+
+````````````````````````````````
+
+Empty lines in middle of alert:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First
+>
+>
+> After empty lines
+.
+
+
Note
+
First
+
After empty lines
+
+````````````````````````````````
+
+## Not an alert
+
+Text after marker on the same line:
+
+```````````````````````````````` example alert
+> [!NOTE] Some text
+.
+
+
[!NOTE] Some text
+
+````````````````````````````````
+
+Unknown type:
+
+```````````````````````````````` example alert
+> [!INVALID]
+> Some text
+.
+
+
[!INVALID]
+Some text
+
+````````````````````````````````
+
+Unconfigured custom type is not an alert:
+
+```````````````````````````````` example alert
+> [!INFO]
+> Should be blockquote
+.
+
+
[!INFO]
+Should be blockquote
+
+````````````````````````````````
+
+Extra space inside marker:
+
+```````````````````````````````` example alert
+> [! NOTE]
+> Should be blockquote
+.
+
+
[! NOTE]
+Should be blockquote
+
+````````````````````````````````
+
+Missing brackets:
+
+```````````````````````````````` example alert
+> !NOTE
+> Should be blockquote
+.
+
+
!NOTE
+Should be blockquote
+
+````````````````````````````````
+
+Missing exclamation mark:
+
+```````````````````````````````` example alert
+> [NOTE]
+> Should be blockquote
+.
+
+
[NOTE]
+Should be blockquote
+
+````````````````````````````````
+
+Regular blockquote is not affected:
+
+```````````````````````````````` example alert
+> This is a regular blockquote
+.
+
+
This is a regular blockquote
+
+````````````````````````````````
+
+## Boundaries
+
+Trailing spaces after marker:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+.
+
+
Note
+
This is a note
+
+````````````````````````````````
+
+Trailing tabs after marker:
+
+```````````````````````````````` example alert
+> [!WARNING]→→
+> Be careful
+.
+
+
Warning
+
Be careful
+
+````````````````````````````````
+
+Leading spaces before blockquote marker:
+
+```````````````````````````````` example alert
+ > [!IMPORTANT]
+ > Content
+.
+
+
Important
+
Content
+
+````````````````````````````````
+
+Alert followed by blockquote:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is an alert
+
+> This is a blockquote
+.
+
+
Note
+
This is an alert
+
+
+
This is a blockquote
+
+````````````````````````````````
+
+Adjacent alerts:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First alert
+
+> [!WARNING]
+> Second alert
+.
+
+
Note
+
First alert
+
+
+
Warning
+
Second alert
+
+````````````````````````````````
+
+## Nesting and containers
+
+Nested alert inside alert renders as blockquote:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+>> [!WARNING]
+>> Nested content
+.
+
+
Note
+
This is a note
+
+
[!WARNING]
+Nested content
+
+
+````````````````````````````````
+
+Nested blockquote inside alert:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+>> Nested blockquote
+.
+
+
Note
+
This is a note
+
+
Nested blockquote
+
+
+````````````````````````````````
+
+Alert inside list item stays as blockquote:
+
+```````````````````````````````` example alert
+- > [!NOTE]
+ > Test
+.
+
+
+
+
[!NOTE]
+Test
+
+
+
+````````````````````````````````
+
+Alert marker in content is treated as text:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+> [!WARNING]
+> This is still part of the note
+.
+
+
Note
+
This is a note
+[!WARNING]
+This is still part of the note
+
+````````````````````````````````
+
+## Continuation and interruption
+
+Lazy continuation:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First line
+Lazy continuation
+> Continues alert
+.
+
+
Note
+
First line
+Lazy continuation
+Continues alert
+
+````````````````````````````````
+
+Alert type after regular blockquote content is not an alert:
+
+```````````````````````````````` example alert
+> Regular blockquote
+> [!NOTE]
+> More text
+.
+
+
Regular blockquote
+[!NOTE]
+More text
+
+````````````````````````````````
diff --git a/commonmark-ext-gfm-alerts/src/test/resources/generate-alerts-spec.java b/commonmark-ext-gfm-alerts/src/test/resources/generate-alerts-spec.java
new file mode 100644
index 000000000..06192f107
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/resources/generate-alerts-spec.java
@@ -0,0 +1,111 @@
+///usr/bin/env jbang "$0" "$@" ; exit $?
+
+// Generates alerts-spec.txt from alerts-spec-template.md by rendering each example
+// through the GitHub Markdown API and inserting the normalized HTML expectation.
+//
+// Prerequisites: gh CLI installed and authenticated (gh auth login)
+// Usage: cd commonmark-ext-gfm-alerts/src/test/resources && jbang generate-alerts-spec.java
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+class GenerateAlertsSpec {
+
+ private static final String FENCE = "````````````````````````````````";
+ private static final String EXAMPLE_OPEN = FENCE + " example alert";
+
+ public static void main(String[] args) throws Exception {
+ var templatePath = Path.of("alerts-spec-template.md");
+ if (!Files.exists(templatePath)) {
+ System.err.println("Run from the directory containing alerts-spec-template.md");
+ System.exit(1);
+ }
+
+ var lines = Files.readAllLines(templatePath);
+ var output = new ArrayList();
+ var header = "Expectations verified against GitHub Markdown API (gh api markdown -f mode=gfm).\n" +
+ "Our HTML omits GitHub's SVG icons and uses a `data-alert-type` attribute instead.";
+
+ int exampleCount = 0;
+ int i = 0;
+ while (i < lines.size()) {
+ var line = lines.get(i);
+
+ // Insert header after the first heading
+ if (i == 0 && line.startsWith("# ")) {
+ output.add(line);
+ output.add("");
+ output.add(header);
+ i++;
+ continue;
+ }
+
+ if (line.equals(EXAMPLE_OPEN)) {
+ // Collect source lines until closing fence
+ output.add(line);
+ i++;
+ var sourceLines = new ArrayList();
+ while (i < lines.size() && !lines.get(i).equals(FENCE)) {
+ sourceLines.add(lines.get(i));
+ output.add(lines.get(i));
+ i++;
+ }
+
+ // Render via GitHub API (→ represents tabs in the spec format)
+ var source = String.join("\n", sourceLines).replace("\u2192", "\t");
+ exampleCount++;
+ System.out.printf("%d: %s%n", exampleCount,
+ source.substring(0, Math.min(40, source.length())).replace("\n", "\\n"));
+
+ var ghHtml = normalizeHtml(renderViaGh(source));
+
+ // Insert separator and HTML expectation
+ output.add(".");
+ output.add(ghHtml);
+ output.add(FENCE);
+ i++; // skip closing fence from template
+ } else {
+ output.add(line);
+ i++;
+ }
+ }
+
+ var specPath = Path.of("alerts-spec.txt");
+ Files.writeString(specPath, String.join("\n", output) + "\n");
+ System.out.println("Done — " + exampleCount + " examples written to alerts-spec.txt");
+ }
+
+ static String renderViaGh(String markdown) throws Exception {
+ var process = new ProcessBuilder("gh", "api", "markdown", "-f", "mode=gfm", "-f", "text=" + markdown)
+ .redirectErrorStream(true)
+ .start();
+ var output = new String(process.getInputStream().readAllBytes());
+ if (process.waitFor() != 0) {
+ throw new RuntimeException("gh api failed: " + output);
+ }
+ return output;
+ }
+
+ // Normalize GitHub API HTML to match our renderer output.
+ static String normalizeHtml(String html) {
+ // Strip GitHub-specific elements and attributes
+ html = Pattern.compile("", Pattern.DOTALL).matcher(html).replaceAll("");
+ html = html.replaceAll(" (dir=\"auto\"|rel=\"nofollow\"|class=\"notranslate\")", "");
+ // Add data-alert-type and insert newlines to match our renderer's formatting
+ html = Pattern.compile("class=\"markdown-alert markdown-alert-(\\w+)\"")
+ .matcher(html)
+ .replaceAll("class=\"markdown-alert markdown-alert-$1\" data-alert-type=\"$1\"");
+ html = Pattern.compile("(data-alert-type=\"\\w+\">)(
", "
\n
");
+ return html.replace("\r\n", "\n").lines()
+ .map(String::stripTrailing)
+ .reduce((a, b) -> a + "\n" + b)
+ .orElse("")
+ .strip();
+ }
+}
\ No newline at end of file
diff --git a/commonmark-ext-gfm-strikethrough/pom.xml b/commonmark-ext-gfm-strikethrough/pom.xml
index f6cedc69a..9d8f55e5f 100644
--- a/commonmark-ext-gfm-strikethrough/pom.xml
+++ b/commonmark-ext-gfm-strikethrough/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-gfm-strikethrough
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java
index 364205aed..0571aed84 100644
--- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java
@@ -7,14 +7,10 @@
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughTextContentNodeRenderer;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
-import org.commonmark.renderer.html.HtmlNodeRendererContext;
-import org.commonmark.renderer.html.HtmlNodeRendererFactory;
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.TextContentNodeRendererContext;
-import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.renderer.text.TextContentRenderer;
import java.util.Set;
@@ -77,22 +73,12 @@ public void extend(Parser.Builder parserBuilder) {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() {
- @Override
- public NodeRenderer create(HtmlNodeRendererContext context) {
- return new StrikethroughHtmlNodeRenderer(context);
- }
- });
+ rendererBuilder.nodeRendererFactory(StrikethroughHtmlNodeRenderer::new);
}
@Override
public void extend(TextContentRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new TextContentNodeRendererFactory() {
- @Override
- public NodeRenderer create(TextContentNodeRendererContext context) {
- return new StrikethroughTextContentNodeRenderer(context);
- }
- });
+ rendererBuilder.nodeRendererFactory(StrikethroughTextContentNodeRenderer::new);
}
@Override
diff --git a/commonmark-ext-gfm-tables/pom.xml b/commonmark-ext-gfm-tables/pom.xml
index 4e94f623c..5bd323168 100644
--- a/commonmark-ext-gfm-tables/pom.xml
+++ b/commonmark-ext-gfm-tables/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-gfm-tables
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
index f754b8276..4740e9aad 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
@@ -7,14 +7,10 @@
import org.commonmark.ext.gfm.tables.internal.TableTextContentNodeRenderer;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
-import org.commonmark.renderer.html.HtmlNodeRendererContext;
-import org.commonmark.renderer.html.HtmlNodeRendererFactory;
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.TextContentNodeRendererContext;
-import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.renderer.text.TextContentRenderer;
import java.util.Set;
@@ -49,22 +45,12 @@ public void extend(Parser.Builder parserBuilder) {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() {
- @Override
- public NodeRenderer create(HtmlNodeRendererContext context) {
- return new TableHtmlNodeRenderer(context);
- }
- });
+ rendererBuilder.nodeRendererFactory(TableHtmlNodeRenderer::new);
}
@Override
public void extend(TextContentRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new TextContentNodeRendererFactory() {
- @Override
- public NodeRenderer create(TextContentNodeRendererContext context) {
- return new TableTextContentNodeRenderer(context);
- }
- });
+ rendererBuilder.nodeRendererFactory(TableTextContentNodeRenderer::new);
}
@Override
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java
index 57af128d8..49f17194d 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java
@@ -274,7 +274,7 @@ public static class Factory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
List paragraphLines = matchedBlockParser.getParagraphLines().getLines();
- if (paragraphLines.size() >= 1 && Characters.find('|', paragraphLines.get(paragraphLines.size() - 1).getContent(), 0) != -1) {
+ if (!paragraphLines.isEmpty() && Characters.find('|', paragraphLines.get(paragraphLines.size() - 1).getContent(), 0) != -1) {
SourceLine line = state.getLine();
SourceLine separatorLine = line.substring(state.getIndex(), line.getContent().length());
List columns = parseSeparator(separatorLine.getContent());
diff --git a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java
index 3f4b37d54..98c3ad4d7 100644
--- a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java
+++ b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java
@@ -4,15 +4,12 @@
import org.commonmark.node.*;
import org.commonmark.parser.IncludeSourceSpans;
import org.commonmark.parser.Parser;
-import org.commonmark.renderer.html.AttributeProvider;
-import org.commonmark.renderer.html.AttributeProviderContext;
import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.testutil.RenderingTestCase;
import org.junit.jupiter.api.Test;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -718,25 +715,17 @@ public void interruptsParagraph() {
@Test
public void attributeProviderIsApplied() {
- AttributeProviderFactory factory = new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return new AttributeProvider() {
- @Override
- public void setAttributes(Node node, String tagName, Map attributes) {
- if (node instanceof TableBlock) {
- attributes.put("test", "block");
- } else if (node instanceof TableHead) {
- attributes.put("test", "head");
- } else if (node instanceof TableBody) {
- attributes.put("test", "body");
- } else if (node instanceof TableRow) {
- attributes.put("test", "row");
- } else if (node instanceof TableCell) {
- attributes.put("test", "cell");
- }
- }
- };
+ AttributeProviderFactory factory = context -> (node, tagName, attributes) -> {
+ if (node instanceof TableBlock) {
+ attributes.put("test", "block");
+ } else if (node instanceof TableHead) {
+ attributes.put("test", "head");
+ } else if (node instanceof TableBody) {
+ attributes.put("test", "body");
+ } else if (node instanceof TableRow) {
+ attributes.put("test", "row");
+ } else if (node instanceof TableCell) {
+ attributes.put("test", "cell");
}
};
HtmlRenderer renderer = HtmlRenderer.builder()
@@ -762,17 +751,9 @@ public void setAttributes(Node node, String tagName, Map attribu
@Test
public void columnWidthIsRecorded() {
- AttributeProviderFactory factory = new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return new AttributeProvider() {
- @Override
- public void setAttributes(Node node, String tagName, Map attributes) {
- if (node instanceof TableCell && "th".equals(tagName)) {
- attributes.put("width", ((TableCell) node).getWidth() + "em");
- }
- }
- };
+ AttributeProviderFactory factory = context -> (node, tagName, attributes) -> {
+ if (node instanceof TableCell && "th".equals(tagName)) {
+ attributes.put("width", ((TableCell) node).getWidth() + "em");
}
};
HtmlRenderer renderer = HtmlRenderer.builder()
diff --git a/commonmark-ext-heading-anchor/pom.xml b/commonmark-ext-heading-anchor/pom.xml
index 707948a10..26d2d19b1 100644
--- a/commonmark-ext-heading-anchor/pom.xml
+++ b/commonmark-ext-heading-anchor/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-heading-anchor
diff --git a/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/HeadingAnchorExtension.java b/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/HeadingAnchorExtension.java
index cee414da2..5921f3c5b 100644
--- a/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/HeadingAnchorExtension.java
+++ b/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/HeadingAnchorExtension.java
@@ -2,9 +2,6 @@
import org.commonmark.Extension;
import org.commonmark.ext.heading.anchor.internal.HeadingIdAttributeProvider;
-import org.commonmark.renderer.html.AttributeProvider;
-import org.commonmark.renderer.html.AttributeProviderContext;
-import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;
/**
@@ -55,12 +52,7 @@ public static Builder builder() {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
- rendererBuilder.attributeProviderFactory(new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return HeadingIdAttributeProvider.create(defaultId, idPrefix, idSuffix);
- }
- });
+ rendererBuilder.attributeProviderFactory(context -> HeadingIdAttributeProvider.create(defaultId, idPrefix, idSuffix));
}
public static class Builder {
diff --git a/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/IdGenerator.java b/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/IdGenerator.java
index 6eb85b6c1..2c37433e2 100644
--- a/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/IdGenerator.java
+++ b/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/IdGenerator.java
@@ -66,7 +66,7 @@ public static Builder builder() {
public String generateId(String text) {
String normalizedIdentity = text != null ? normalizeText(text) : defaultIdentifier;
- if (normalizedIdentity.length() == 0) {
+ if (normalizedIdentity.isEmpty()) {
normalizedIdentity = defaultIdentifier;
}
diff --git a/commonmark-ext-image-attributes/pom.xml b/commonmark-ext-image-attributes/pom.xml
index 456ac04a2..e646bc3fd 100644
--- a/commonmark-ext-image-attributes/pom.xml
+++ b/commonmark-ext-image-attributes/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-image-attributes
diff --git a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/ImageAttributesExtension.java b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/ImageAttributesExtension.java
index 28c6abab2..9a4de2628 100644
--- a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/ImageAttributesExtension.java
+++ b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/ImageAttributesExtension.java
@@ -4,9 +4,6 @@
import org.commonmark.ext.image.attributes.internal.ImageAttributesAttributeProvider;
import org.commonmark.ext.image.attributes.internal.ImageAttributesDelimiterProcessor;
import org.commonmark.parser.Parser;
-import org.commonmark.renderer.html.AttributeProvider;
-import org.commonmark.renderer.html.AttributeProviderContext;
-import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;
/**
@@ -35,11 +32,6 @@ public void extend(Parser.Builder parserBuilder) {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
- rendererBuilder.attributeProviderFactory(new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return ImageAttributesAttributeProvider.create();
- }
- });
+ rendererBuilder.attributeProviderFactory(context -> ImageAttributesAttributeProvider.create());
}
}
diff --git a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesAttributeProvider.java b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesAttributeProvider.java
index edd9c4692..bd89738de 100644
--- a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesAttributeProvider.java
+++ b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesAttributeProvider.java
@@ -26,9 +26,7 @@ public void setAttributes(Node node, String tagName, final Map a
public void visit(CustomNode node) {
if (node instanceof ImageAttributes) {
ImageAttributes imageAttributes = (ImageAttributes) node;
- for (Map.Entry entry : imageAttributes.getAttributes().entrySet()) {
- attributes.put(entry.getKey(), entry.getValue());
- }
+ attributes.putAll(imageAttributes.getAttributes());
// Now that we have used the image attributes we remove the node.
imageAttributes.unlink();
}
diff --git a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java
index a335ccadc..06a39ba23 100644
--- a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java
+++ b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java
@@ -75,7 +75,7 @@ public int process(DelimiterRun openingRun, DelimiterRun closingRun) {
node.unlink();
}
- if (attributesMap.size() > 0) {
+ if (!attributesMap.isEmpty()) {
ImageAttributes imageAttributes = new ImageAttributes(attributesMap);
// The new node is added as a child of the image node to which the attributes apply.
diff --git a/commonmark-ext-ins/pom.xml b/commonmark-ext-ins/pom.xml
index a06c27b29..48481c073 100644
--- a/commonmark-ext-ins/pom.xml
+++ b/commonmark-ext-ins/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-ins
diff --git a/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/InsExtension.java b/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/InsExtension.java
index e8a53e59a..3d2974916 100644
--- a/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/InsExtension.java
+++ b/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/InsExtension.java
@@ -7,14 +7,10 @@
import org.commonmark.ext.ins.internal.InsTextContentNodeRenderer;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
-import org.commonmark.renderer.html.HtmlNodeRendererContext;
-import org.commonmark.renderer.html.HtmlNodeRendererFactory;
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.TextContentNodeRendererContext;
-import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.renderer.text.TextContentRenderer;
import java.util.Set;
@@ -46,22 +42,12 @@ public void extend(Parser.Builder parserBuilder) {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() {
- @Override
- public NodeRenderer create(HtmlNodeRendererContext context) {
- return new InsHtmlNodeRenderer(context);
- }
- });
+ rendererBuilder.nodeRendererFactory(InsHtmlNodeRenderer::new);
}
@Override
public void extend(TextContentRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new TextContentNodeRendererFactory() {
- @Override
- public NodeRenderer create(TextContentNodeRendererContext context) {
- return new InsTextContentNodeRenderer(context);
- }
- });
+ rendererBuilder.nodeRendererFactory(InsTextContentNodeRenderer::new);
}
@Override
diff --git a/commonmark-ext-task-list-items/pom.xml b/commonmark-ext-task-list-items/pom.xml
index 0fc164672..4359f8707 100644
--- a/commonmark-ext-task-list-items/pom.xml
+++ b/commonmark-ext-task-list-items/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-task-list-items
diff --git a/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/TaskListItemsExtension.java b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/TaskListItemsExtension.java
index 9bf0a2155..1c89256d3 100644
--- a/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/TaskListItemsExtension.java
+++ b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/TaskListItemsExtension.java
@@ -1,13 +1,16 @@
package org.commonmark.ext.task.list.items;
+import java.util.Set;
import org.commonmark.Extension;
import org.commonmark.ext.task.list.items.internal.TaskListItemHtmlNodeRenderer;
+import org.commonmark.ext.task.list.items.internal.TaskListItemMarkdownNodeRenderer;
import org.commonmark.ext.task.list.items.internal.TaskListItemPostProcessor;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
-import org.commonmark.renderer.html.HtmlNodeRendererContext;
-import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
/**
* Extension for adding task list items.
@@ -19,7 +22,8 @@
*
* @since 0.15.0
*/
-public class TaskListItemsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
+public class TaskListItemsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
+ MarkdownRenderer.MarkdownRendererExtension {
private TaskListItemsExtension() {
}
@@ -35,10 +39,20 @@ public void extend(Parser.Builder parserBuilder) {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
- rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() {
+ rendererBuilder.nodeRendererFactory(TaskListItemHtmlNodeRenderer::new);
+ }
+
+ @Override
+ public void extend(MarkdownRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
+ @Override
+ public NodeRenderer create(MarkdownNodeRendererContext context) {
+ return new TaskListItemMarkdownNodeRenderer(context);
+ }
+
@Override
- public NodeRenderer create(HtmlNodeRendererContext context) {
- return new TaskListItemHtmlNodeRenderer(context);
+ public Set getSpecialCharacters() {
+ return Set.of();
}
});
}
diff --git a/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemHtmlNodeRenderer.java b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemHtmlNodeRenderer.java
index 331b301e9..a27b125c8 100644
--- a/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemHtmlNodeRenderer.java
+++ b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemHtmlNodeRenderer.java
@@ -2,15 +2,13 @@
import org.commonmark.ext.task.list.items.TaskListItemMarker;
import org.commonmark.node.Node;
-import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlWriter;
import java.util.LinkedHashMap;
import java.util.Map;
-import java.util.Set;
-public class TaskListItemHtmlNodeRenderer implements NodeRenderer {
+public class TaskListItemHtmlNodeRenderer extends TaskListItemNodeRenderer {
private final HtmlNodeRendererContext context;
private final HtmlWriter html;
@@ -20,11 +18,6 @@ public TaskListItemHtmlNodeRenderer(HtmlNodeRendererContext context) {
this.html = context.getWriter();
}
- @Override
- public Set> getNodeTypes() {
- return Set.of(TaskListItemMarker.class);
- }
-
@Override
public void render(Node node) {
if (node instanceof TaskListItemMarker) {
diff --git a/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemMarkdownNodeRenderer.java b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemMarkdownNodeRenderer.java
new file mode 100644
index 000000000..d2b363952
--- /dev/null
+++ b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemMarkdownNodeRenderer.java
@@ -0,0 +1,36 @@
+package org.commonmark.ext.task.list.items.internal;
+
+import org.commonmark.ext.task.list.items.TaskListItemMarker;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+
+public class TaskListItemMarkdownNodeRenderer extends TaskListItemNodeRenderer {
+
+ private final MarkdownNodeRendererContext context;
+ private final MarkdownWriter writer;
+
+ public TaskListItemMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+ this.context = context;
+ this.writer = context.getWriter();
+ }
+
+ @Override
+ public void render(Node node) {
+ if (node instanceof TaskListItemMarker) {
+ var taskListItemNode = (TaskListItemMarker) node;
+ var checkboxFill = taskListItemNode.isChecked() ? "x" : " ";
+ writer.raw("[" + checkboxFill + "] ");
+ renderChildren(node);
+ }
+ }
+
+ 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-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemNodeRenderer.java b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemNodeRenderer.java
new file mode 100644
index 000000000..24efd4e7d
--- /dev/null
+++ b/commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemNodeRenderer.java
@@ -0,0 +1,13 @@
+package org.commonmark.ext.task.list.items.internal;
+
+import java.util.Set;
+import org.commonmark.ext.task.list.items.TaskListItemMarker;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.NodeRenderer;
+
+public abstract class TaskListItemNodeRenderer implements NodeRenderer {
+ @Override
+ public Set> getNodeTypes() {
+ return Set.of(TaskListItemMarker.class);
+ }
+}
diff --git a/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemMarkdownRendererTest.java b/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemMarkdownRendererTest.java
new file mode 100644
index 000000000..cf73f434c
--- /dev/null
+++ b/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemMarkdownRendererTest.java
@@ -0,0 +1,84 @@
+package org.commonmark.ext.task.list.items;
+
+import java.util.Set;
+import org.commonmark.Extension;
+import org.commonmark.node.BulletList;
+import org.commonmark.node.Document;
+import org.commonmark.node.ListItem;
+import org.commonmark.node.Node;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.Text;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TaskListItemMarkdownRendererTest {
+
+ private static final Set EXTENSIONS = Set.of(TaskListItemsExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void testCheckedRoundTrip() {
+ assertRoundTrip("- [x] I am checked\n");
+ }
+
+ @Test
+ public void testUncheckedRoundTrip() {
+ assertRoundTrip("- [ ] I am unchecked\n");
+ }
+
+ @Test
+ public void testMixedRoundTrip() {
+ assertRoundTrip("- [x] I am checked\n- [ ] I am unchecked\n");
+ }
+
+ @Test
+ public void testNestedRoundTrip() {
+ assertRoundTrip("- [ ] I am unchecked\n - [x] I am a checked child\n");
+ }
+
+ @Test
+ public void testFormattingRoundTrip() {
+ assertRoundTrip("- [x] I am **boldly** checked\n- [ ] I am *italicly* unchecked\n");
+ }
+
+ @Test
+ public void testNonTaskListItemRoundTrip() {
+ assertRoundTrip("- [x] I am checked\n- [ ] I am unchecked\n- I am not a task item\n");
+ }
+
+ @Test
+ public void testOrderedListRoundTrip() {
+ assertRoundTrip("1. [x] I am checked\n2. [ ] I am unchecked\n");
+ }
+
+ @Test
+ public void testProgrammaticallyBuilt() {
+ var doc = new Document();
+ var list = new BulletList();
+ var item = new ListItem();
+ var taskMarker = new TaskListItemMarker(false);
+ var para = new Paragraph();
+ var text = new Text("I am a task");
+ para.appendChild(text);
+ item.appendChild(taskMarker);
+ item.appendChild(para);
+ list.appendChild(item);
+ doc.appendChild(list);
+
+ assertRenderedEquals(doc, "- [ ] I am a task\n");
+ }
+
+ private void assertRoundTrip(String input) {
+ String rendered = RENDERER.render(PARSER.parse(input));
+ assertThat(rendered).isEqualTo(input);
+ }
+
+ private void assertRenderedEquals(Node inputNode, String expectedOutput) {
+ var renderedOutput = RENDERER.render(inputNode);
+ assertThat(renderedOutput).isEqualTo(expectedOutput);
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/pom.xml b/commonmark-ext-yaml-front-matter/pom.xml
index 860cfbac7..e6822f771 100644
--- a/commonmark-ext-yaml-front-matter/pom.xml
+++ b/commonmark-ext-yaml-front-matter/pom.xml
@@ -4,7 +4,7 @@
commonmark-parentorg.commonmark
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-yaml-front-matter
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java
index 7a2c9f9f5..99ca17708 100644
--- a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java
@@ -1,9 +1,15 @@
package org.commonmark.ext.front.matter;
+import java.util.Set;
import org.commonmark.Extension;
import org.commonmark.ext.front.matter.internal.YamlFrontMatterBlockParser;
+import org.commonmark.ext.front.matter.internal.YamlFrontMatterMarkdownNodeRenderer;
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;
/**
* Extension for YAML-like metadata.
@@ -16,7 +22,7 @@
* The parsed metadata is turned into {@link YamlFrontMatterNode}. You can access the metadata using {@link YamlFrontMatterVisitor}.
*
*/
-public class YamlFrontMatterExtension implements Parser.ParserExtension {
+public class YamlFrontMatterExtension implements Parser.ParserExtension, MarkdownRenderer.MarkdownRendererExtension {
private YamlFrontMatterExtension() {
}
@@ -29,4 +35,19 @@ public void extend(Parser.Builder parserBuilder) {
public static Extension create() {
return new YamlFrontMatterExtension();
}
+
+ @Override
+ public void extend(MarkdownRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
+ @Override
+ public NodeRenderer create(MarkdownNodeRendererContext context) {
+ return new YamlFrontMatterMarkdownNodeRenderer(context);
+ }
+
+ @Override
+ public Set getSpecialCharacters() {
+ return Set.of();
+ }
+ });
+ }
}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java
index 469cf4e2f..f7f03c001 100644
--- a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java
@@ -4,8 +4,6 @@
import org.commonmark.ext.front.matter.YamlFrontMatterNode;
import org.commonmark.node.Block;
import org.commonmark.node.Document;
-import org.commonmark.parser.InlineParser;
-import org.commonmark.parser.SourceLine;
import org.commonmark.parser.block.*;
import java.util.ArrayList;
@@ -37,10 +35,6 @@ public Block getBlock() {
return block;
}
- @Override
- public void addLine(SourceLine line) {
- }
-
@Override
public BlockContinue tryContinue(ParserState parserState) {
final CharSequence line = parserState.getLine().getContent();
@@ -91,10 +85,6 @@ public BlockContinue tryContinue(ParserState parserState) {
}
}
- @Override
- public void parseInlines(InlineParser inlineParser) {
- }
-
private static String parseString(String s) {
// Limited parsing of https://yaml.org/spec/1.2.2/#73-flow-scalar-styles
// We assume input is well-formed and otherwise treat it as a plain string. In a real
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterMarkdownNodeRenderer.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterMarkdownNodeRenderer.java
new file mode 100644
index 000000000..355596c8e
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterMarkdownNodeRenderer.java
@@ -0,0 +1,116 @@
+package org.commonmark.ext.front.matter.internal;
+
+import java.util.List;
+import org.commonmark.ext.front.matter.YamlFrontMatterNode;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+
+public class YamlFrontMatterMarkdownNodeRenderer extends YamlFrontMatterNodeRenderer {
+
+ private final MarkdownWriter writer;
+
+ public YamlFrontMatterMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+ this.writer = context.getWriter();
+ }
+
+ @Override
+ public void render(Node node) {
+ renderBoundary();
+ Node child = node.getFirstChild();
+ while (child != null) {
+ if (child instanceof YamlFrontMatterNode) {
+ renderNode((YamlFrontMatterNode) child);
+ }
+ child = child.getNext();
+ }
+ renderBoundary();
+ writer.line();
+ }
+
+ private void renderBoundary() {
+ writer.raw("---");
+ writer.line();
+ }
+
+ private void renderNode(YamlFrontMatterNode node) {
+ var values = node.getValues();
+ if (values.isEmpty()) {
+ renderEmptyValue(node.getKey());
+ } else if (values.size() == 1) {
+ var value = values.get(0);
+ if (value.contains("\n")) {
+ renderMultiLineValue(node.getKey(), value.split("\n"));
+ } else {
+ renderSingleValue(node.getKey(), value);
+ }
+ } else {
+ renderListValue(node.getKey(), values);
+ }
+ }
+
+ private void renderEmptyValue(String key) {
+ writer.raw(key + ":");
+ writer.line();
+ }
+
+ private void renderSingleValue(String key, String value) {
+ writer.raw(key + ": " + escapeValue(value));
+ writer.line();
+ }
+
+ private void renderMultiLineValue(String key, String[] lines) {
+ writer.raw(key + ": |");
+ writer.line();
+ for (var line : lines) {
+ writer.raw(" " + line);
+ writer.line();
+ }
+ }
+
+ private void renderListValue(String key, List values) {
+ writer.raw(key + ":");
+ writer.line();
+ for (var value : values) {
+ writer.raw(" - " + escapeValue(value));
+ writer.line();
+ }
+ }
+
+ private String escapeValue(String value) {
+ if (needsQuoting(value)) {
+ return "'" + value.replace("'", "''") + "'";
+ }
+ return value;
+ }
+
+ private boolean needsQuoting(String value) {
+ /*
+ * NOTE: Deliberately not escaping values which are balanced flow-style arrays/mappings.
+ * This preserves the round-trip behaviour where these are parsed as a plain string - outputting them as-is will
+ * result in a valid flow-style array/mapping in the output.
+ */
+ if (isFlowCollection(value)) {
+ return false;
+ }
+
+ return value.isEmpty()
+ // Key/value separator
+ || value.contains(": ")
+ // Comment indicator
+ || value.contains(" #")
+ // List indicator
+ || value.startsWith("-")
+ || value.contains("'")
+ || value.contains("\"")
+ // Unbalanced flow-style list
+ || value.startsWith("[")
+ // Unbalanced flow-style mapping
+ || value.startsWith("{");
+ }
+
+ private boolean isFlowCollection(String value) {
+ return (value.startsWith("[") && value.endsWith("]"))
+ || (value.startsWith("{") && value.endsWith("}"));
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterNodeRenderer.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterNodeRenderer.java
new file mode 100644
index 000000000..607b621ec
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterNodeRenderer.java
@@ -0,0 +1,13 @@
+package org.commonmark.ext.front.matter.internal;
+
+import java.util.Set;
+import org.commonmark.ext.front.matter.YamlFrontMatterBlock;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.NodeRenderer;
+
+abstract class YamlFrontMatterNodeRenderer implements NodeRenderer {
+ @Override
+ public Set> getNodeTypes() {
+ return Set.of(YamlFrontMatterBlock.class);
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterMarkdownRendererTest.java b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterMarkdownRendererTest.java
new file mode 100644
index 000000000..987513ea4
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterMarkdownRendererTest.java
@@ -0,0 +1,199 @@
+package org.commonmark.ext.front.matter;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Document;
+import org.commonmark.node.Node;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.Text;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class YamlFrontMatterMarkdownRendererTest {
+
+ private static final List EXTENSIONS = List.of(YamlFrontMatterExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
+
+ // ===== Round-trip tests (parse string -> render -> compare to input) =====
+
+ @Test
+ public void testRoundTripSimple() {
+ assertRoundTrip("---\ntitle: My Document\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripEmptyValue() {
+ assertRoundTrip("---\nkey:\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripMultipleKeys() {
+ assertRoundTrip("---\ntitle: My Document\nauthor: John Doe\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripListValues() {
+ assertRoundTrip("---\ntags:\n - java\n - markdown\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripLiteralBlock() {
+ assertRoundTrip("---\ndescription: |\n first line\n second line\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripSingleQuotedValue() {
+ assertRoundTrip("---\nkey: 'value with ''single quotes'''\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripDoubleQuotedValue() {
+ /*
+ * NOTE: We don't know what the original escape character was and the markdown renderer always uses single
+ * quote, hence why this technically doesn't round-trip.
+ */
+ var input = "---\nkey: \"value with \\\"double quotes\\\"\"\n---\n\nMarkdown content\n";
+ var rendered = RENDERER.render(PARSER.parse(input));
+ var expected = "---\nkey: 'value with \"double quotes\"'\n---\n\nMarkdown content\n";
+ assertThat(rendered).isEqualTo(expected);
+ }
+
+ @Test
+ public void testRoundTripFlowList() {
+ // Flow-style list is stored as a single value - "[java, markdown]" - rendered back unquoted
+ assertRoundTrip("---\ntags: [java, markdown]\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripFlowMapping() {
+ // Flow-style mapping is stored as a single value - "{key: value}" - rendered back unquoted
+ assertRoundTrip("---\ndata: {key: value}\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testRoundTripEmptyFrontmatter() {
+ assertRoundTrip("---\n---\n\nMarkdown content\n");
+ }
+
+ // ===== Programmatic construction tests =====
+
+ @Test
+ public void testProgrammaticallyBuilt() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("title", List.of("My Document"))));
+
+ assertRenderedEquals(doc, "---\ntitle: My Document\n---\n\nMarkdown content\n");
+ }
+
+ // ===== Quoting tests (values needing special treatment) =====
+
+ @Test
+ public void testValueWithColonSpace() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("value with a: colon inside"))));
+
+ assertRenderedEquals(doc, "---\nkey: 'value with a: colon inside'\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueWithColonNoSpace() {
+ // Colon without trailing space is fine unquoted (e.g. timestamps, URLs)
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("time", List.of("12:30:00"))));
+
+ assertRenderedEquals(doc, "---\ntime: 12:30:00\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueStartingWithDash() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("- not a list"))));
+
+ assertRenderedEquals(doc, "---\nkey: '- not a list'\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueStartingWithUnmatchedBracket() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("[broken"))));
+
+ assertRenderedEquals(doc, "---\nkey: '[broken'\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueStartingWithMatchedBrackets() {
+ // Valid flow list - should NOT be quoted
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("flowList", List.of("[1, 2, 3]"))));
+
+ assertRenderedEquals(doc, "---\nflowList: [1, 2, 3]\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueStartingWithUnmatchedBrace() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("{broken"))));
+
+ assertRenderedEquals(doc, "---\nkey: '{broken'\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueStartingWithMatchedBraces() {
+ // Valid flow mapping - should NOT be quoted
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("flowMapping", List.of("{key: val}"))));
+
+ assertRenderedEquals(doc, "---\nflowMapping: {key: val}\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueContainingHashComment() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("value # not a comment"))));
+
+ assertRenderedEquals(doc, "---\nkey: 'value # not a comment'\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueContainingApostrophe() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("it's a test"))));
+
+ assertRenderedEquals(doc, "---\nkey: 'it''s a test'\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testEmptyStringValue() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("empty", List.of(""))));
+
+ assertRenderedEquals(doc, "---\nempty: ''\n---\n\nMarkdown content\n");
+ }
+
+ @Test
+ public void testValueStartingWithDoubleQuote() {
+ var doc = buildDocumentWithFrontMatter(List.of(new YamlFrontMatterNode("key", List.of("\"quotes within value\""))));
+
+ assertRenderedEquals(doc, "---\nkey: '\"quotes within value\"'\n---\n\nMarkdown content\n");
+ }
+
+ private void assertRoundTrip(String input) {
+ String rendered = RENDERER.render(PARSER.parse(input));
+ assertThat(rendered).isEqualTo(input);
+ }
+
+ private void assertRenderedEquals(Node inputNode, String expectedOutput) {
+ var renderedOutput = RENDERER.render(inputNode);
+ assertThat(renderedOutput).isEqualTo(expectedOutput);
+ }
+
+ private Document buildDocumentWithFrontMatter(List frontMatterNodes) {
+ var doc = new Document();
+
+ var frontmatter = new YamlFrontMatterBlock();
+ for (var frontMatterNode : frontMatterNodes) {
+ frontmatter.appendChild(frontMatterNode);
+ }
+ doc.appendChild(frontmatter);
+
+ var para = new Paragraph();
+ para.appendChild(new Text("Markdown content"));
+ doc.appendChild(para);
+
+ return doc;
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java
index db17d4a4e..e0555dc0a 100644
--- a/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java
+++ b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java
@@ -322,8 +322,7 @@ private Map> getFrontMatter(String input) {
Node document = PARSER.parse(input);
document.accept(visitor);
- Map> data = visitor.getData();
- return data;
+ return visitor.getData();
}
// Custom node for tests
diff --git a/commonmark-integration-test/pom.xml b/commonmark-integration-test/pom.xml
index 3433b42d4..7e0048a73 100644
--- a/commonmark-integration-test/pom.xml
+++ b/commonmark-integration-test/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-integration-test
@@ -28,6 +28,10 @@
org.commonmarkcommonmark-ext-ins
+
+ org.commonmark
+ commonmark-ext-gfm-alerts
+ org.commonmarkcommonmark-ext-gfm-strikethrough
diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java
index 8df0408cb..9090c797f 100644
--- a/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java
+++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java
@@ -4,6 +4,7 @@
import org.commonmark.ext.autolink.AutolinkExtension;
import org.commonmark.ext.footnotes.FootnotesExtension;
import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
+import org.commonmark.ext.gfm.alerts.AlertsExtension;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.ext.image.attributes.ImageAttributesExtension;
@@ -19,6 +20,7 @@ public class Extensions {
FootnotesExtension.create(),
ImageAttributesExtension.create(),
InsExtension.create(),
+ AlertsExtension.create(),
StrikethroughExtension.create(),
TablesExtension.create(),
TaskListItemsExtension.create(),
diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java
index fe14273ab..9e64f9717 100644
--- a/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java
+++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java
@@ -1,19 +1,9 @@
package org.commonmark.integration;
-import org.commonmark.Extension;
-import org.commonmark.ext.autolink.AutolinkExtension;
-import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
-import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
-import org.commonmark.ext.gfm.tables.TablesExtension;
-import org.commonmark.ext.image.attributes.ImageAttributesExtension;
-import org.commonmark.ext.ins.InsExtension;
-import org.commonmark.ext.task.list.items.TaskListItemsExtension;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.markdown.MarkdownRenderer;
import org.junit.jupiter.api.Test;
-import java.util.List;
-
import static org.assertj.core.api.Assertions.assertThat;
public class MarkdownRendererIntegrationTest {
diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java
index 171cc51b1..4d18fa937 100644
--- a/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java
+++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java
@@ -2,7 +2,6 @@
import org.commonmark.parser.IncludeSourceSpans;
import org.commonmark.parser.Parser;
-import org.commonmark.testutil.example.Example;
/**
* Spec and all extensions, with source spans enabled.
diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java
index 07853d402..6dca4024e 100644
--- a/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java
+++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java
@@ -2,7 +2,6 @@
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.parser.Parser;
-import org.commonmark.testutil.example.Example;
import org.commonmark.testutil.SpecTestCase;
import org.junit.jupiter.api.Test;
diff --git a/commonmark-integration-test/src/test/java/org/commonmark/ui/DingusApp.java b/commonmark-integration-test/src/test/java/org/commonmark/ui/DingusApp.java
index 0e98386bb..d8132fe25 100644
--- a/commonmark-integration-test/src/test/java/org/commonmark/ui/DingusApp.java
+++ b/commonmark-integration-test/src/test/java/org/commonmark/ui/DingusApp.java
@@ -6,8 +6,6 @@
import java.awt.*;
import javax.swing.*;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
@@ -79,12 +77,7 @@ public void changedUpdate(DocumentEvent e) {
tabbedPane.addTab("HTML source", htmlSourceRendererOutput);
tabbedPane.addTab("Plain text", textRendererOutput);
- tabbedPane.addChangeListener(new ChangeListener() {
- @Override
- public void stateChanged(ChangeEvent e) {
- updateOutput(input.getText());
- }
- });
+ tabbedPane.addChangeListener(e -> updateOutput(input.getText()));
input.setText("# Example\n" +
"Enter text *here* and see how it renders on the right.\n\n" +
diff --git a/commonmark-test-util/pom.xml b/commonmark-test-util/pom.xml
index 699adb3de..6a9c342cc 100644
--- a/commonmark-test-util/pom.xml
+++ b/commonmark-test-util/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-test-util
diff --git a/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java b/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java
index f7da4c008..b585f4604 100644
--- a/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java
+++ b/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java
@@ -1,7 +1,5 @@
package org.commonmark.testutil;
-import static org.assertj.core.api.Assertions.assertThat;
-
public abstract class RenderingTestCase {
protected abstract String render(String source);
diff --git a/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java b/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java
index c29a6a69a..d61afbc4a 100644
--- a/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java
+++ b/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java
@@ -4,10 +4,8 @@
import org.commonmark.testutil.example.ExampleReader;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
-import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
-import java.util.ArrayList;
import java.util.List;
@ParameterizedClass
diff --git a/commonmark/pom.xml b/commonmark/pom.xml
index 76dad6c83..4e060edaa 100644
--- a/commonmark/pom.xml
+++ b/commonmark/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark
diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentBlockParser.java
index db3d3854f..4b51275ee 100644
--- a/commonmark/src/main/java/org/commonmark/internal/DocumentBlockParser.java
+++ b/commonmark/src/main/java/org/commonmark/internal/DocumentBlockParser.java
@@ -2,7 +2,6 @@
import org.commonmark.node.Block;
import org.commonmark.node.Document;
-import org.commonmark.parser.SourceLine;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.ParserState;
@@ -31,8 +30,4 @@ public BlockContinue tryContinue(ParserState state) {
return BlockContinue.atIndex(state.getIndex());
}
- @Override
- public void addLine(SourceLine line) {
- }
-
}
diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java
index d935f8d27..17e7b9c84 100644
--- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java
+++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java
@@ -76,6 +76,7 @@ public class DocumentParser implements ParserState {
private final List linkProcessors;
private final Set linkMarkers;
private final IncludeSourceSpans includeSourceSpans;
+ private final int maxOpenBlockParsers;
private final DocumentBlockParser documentBlockParser;
private final Definitions definitions = new Definitions();
@@ -84,7 +85,8 @@ public class DocumentParser implements ParserState {
public DocumentParser(List blockParserFactories, InlineParserFactory inlineParserFactory,
List inlineContentParserFactories, List delimiterProcessors,
- List linkProcessors, Set linkMarkers, IncludeSourceSpans includeSourceSpans) {
+ List linkProcessors, Set linkMarkers,
+ IncludeSourceSpans includeSourceSpans, int maxOpenBlockParsers) {
this.blockParserFactories = blockParserFactories;
this.inlineParserFactory = inlineParserFactory;
this.inlineContentParserFactories = inlineContentParserFactories;
@@ -92,6 +94,7 @@ public DocumentParser(List blockParserFactories, InlineParse
this.linkProcessors = linkProcessors;
this.linkMarkers = linkMarkers;
this.includeSourceSpans = includeSourceSpans;
+ this.maxOpenBlockParsers = maxOpenBlockParsers;
this.documentBlockParser = new DocumentBlockParser();
activateBlockParser(new OpenBlockParser(documentBlockParser, 0));
@@ -102,9 +105,8 @@ public static Set> getDefaultBlockParserTypes() {
}
public static List calculateBlockParserFactories(List customBlockParserFactories, Set> enabledBlockTypes) {
- List list = new ArrayList<>();
// By having the custom factories come first, extensions are able to change behavior of core syntax.
- list.addAll(customBlockParserFactories);
+ List list = new ArrayList<>(customBlockParserFactories);
for (Class extends Block> blockType : enabledBlockTypes) {
list.add(NODES_TO_CORE_FACTORIES.get(blockType));
}
@@ -461,6 +463,9 @@ private void addSourceSpans() {
}
private BlockStartImpl findBlockStart(BlockParser blockParser) {
+ if (openBlockParsers.size() > maxOpenBlockParsers) {
+ return null;
+ }
MatchedBlockParser matchedBlockParser = new MatchedBlockParserImpl(blockParser);
for (BlockParserFactory blockParserFactory : blockParserFactories) {
BlockStart result = blockParserFactory.tryStart(this, matchedBlockParser);
diff --git a/commonmark/src/main/java/org/commonmark/internal/util/Escaping.java b/commonmark/src/main/java/org/commonmark/internal/util/Escaping.java
index 3350003c0..caf802a43 100644
--- a/commonmark/src/main/java/org/commonmark/internal/util/Escaping.java
+++ b/commonmark/src/main/java/org/commonmark/internal/util/Escaping.java
@@ -7,7 +7,7 @@
public class Escaping {
- public static final String ESCAPABLE = "[!\"#$%&\'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~-]";
+ public static final String ESCAPABLE = "[!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~-]";
public static final String ENTITY = "&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});";
@@ -25,36 +25,30 @@ public class Escaping {
private static final Pattern WHITESPACE = Pattern.compile("[ \t\r\n]+");
- private static final Replacer UNESCAPE_REPLACER = new Replacer() {
- @Override
- public void replace(String input, StringBuilder sb) {
- if (input.charAt(0) == '\\') {
- sb.append(input, 1, input.length());
- } else {
- sb.append(Html5Entities.entityToString(input));
- }
+ private static final Replacer UNESCAPE_REPLACER = (input, sb) -> {
+ if (input.charAt(0) == '\\') {
+ sb.append(input, 1, input.length());
+ } else {
+ sb.append(Html5Entities.entityToString(input));
}
};
- private static final Replacer URI_REPLACER = new Replacer() {
- @Override
- public void replace(String input, StringBuilder sb) {
- if (input.startsWith("%")) {
- if (input.length() == 3) {
- // Already percent-encoded, preserve
- sb.append(input);
- } else {
- // %25 is the percent-encoding for %
- sb.append("%25");
- sb.append(input, 1, input.length());
- }
+ private static final Replacer URI_REPLACER = (input, sb) -> {
+ if (input.startsWith("%")) {
+ if (input.length() == 3) {
+ // Already percent-encoded, preserve
+ sb.append(input);
} else {
- byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
- for (byte b : bytes) {
- sb.append('%');
- sb.append(HEX_DIGITS[(b >> 4) & 0xF]);
- sb.append(HEX_DIGITS[b & 0xF]);
- }
+ // %25 is the percent-encoding for %
+ sb.append("%25");
+ sb.append(input, 1, input.length());
+ }
+ } else {
+ byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
+ for (byte b : bytes) {
+ sb.append('%');
+ sb.append(HEX_DIGITS[(b >> 4) & 0xF]);
+ sb.append(HEX_DIGITS[b & 0xF]);
}
}
};
@@ -63,7 +57,6 @@ public static String escapeHtml(String input) {
// Avoid building a new string in the majority of cases (nothing to escape)
StringBuilder sb = null;
- loop:
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
String replacement;
@@ -84,7 +77,7 @@ public static String escapeHtml(String input) {
if (sb != null) {
sb.append(c);
}
- continue loop;
+ continue;
}
if (sb == null) {
sb = new StringBuilder();
diff --git a/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java b/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java
index 8da53c053..d6034ff14 100644
--- a/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java
+++ b/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java
@@ -54,7 +54,7 @@ private static Map readEntities() {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream, charset))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
- if (line.length() == 0) {
+ if (line.isEmpty()) {
continue;
}
int equal = line.indexOf("=");
diff --git a/commonmark/src/main/java/org/commonmark/node/Node.java b/commonmark/src/main/java/org/commonmark/node/Node.java
index d95a72c60..abbda67f2 100644
--- a/commonmark/src/main/java/org/commonmark/node/Node.java
+++ b/commonmark/src/main/java/org/commonmark/node/Node.java
@@ -50,11 +50,10 @@ public void appendChild(Node child) {
if (this.lastChild != null) {
this.lastChild.next = child;
child.prev = this.lastChild;
- this.lastChild = child;
} else {
this.firstChild = child;
- this.lastChild = child;
}
+ this.lastChild = child;
}
public void prependChild(Node child) {
diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java
index b98d0581f..8faac789b 100644
--- a/commonmark/src/main/java/org/commonmark/parser/Parser.java
+++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java
@@ -37,6 +37,7 @@ public class Parser {
private final InlineParserFactory inlineParserFactory;
private final List postProcessors;
private final IncludeSourceSpans includeSourceSpans;
+ private final int maxOpenBlockParsers;
private Parser(Builder builder) {
this.blockParserFactories = DocumentParser.calculateBlockParserFactories(builder.blockParserFactories, builder.enabledBlockTypes);
@@ -47,6 +48,7 @@ private Parser(Builder builder) {
this.linkProcessors = builder.linkProcessors;
this.linkMarkers = builder.linkMarkers;
this.includeSourceSpans = builder.includeSourceSpans;
+ this.maxOpenBlockParsers = builder.maxOpenBlockParsers;
// Try to construct an inline parser. Invalid configuration might result in an exception, which we want to
// detect as soon as possible.
@@ -106,7 +108,7 @@ public Node parseReader(Reader input) throws IOException {
private DocumentParser createDocumentParser() {
return new DocumentParser(blockParserFactories, inlineParserFactory, inlineContentParserFactories,
- delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans);
+ delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans, maxOpenBlockParsers);
}
private Node postProcess(Node document) {
@@ -129,6 +131,7 @@ public static class Builder {
private Set> enabledBlockTypes = DocumentParser.getDefaultBlockParserTypes();
private InlineParserFactory inlineParserFactory;
private IncludeSourceSpans includeSourceSpans = IncludeSourceSpans.NONE;
+ private int maxOpenBlockParsers = Integer.MAX_VALUE;
/**
* @return the configured {@link Parser}
@@ -200,6 +203,27 @@ public Builder includeSourceSpans(IncludeSourceSpans includeSourceSpans) {
return this;
}
+ /**
+ * Limit how many block parsers may be open at once while parsing.
+ *
+ * Once the limit is reached, additional block starts are treated as plain text instead of
+ * creating deeper nested block structure.
+ *
+ * The document root parser is not counted. The default is unlimited, so callers that keep
+ * using {@code Parser.builder().build()} preserve behavior.
+ *
+ * @param maxOpenBlockParsers maximum number of open non-document block parsers, must be
+ * zero or greater
+ * @return {@code this}
+ */
+ public Builder maxOpenBlockParsers(int maxOpenBlockParsers) {
+ if (maxOpenBlockParsers < 0) {
+ throw new IllegalArgumentException("maxOpenBlockParsers must be >= 0");
+ }
+ this.maxOpenBlockParsers = maxOpenBlockParsers;
+ return this;
+ }
+
/**
* Add a custom block parser factory.
*
diff --git a/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java
index 386abebf0..b0264fc72 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java
@@ -4,7 +4,6 @@
import org.commonmark.internal.renderer.NodeRendererMap;
import org.commonmark.internal.util.Escaping;
import org.commonmark.node.*;
-import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.Renderer;
import java.util.*;
@@ -41,12 +40,7 @@ private HtmlRenderer(Builder builder) {
this.nodeRendererFactories = new ArrayList<>(builder.nodeRendererFactories.size() + 1);
this.nodeRendererFactories.addAll(builder.nodeRendererFactories);
// Add as last. This means clients can override the rendering of core nodes if they want.
- this.nodeRendererFactories.add(new HtmlNodeRendererFactory() {
- @Override
- public NodeRenderer create(HtmlNodeRendererContext context) {
- return new CoreHtmlNodeRenderer(context);
- }
- });
+ this.nodeRendererFactories.add(CoreHtmlNodeRenderer::new);
}
/**
diff --git a/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java
index ee564cbdb..62a1a054d 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java
@@ -236,7 +236,7 @@ private void writeText(String text) {
private void writeLink(Node node, String title, String destination) {
boolean hasChild = node.getFirstChild() != null;
boolean hasTitle = title != null && !title.equals(destination);
- boolean hasDestination = destination != null && !destination.equals("");
+ boolean hasDestination = destination != null && !destination.isEmpty();
if (hasChild) {
textContent.write('"');
diff --git a/commonmark/src/main/java/org/commonmark/renderer/text/TextContentRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/text/TextContentRenderer.java
index d64d0c7ef..9a8bcc997 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/text/TextContentRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/text/TextContentRenderer.java
@@ -3,7 +3,6 @@
import org.commonmark.Extension;
import org.commonmark.internal.renderer.NodeRendererMap;
import org.commonmark.node.Node;
-import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.Renderer;
import java.util.ArrayList;
@@ -24,12 +23,7 @@ private TextContentRenderer(Builder builder) {
this.nodeRendererFactories = new ArrayList<>(builder.nodeRendererFactories.size() + 1);
this.nodeRendererFactories.addAll(builder.nodeRendererFactories);
// Add as last. This means clients can override the rendering of core nodes if they want.
- this.nodeRendererFactories.add(new TextContentNodeRendererFactory() {
- @Override
- public NodeRenderer create(TextContentNodeRendererContext context) {
- return new CoreTextContentNodeRenderer(context);
- }
- });
+ this.nodeRendererFactories.add(CoreTextContentNodeRenderer::new);
}
/**
diff --git a/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java b/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java
index b52713846..7d0e0beb7 100644
--- a/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java
+++ b/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java
@@ -100,7 +100,7 @@ private SlowStringReader(String s) {
}
@Override
- public int read(char[] cbuf, int off, int len) throws IOException {
+ public int read(char[] cbuf, int off, int len) {
Objects.checkFromIndexSize(off, len, cbuf.length);
if (len == 0) {
return 0;
@@ -118,7 +118,7 @@ public int read(char[] cbuf, int off, int len) throws IOException {
}
@Override
- public void close() throws IOException {
+ public void close() {
}
}
}
diff --git a/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java b/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java
index 02d970949..92751147a 100644
--- a/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java
@@ -162,23 +162,15 @@ public void percentEncodeUrl() {
@Test
public void attributeProviderForCodeBlock() {
- AttributeProviderFactory custom = new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return new AttributeProvider() {
- @Override
- public void setAttributes(Node node, String tagName, Map attributes) {
- if (node instanceof FencedCodeBlock && tagName.equals("code")) {
- FencedCodeBlock fencedCodeBlock = (FencedCodeBlock) node;
- // Remove the default attribute for info
- attributes.remove("class");
- // Put info in custom attribute instead
- attributes.put("data-custom", fencedCodeBlock.getInfo());
- } else if (node instanceof FencedCodeBlock && tagName.equals("pre")) {
- attributes.put("data-code-block", "fenced");
- }
- }
- };
+ AttributeProviderFactory custom = context -> (node, tagName, attributes) -> {
+ if (node instanceof FencedCodeBlock && tagName.equals("code")) {
+ FencedCodeBlock fencedCodeBlock = (FencedCodeBlock) node;
+ // Remove the default attribute for info
+ attributes.remove("class");
+ // Put info in custom attribute instead
+ attributes.put("data-custom", fencedCodeBlock.getInfo());
+ } else if (node instanceof FencedCodeBlock && tagName.equals("pre")) {
+ attributes.put("data-code-block", "fenced");
}
};
@@ -192,18 +184,10 @@ public void setAttributes(Node node, String tagName, Map attribu
@Test
public void attributeProviderForImage() {
- AttributeProviderFactory custom = new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return new AttributeProvider() {
- @Override
- public void setAttributes(Node node, String tagName, Map attributes) {
- if (node instanceof Image) {
- attributes.remove("alt");
- attributes.put("test", "hey");
- }
- }
- };
+ AttributeProviderFactory custom = context -> (node, tagName, attributes) -> {
+ if (node instanceof Image) {
+ attributes.remove("alt");
+ attributes.put("test", "hey");
}
};
@@ -214,18 +198,13 @@ public void setAttributes(Node node, String tagName, Map attribu
@Test
public void attributeProviderFactoryNewInstanceForEachRender() {
- AttributeProviderFactory factory = new AttributeProviderFactory() {
+ AttributeProviderFactory factory = context -> new AttributeProvider() {
+ int i = 0;
+
@Override
- public AttributeProvider create(AttributeProviderContext context) {
- return new AttributeProvider() {
- int i = 0;
-
- @Override
- public void setAttributes(Node node, String tagName, Map attributes) {
- attributes.put("key", "" + i);
- i++;
- }
- };
+ public void setAttributes(Node node, String tagName, Map attributes) {
+ attributes.put("key", "" + i);
+ i++;
}
};
@@ -237,20 +216,15 @@ public void setAttributes(Node node, String tagName, Map attribu
@Test
public void overrideNodeRender() {
- HtmlNodeRendererFactory nodeRendererFactory = new HtmlNodeRendererFactory() {
+ HtmlNodeRendererFactory nodeRendererFactory = context -> new NodeRenderer() {
+ @Override
+ public Set> getNodeTypes() {
+ return Set.of(Link.class);
+ }
+
@Override
- public NodeRenderer create(final HtmlNodeRendererContext context) {
- return new NodeRenderer() {
- @Override
- public Set> getNodeTypes() {
- return Set.of(Link.class);
- }
-
- @Override
- public void render(Node node) {
- context.getWriter().text("test");
- }
- };
+ public void render(Node node) {
+ context.getWriter().text("test");
}
};
diff --git a/commonmark/src/test/java/org/commonmark/test/ParserTest.java b/commonmark/src/test/java/org/commonmark/test/ParserTest.java
index c119b5e2d..4348563bd 100644
--- a/commonmark/src/test/java/org/commonmark/test/ParserTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/ParserTest.java
@@ -2,8 +2,8 @@
import org.commonmark.node.*;
import org.commonmark.parser.*;
-import org.commonmark.parser.block.*;
import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
import org.commonmark.testutil.TestResources;
import org.junit.jupiter.api.Test;
@@ -13,10 +13,7 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -92,20 +89,9 @@ public void indentation() {
@Test
public void inlineParser() {
- final InlineParser fakeInlineParser = new InlineParser() {
- @Override
- public void parse(SourceLines lines, Node node) {
- node.appendChild(new ThematicBreak());
- }
- };
-
- InlineParserFactory fakeInlineParserFactory = new InlineParserFactory() {
+ final InlineParser fakeInlineParser = (lines, node) -> node.appendChild(new ThematicBreak());
- @Override
- public InlineParser create(InlineParserContext inlineParserContext) {
- return fakeInlineParser;
- }
- };
+ InlineParserFactory fakeInlineParserFactory = inlineParserContext -> fakeInlineParser;
Parser parser = Parser.builder().inlineParserFactory(fakeInlineParserFactory).build();
String input = "**bold** **bold** ~~strikethrough~~";
@@ -135,6 +121,76 @@ public void threading() throws Exception {
}
}
+ @Test
+ public void maxOpenBlockParsersMustBeZeroOrGreater() {
+ assertThatThrownBy(() ->
+ Parser.builder().maxOpenBlockParsers(-1)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void maxOpenBlockParsersIsOptIn() {
+ var parser = Parser.builder().build();
+
+ var document = parser.parse(alternatingNestedList(9));
+
+ assertThat(renderText(deepestStructuredParagraph(document, 9))).isEqualTo("level9");
+ }
+
+ @Test
+ public void maxOpenBlockParsersPreservesSevenLogicalListLevelsAtSeventeenBlocks() {
+ var parser = Parser.builder().maxOpenBlockParsers(17).build();
+
+ var document = parser.parse(alternatingNestedList(7));
+
+ assertThat(renderText(deepestStructuredParagraph(document, 7))).isEqualTo("level7");
+ }
+
+ @Test
+ public void maxOpenBlockParsersPreservesEightLogicalListLevelsAtSeventeenBlocks() {
+ var parser = Parser.builder().maxOpenBlockParsers(17).build();
+
+ var document = parser.parse(alternatingNestedList(8));
+
+ assertThat(renderText(deepestStructuredParagraph(document, 8))).isEqualTo("level8");
+ }
+
+ @Test
+ public void maxOpenBlockParsersDegradesTheNinthLogicalListLevelToPlainText() {
+ var parser = Parser.builder().maxOpenBlockParsers(17).build();
+
+ var document = parser.parse(alternatingNestedList(9));
+ var deepestParagraph = deepestStructuredParagraph(document, 8);
+
+ assertThat(renderText(deepestParagraph)).isEqualTo("level8\n\\- level9");
+ assertThat(deepestParagraph.getNext()).isNull();
+ }
+
+ @Test
+ public void maxOpenBlockParsersAlsoLimitsMixedListAndBlockQuoteNesting() {
+ var parser = Parser.builder().maxOpenBlockParsers(5).build();
+
+ var document = parser.parse(String.join("\n",
+ "- level1",
+ " > level2",
+ " > > level3",
+ " > > > level4"));
+
+ var listBlock = document.getFirstChild();
+ assertThat(listBlock).isInstanceOf(BulletList.class);
+
+ var listItem = listBlock.getFirstChild();
+ var blockQuote1 = listItem.getLastChild();
+ assertThat(blockQuote1).isInstanceOf(BlockQuote.class);
+
+ var blockQuote2 = blockQuote1.getLastChild();
+ assertThat(blockQuote2).isInstanceOf(BlockQuote.class);
+
+ var deepestParagraph = blockQuote2.getLastChild();
+ assertThat(deepestParagraph).isInstanceOf(Paragraph.class);
+ assertThat(renderText(deepestParagraph)).isEqualTo("level3\n\\> level4");
+ assertThat(deepestParagraph.getNext()).isNull();
+ }
+
private String firstText(Node n) {
while (!(n instanceof Text)) {
assertThat(n).isNotNull();
@@ -142,4 +198,44 @@ private String firstText(Node n) {
}
return ((Text) n).getLiteral();
}
+
+ private Paragraph deepestStructuredParagraph(Node document, int levels) {
+ Node node = document.getFirstChild();
+ for (int level = 1; level <= levels; level++) {
+ assertThat(node).isInstanceOf(ListBlock.class);
+ var listItem = node.getFirstChild();
+ assertThat(listItem).isNotNull();
+ if (level == levels) {
+ assertThat(listItem.getFirstChild()).isInstanceOf(Paragraph.class);
+ return (Paragraph) listItem.getFirstChild();
+ }
+ node = listItem.getLastChild();
+ }
+ throw new AssertionError("unreachable");
+ }
+
+ private String renderText(Node node) {
+ return MarkdownRenderer.builder().build().render(node).trim();
+ }
+
+ private String alternatingNestedList(int levels) {
+ int indent = 0;
+ var lines = new ArrayList();
+ for (int level = 1; level <= levels; level++) {
+ var ordered = level % 2 == 0;
+ var marker = ordered ? "1. " : "- ";
+ lines.add(" ".repeat(indent) + marker + "level" + level);
+ indent += marker.length();
+ }
+ return String.join("\n", lines);
+ }
+
+ private int depth(Node node) {
+ int depth = 0;
+ while (node.getParent() != null) {
+ node = node.getParent();
+ depth++;
+ }
+ return depth;
+ }
}
diff --git a/commonmark/src/test/java/org/commonmark/test/SourceSpanRenderer.java b/commonmark/src/test/java/org/commonmark/test/SourceSpanRenderer.java
index c29aac61e..597a1e31c 100644
--- a/commonmark/src/test/java/org/commonmark/test/SourceSpanRenderer.java
+++ b/commonmark/src/test/java/org/commonmark/test/SourceSpanRenderer.java
@@ -43,7 +43,7 @@ public static String renderWithInputIndex(Node document, String source) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < source.length(); i++) {
- markers.getOrDefault(i, List.of()).forEach(marker -> sb.append(marker));
+ markers.getOrDefault(i, List.of()).forEach(sb::append);
sb.append(source.charAt(i));
}
return sb.toString();
diff --git a/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java b/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java
index fefd8fb30..047a1e6cf 100644
--- a/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java
@@ -6,7 +6,6 @@
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.testutil.SpecTestCase;
-import org.commonmark.testutil.example.Example;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.fail;
diff --git a/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java b/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java
index 47ca3da4e..eb0fcd0dd 100644
--- a/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java
@@ -3,7 +3,6 @@
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.testutil.SpecTestCase;
-import org.commonmark.testutil.example.Example;
import org.junit.jupiter.api.Test;
import static org.commonmark.testutil.Asserts.assertRendering;
diff --git a/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java b/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java
index a9f37792e..4641b616e 100644
--- a/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java
@@ -8,7 +8,7 @@
public class TextContentWriterTest {
@Test
- public void whitespace() throws Exception {
+ public void whitespace() {
StringBuilder stringBuilder = new StringBuilder();
TextContentWriter writer = new TextContentWriter(stringBuilder);
writer.write("foo");
@@ -18,7 +18,7 @@ public void whitespace() throws Exception {
}
@Test
- public void colon() throws Exception {
+ public void colon() {
StringBuilder stringBuilder = new StringBuilder();
TextContentWriter writer = new TextContentWriter(stringBuilder);
writer.write("foo");
@@ -28,7 +28,7 @@ public void colon() throws Exception {
}
@Test
- public void line() throws Exception {
+ public void line() {
StringBuilder stringBuilder = new StringBuilder();
TextContentWriter writer = new TextContentWriter(stringBuilder);
writer.write("foo");
@@ -38,7 +38,7 @@ public void line() throws Exception {
}
@Test
- public void writeStripped() throws Exception {
+ public void writeStripped() {
StringBuilder stringBuilder = new StringBuilder();
TextContentWriter writer = new TextContentWriter(stringBuilder);
writer.writeStripped("foo\n bar");
@@ -46,7 +46,7 @@ public void writeStripped() throws Exception {
}
@Test
- public void write() throws Exception {
+ public void write() {
StringBuilder stringBuilder = new StringBuilder();
TextContentWriter writer = new TextContentWriter(stringBuilder);
writer.writeStripped("foo bar");
diff --git a/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java b/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java
index 20cd9f5ab..1a3625848 100644
--- a/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java
@@ -78,12 +78,7 @@ public void sourcePositions() {
public void addAttributes() {
Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder()
- .attributeProviderFactory(new AttributeProviderFactory() {
- @Override
- public AttributeProvider create(AttributeProviderContext context) {
- return new ImageAttributeProvider();
- }
- })
+ .attributeProviderFactory(context -> new ImageAttributeProvider())
.build();
Node document = parser.parse("");
@@ -94,19 +89,14 @@ public AttributeProvider create(AttributeProviderContext context) {
public void customizeRendering() {
Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder()
- .nodeRendererFactory(new HtmlNodeRendererFactory() {
- @Override
- public NodeRenderer create(HtmlNodeRendererContext context) {
- return new IndentedCodeBlockNodeRenderer(context);
- }
- })
+ .nodeRendererFactory(IndentedCodeBlockNodeRenderer::new)
.build();
Node document = parser.parse("Example:\n\n code");
assertThat(renderer.render(document)).isEqualTo("
Example:
\n
code\n
\n");
}
- class WordCountVisitor extends AbstractVisitor {
+ static class WordCountVisitor extends AbstractVisitor {
int wordCount = 0;
@@ -122,7 +112,7 @@ public void visit(Text text) {
}
}
- class ImageAttributeProvider implements AttributeProvider {
+ static class ImageAttributeProvider implements AttributeProvider {
@Override
public void setAttributes(Node node, String tagName, Map attributes) {
if (node instanceof Image) {
@@ -131,7 +121,7 @@ public void setAttributes(Node node, String tagName, Map attribu
}
}
- class IndentedCodeBlockNodeRenderer implements NodeRenderer {
+ static class IndentedCodeBlockNodeRenderer implements NodeRenderer {
private final HtmlWriter html;
diff --git a/pom.xml b/pom.xml
index d55d1fd85..f12805316 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.commonmarkcommonmark-parent
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-java parent
Java implementation of CommonMark, a specification of the Markdown format for turning plain text into formatted
@@ -17,6 +17,7 @@
commonmarkcommonmark-ext-autolinkcommonmark-ext-footnotes
+ commonmark-ext-gfm-alertscommonmark-ext-gfm-strikethroughcommonmark-ext-gfm-tablescommonmark-ext-heading-anchor
@@ -132,57 +133,62 @@
org.commonmarkcommonmark
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-autolink
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-footnotes
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-image-attributes
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-ins
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOT
+
+
+ org.commonmark
+ commonmark-ext-gfm-alerts
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-gfm-strikethrough
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-gfm-tables
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-heading-anchor
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-task-list-items
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-ext-yaml-front-matter
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOTorg.commonmarkcommonmark-test-util
- 0.27.2-SNAPSHOT
+ 0.28.1-SNAPSHOT