Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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) {
TaskListItemMarker taskListItemNode = (TaskListItemMarker) node;
Comment thread
stupar73 marked this conversation as resolved.
Outdated
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);
}
}
Loading