Skip to content

Commit 2753bf2

Browse files
committed
TextContentRenderer: Fix nested lists on the same line
1 parent 353278c commit 2753bf2

File tree

5 files changed

+85
-25
lines changed

5 files changed

+85
-25
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
Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,13 @@
11
package org.commonmark.internal.renderer.text;
22

33
public abstract class ListHolder {
4-
private static final String INDENT_DEFAULT = " ";
5-
private static final String INDENT_EMPTY = "";
6-
74
private final ListHolder parent;
8-
private final String indent;
95

106
ListHolder(ListHolder parent) {
117
this.parent = parent;
12-
13-
if (parent != null) {
14-
indent = parent.indent + INDENT_DEFAULT;
15-
} else {
16-
indent = INDENT_EMPTY;
17-
}
188
}
199

2010
public ListHolder getParent() {
2111
return parent;
2212
}
23-
24-
public String getIndent() {
25-
return indent;
26-
}
2713
}

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,19 +161,30 @@ public void visit(Link link) {
161161
@Override
162162
public void visit(ListItem listItem) {
163163
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() + " ");
164+
var orderedListHolder = (OrderedListHolder) listHolder;
165+
var marker = orderedListHolder.getCounter() + orderedListHolder.getDelimiter();
166+
var spaces = " ";
167+
textContent.write(marker);
168+
textContent.write(spaces);
169+
textContent.pushPrefix(repeat(" ", marker.length() + spaces.length()));
167170
visitChildren(listItem);
168171
textContent.block();
172+
textContent.popPrefix();
169173
orderedListHolder.increaseCounter();
170174
} else if (listHolder != null && listHolder instanceof BulletListHolder) {
171175
BulletListHolder bulletListHolder = (BulletListHolder) listHolder;
172176
if (!stripNewlines()) {
173-
textContent.write(bulletListHolder.getIndent() + bulletListHolder.getMarker() + " ");
177+
var marker = bulletListHolder.getMarker();
178+
var spaces = " ";
179+
textContent.write(marker);
180+
textContent.write(spaces);
181+
textContent.pushPrefix(repeat(" ", marker.length() + spaces.length()));
174182
}
175183
visitChildren(listItem);
176184
textContent.block();
185+
if (!stripNewlines()) {
186+
textContent.popPrefix();
187+
}
177188
}
178189
}
179190

@@ -268,4 +279,13 @@ private static String stripTrailingNewline(String s) {
268279
return s;
269280
}
270281
}
282+
283+
// Keep for Android compat (String.repeat only available on Android 12 and later)
284+
private static String repeat(String s, int count) {
285+
var sb = new StringBuilder(s.length() * count);
286+
for (int i = 0; i < count; i++) {
287+
sb.append(s);
288+
}
289+
return sb.toString();
290+
}
271291
}

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)