Skip to content

Commit 76978a0

Browse files
authored
Merge pull request #433 from stupar73/gfm-task-list-item-markdown-render
Add support for Markdown rendering for GFM task list items
2 parents 37df896 + 63b623a commit 76978a0

6 files changed

Lines changed: 158 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ with the exception that 0.x versions can break between minor versions.
99
## [Unreleased]
1010
### Added
1111
- Allow customizing HTML attributes for alert title `<p>` tag via `AttributeProvider`
12+
- Support rendering GFM task list items to Markdown
1213

1314
## [0.28.0] - 2026-03-31
1415
### Added

commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/TaskListItemsExtension.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package org.commonmark.ext.task.list.items;
22

3+
import java.util.Set;
34
import org.commonmark.Extension;
45
import org.commonmark.ext.task.list.items.internal.TaskListItemHtmlNodeRenderer;
6+
import org.commonmark.ext.task.list.items.internal.TaskListItemMarkdownNodeRenderer;
57
import org.commonmark.ext.task.list.items.internal.TaskListItemPostProcessor;
68
import org.commonmark.parser.Parser;
9+
import org.commonmark.renderer.NodeRenderer;
710
import org.commonmark.renderer.html.HtmlRenderer;
11+
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
12+
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
13+
import org.commonmark.renderer.markdown.MarkdownRenderer;
814

915
/**
1016
* Extension for adding task list items.
@@ -16,7 +22,8 @@
1622
*
1723
* @since 0.15.0
1824
*/
19-
public class TaskListItemsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
25+
public class TaskListItemsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
26+
MarkdownRenderer.MarkdownRendererExtension {
2027

2128
private TaskListItemsExtension() {
2229
}
@@ -34,4 +41,19 @@ public void extend(Parser.Builder parserBuilder) {
3441
public void extend(HtmlRenderer.Builder rendererBuilder) {
3542
rendererBuilder.nodeRendererFactory(TaskListItemHtmlNodeRenderer::new);
3643
}
44+
45+
@Override
46+
public void extend(MarkdownRenderer.Builder rendererBuilder) {
47+
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
48+
@Override
49+
public NodeRenderer create(MarkdownNodeRendererContext context) {
50+
return new TaskListItemMarkdownNodeRenderer(context);
51+
}
52+
53+
@Override
54+
public Set<Character> getSpecialCharacters() {
55+
return Set.of();
56+
}
57+
});
58+
}
3759
}

commonmark-ext-task-list-items/src/main/java/org/commonmark/ext/task/list/items/internal/TaskListItemHtmlNodeRenderer.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
import org.commonmark.ext.task.list.items.TaskListItemMarker;
44
import org.commonmark.node.Node;
5-
import org.commonmark.renderer.NodeRenderer;
65
import org.commonmark.renderer.html.HtmlNodeRendererContext;
76
import org.commonmark.renderer.html.HtmlWriter;
87

98
import java.util.LinkedHashMap;
109
import java.util.Map;
11-
import java.util.Set;
1210

