Skip to content

Commit 582855d

Browse files
authored
Merge pull request #417 from commonmark/issue-413
TextContentRenderer: Fix nested lists on the same line
2 parents fc23e49 + 67f8405 commit 582855d

File tree

7 files changed

+133
-83
lines changed

7 files changed

+133
-83
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ with the exception that 0.x versions can break between minor versions.
1010
### Fixed
1111
- Line(s) after a hard line break would sometimes also get an unwanted hard
1212
line break, e.g. if they ended in emphasis or other non-text inlines (#415)
13+
- `TextContentRenderer` (for plain text): Fix nested lists on the same line (#413)
1314

1415
## [0.27.0] - 2025-10-12
1516
### Added

commonmark/src/main/java/org/commonmark/internal/renderer/text/BulletListHolder.java

Lines changed: 0 additions & 16 deletions
This file was deleted.

commonmark/src/main/java/org/commonmark/internal/renderer/text/ListHolder.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

commonmark/src/main/java/org/commonmark/internal/renderer/text/OrderedListHolder.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package org.commonmark.renderer.text;
22

3-
import org.commonmark.internal.renderer.text.BulletListHolder;
4-
import org.commonmark.internal.renderer.text.ListHolder;
5-
import org.commonmark.internal.renderer.text.OrderedListHolder;
63
import org.commonmark.node.*;
74
import org.commonmark.renderer.NodeRenderer;
85

@@ -161,19 +158,30 @@ public void visit(Link link) {
161158
@Override
162159
public void visit(ListItem listItem) {
163160
if (listHolder != null && listHolder instanceof OrderedListHolder) {
164-
OrderedListHolder orderedListHolder = (OrderedListHolder) listHolder;
165-
String indent = stripNewlines() ? "" : orderedListHolder.getIndent();
166-
textContent.write(indent + orderedListHolder.getCounter() + orderedListHolder.getDelimiter() + " ");
161+
var orderedListHolder = (OrderedListHolder) listHolder;
162+
var marker = orderedListHolder.getCounter() + orderedListHolder.getDelimiter();
163+
var spaces = " ";
164+
textContent.write(marker);
165+
textContent.write(spaces);
166+
textContent.pushPrefix(repeat(" ", marker.length() + spaces.length()));
167167
visitChildren(listItem);
168168
textContent.block();
169+
textContent.popPrefix();
169170
orderedListHolder.increaseCounter();
170171
} else if (listHolder != null && listHolder instanceof BulletListHolder) {
171172
BulletListHolder bulletListHolder = (BulletListHolder) listHolder;
172173
if (!stripNewlines()) {
173-
textContent.write(bulletListHolder.getIndent() + bulletListHolder.getMarker() + " ");
174+
var marker = bulletListHolder.getMarker();
175+
var spaces = " ";
176+
textContent.write(marker);
177+
textContent.write(spaces);
178+
textContent.pushPrefix(repeat(" ", marker.length() + spaces.length()));
174179
}
175180
visitChildren(listItem);
176181
textContent.block();
182+
if (!stripNewlines()) {
183+
textContent.popPrefix();
184+
}
177185
}
178186
}
179187

@@ -268,4 +276,61 @@ private static String stripTrailingNewline(String s) {
268276
return s;
269277
}
270278
}
279+
280+
// Keep for Android compat (String.repeat only available on Android 12 and later)
281+
private static String repeat(String s, int count) {
282+
var sb = new StringBuilder(s.length() * count);
283+
for (int i = 0; i < count; i++) {
284+
sb.append(s);
285+
}
286+
return sb.toString();
287+
}
288+
289+
private static class BulletListHolder extends ListHolder {
290+
private final String marker;
291+
292+
public BulletListHolder(ListHolder parent, BulletList list) {
293+
super(parent);
294+
marker = list.getMarker();
295+
}
296+
297+
public String getMarker() {
298+
return marker;
299+
}
300+
}
301+
302+
private abstract static class ListHolder {
303+
private final ListHolder parent;
304+
305+
ListHolder(ListHolder parent) {
306+
this.parent = parent;
307+
}
308+
309+
public ListHolder getParent() {
310+
return parent;
311+
}
312+
}
313+
314+
private static class OrderedListHolder extends ListHolder {
315+
private final String delimiter;
316+
private int counter;
317+
318+
public OrderedListHolder(ListHolder parent, OrderedList list) {
319+
super(parent);
320+
delimiter = list.getMarkerDelimiter() != null ? list.getMarkerDelimiter() : ".";
321+
counter = list.getMarkerStartNumber() != null ? list.getMarkerStartNumber() : 1;
322+
}
323+
324+
public String getDelimiter() {
325+
return delimiter;
326+
}
327+
328+
public int getCounter() {
329+
return counter;
330+
}
331+
332+
public void increaseCounter() {
333+
counter++;
334+
}
335+
}
271336
}

commonmark/src/main/java/org/commonmark/renderer/text/TextContentWriter.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class TextContentWriter {
88
private final Appendable buffer;
99
private final LineBreakRendering lineBreakRendering;
1010

11+
private final LinkedList<String> prefixes = new LinkedList<>();
1112
private final LinkedList<Boolean> tight = new LinkedList<>();
1213

1314
private String blockSeparator = null;
@@ -36,6 +37,7 @@ public void colon() {
3637

3738
public void line() {
3839
append('\n');
40+
writePrefixes();
3941
}
4042

4143
public void block() {
@@ -61,6 +63,32 @@ public void write(char c) {
6163
append(c);
6264
}
6365

66+
/**
67+
* Push a prefix onto the top of the stack. All prefixes are written at the beginning of each line, until the
68+
* prefix is popped again.
69+
*
70+
* @param prefix the raw prefix string
71+
*/
72+
public void pushPrefix(String prefix) {
73+
prefixes.addLast(prefix);
74+
}
75+
76+
/**
77+
* Write a prefix.
78+
*
79+
* @param prefix the raw prefix string to write
80+
*/
81+
public void writePrefix(String prefix) {
82+
write(prefix);
83+
}
84+
85+
/**
86+
* Remove the last prefix from the top of the stack.
87+
*/
88+
public void popPrefix() {
89+
prefixes.removeLast();
90+
}
91+
6492
/**
6593
* Change whether blocks are tight or loose. Loose is the default where blocks are separated by a blank line. Tight
6694
* is where blocks are not separated by a blank line. Tight blocks are used in lists, if there are no blank lines
@@ -84,12 +112,26 @@ private boolean isTight() {
84112
return !tight.isEmpty() && tight.getLast();
85113
}
86114

115+
private void writePrefixes() {
116+
for (String prefix : prefixes) {
117+
append(prefix);
118+
}
119+
}
120+
87121
/**
88122
* If a block separator has been enqueued with {@link #block()} but not yet written, write it now.
89123
*/
90124
private void flushBlockSeparator() {
91125
if (blockSeparator != null) {
92-
append(blockSeparator);
126+
if (blockSeparator.equals("\n") || blockSeparator.equals("\n\n")) {
127+
for (int i = 0; i < blockSeparator.length(); i++) {
128+
var sep = blockSeparator.charAt(i);
129+
append(sep);
130+
writePrefixes();
131+
}
132+
} else {
133+
append(blockSeparator);
134+
}
93135
blockSeparator = null;
94136
}
95137
}

commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,14 @@ public void textContentLists() {
116116
assertSeparate(s, "bar\n\n1. foo\n 1. bar\n2. foo");
117117
assertStripped(s, "bar 1. foo 1. bar 2. foo");
118118

119-
s = "bar\n* foo\n - bar\n* foo";
120-
assertCompact(s, "bar\n* foo\n - bar\n* foo");
121-
assertSeparate(s, "bar\n\n* foo\n - bar\n* foo");
119+
s = "bar\n* foo\n - bar\n* foo";
120+
assertCompact(s, "bar\n* foo\n - bar\n* foo");
121+
assertSeparate(s, "bar\n\n* foo\n - bar\n* foo");
122122
assertStripped(s, "bar foo bar foo");
123123

124-
s = "bar\n* foo\n 1. bar\n 2. bar\n* foo";
125-
assertCompact(s, "bar\n* foo\n 1. bar\n 2. bar\n* foo");
126-
assertSeparate(s, "bar\n\n* foo\n 1. bar\n 2. bar\n* foo");
124+
s = "bar\n* foo\n 1. bar\n 2. bar\n* foo";
125+
assertCompact(s, "bar\n* foo\n 1. bar\n 2. bar\n* foo");
126+
assertSeparate(s, "bar\n\n* foo\n 1. bar\n 2. bar\n* foo");
127127
assertStripped(s, "bar foo 1. bar 2. bar foo");
128128

129129
s = "bar\n1. foo\n * bar\n * bar\n2. foo";
@@ -196,6 +196,17 @@ public void textContentHtml() {
196196
assertAll(html, html);
197197
}
198198

199+
@Test
200+
public void testContentNestedLists() {
201+
var s = "List:\n" +
202+
"1. 2) 3. \n" +
203+
"end";
204+
assertCompact(s, s);
205+
206+
var s2 = "1. A\n 1) B\n 1. Test";
207+
assertCompact(s2, s2);
208+
}
209+
199210
@Test
200211
public void testOverrideNodeRendering() {
201212
var nodeRendererFactory = new TextContentNodeRendererFactory() {

0 commit comments

Comments
 (0)