Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ with the exception that 0.x versions can break between minor versions.
## [Unreleased]
### Added
- Allow customizing HTML attributes for alert title `<p>` tag via `AttributeProvider`
- Support rendering GFM task list items to Markdown

## [0.28.0] - 2026-03-31
### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +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.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.
Expand All @@ -16,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() {
}
Expand All @@ -34,4 +41,19 @@ public void extend(Parser.Builder parserBuilder) {
public void extend(HtmlRenderer.Builder rendererBuilder) {
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 Set<Character> getSpecialCharacters() {
return Set.of();
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,11 +18,6 @@ public TaskListItemHtmlNodeRenderer(HtmlNodeRendererContext context) {
this.html = context.getWriter();
}

@Override
public Set<Class<? extends Node>> getNodeTypes() {
return Set.of(TaskListItemMarker.class);
}

@Override
public void render(Node node) {
if (node instanceof TaskListItemMarker) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Class<? extends Node>> getNodeTypes() {
return Set.of(TaskListItemMarker.class);
}
}
Original file line number Diff line number Diff line change
@@ -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<Extension> 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);
}
}