13-
public class TaskListItemHtmlNodeRenderer implements NodeRenderer {
11+
public class TaskListItemHtmlNodeRenderer extends TaskListItemNodeRenderer {
1412

1513
private final HtmlNodeRendererContext context;
1614
private final HtmlWriter html;
@@ -20,11 +18,6 @@ public TaskListItemHtmlNodeRenderer(HtmlNodeRendererContext context) {
2018
this.html = context.getWriter();
2119
}
2220

23-
@Override
24-
public Set<Class<? extends Node>> getNodeTypes() {
25-
return Set.of(TaskListItemMarker.class);
26-
}
27-
2821
@Override
2922
public void render(Node node) {
3023
if (node instanceof TaskListItemMarker) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.commonmark.ext.task.list.items.internal;
2+
3+
import org.commonmark.ext.task.list.items.TaskListItemMarker;
4+
import org.commonmark.node.Node;
5+
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
6+
import org.commonmark.renderer.markdown.MarkdownWriter;
7+
8+
public class TaskListItemMarkdownNodeRenderer extends TaskListItemNodeRenderer {
9+
10+
private final MarkdownNodeRendererContext context;
11+
private final MarkdownWriter writer;
12+
13+
public TaskListItemMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
14+
this.context = context;
15+
this.writer = context.getWriter();
16+
}
17+
18+
@Override
19+
public void render(Node node) {
20+
if (node instanceof TaskListItemMarker) {
21+
var taskListItemNode = (TaskListItemMarker) node;
22+
var checkboxFill = taskListItemNode.isChecked() ? "x" : " ";
23+
writer.raw("[" + checkboxFill + "] ");
24+
renderChildren(node);
25+
}
26+
}
27+
28+
private void renderChildren(Node parent) {
29+
Node node = parent.getFirstChild();
30+
while (node != null) {
31+
Node next = node.getNext();
32+
context.render(node);
33+
node = next;
34+
}
35+
}
36+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.commonmark.ext.task.list.items.internal;
2+
3+
import java.util.Set;
4+
import org.commonmark.ext.task.list.items.TaskListItemMarker;
5+
import org.commonmark.node.Node;
6+
import org.commonmark.renderer.NodeRenderer;
7+
8+
public abstract class TaskListItemNodeRenderer implements NodeRenderer {
9+
@Override
10+
public Set<Class<? extends Node>> getNodeTypes() {
11+
return Set.of(TaskListItemMarker.class);
12+
}
13+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.commonmark.ext.task.list.items;
2+
3+
import java.util.Set;
4+
import org.commonmark.Extension;
5+
import org.commonmark.node.BulletList;
6+
import org.commonmark.node.Document;
7+
import org.commonmark.node.ListItem;
8+
import org.commonmark.node.Node;
9+
import org.commonmark.node.Paragraph;
10+
import org.commonmark.node.Text;
11+
import org.commonmark.parser.Parser;
12+
import org.commonmark.renderer.markdown.MarkdownRenderer;
13+
import org.junit.jupiter.api.Test;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
public class TaskListItemMarkdownRendererTest {
18+
19+
private static final Set<Extension> EXTENSIONS = Set.of(TaskListItemsExtension.create());
20+
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
21+
private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
22+
23+
@Test
24+
public void testCheckedRoundTrip() {
25+
assertRoundTrip("- [x] I am checked\n");
26+
}
27+
28+
@Test
29+
public void testUncheckedRoundTrip() {
30+
assertRoundTrip("- [ ] I am unchecked\n");
31+
}
32+
33+
@Test
34+
public void testMixedRoundTrip() {
35+
assertRoundTrip("- [x] I am checked\n- [ ] I am unchecked\n");
36+
}
37+
38+
@Test
39+
public void testNestedRoundTrip() {
40+
assertRoundTrip("- [ ] I am unchecked\n - [x] I am a checked child\n");
41+
}
42+
43+
@Test
44+
public void testFormattingRoundTrip() {
45+
assertRoundTrip("- [x] I am **boldly** checked\n- [ ] I am *italicly* unchecked\n");
46+
}
47+
48+
@Test
49+
public void testNonTaskListItemRoundTrip() {
50+
assertRoundTrip("- [x] I am checked\n- [ ] I am unchecked\n- I am not a task item\n");
51+
}
52+
53+
@Test
54+
public void testOrderedListRoundTrip() {
55+
assertRoundTrip("1. [x] I am checked\n2. [ ] I am unchecked\n");
56+
}
57+
58+
@Test
59+
public void testProgrammaticallyBuilt() {
60+
var doc = new Document();
61+
var list = new BulletList();
62+
var item = new ListItem();
63+
var taskMarker = new TaskListItemMarker(false);
64+
var para = new Paragraph();
65+
var text = new Text("I am a task");
66+
para.appendChild(text);
67+
item.appendChild(taskMarker);
68+
item.appendChild(para);
69+
list.appendChild(item);
70+
doc.appendChild(list);
71+
72+
assertRenderedEquals(doc, "- [ ] I am a task\n");
73+
}
74+
75+
private void assertRoundTrip(String input) {
76+
String rendered = RENDERER.render(PARSER.parse(input));
77+
assertThat(rendered).isEqualTo(input);
78+
}
79+
80+
private void assertRenderedEquals(Node inputNode, String expectedOutput) {
81+
var renderedOutput = RENDERER.render(inputNode);
82+
assertThat(renderedOutput).isEqualTo(expectedOutput);
83+
}
84+
}

0 commit comments

Comments
 (0)