diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java index 28461c870e51b6d1f63794e33a53592345aa86bf..5e88333314c97386e864c10c40faef6c121967e2 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java @@ -92,6 +92,49 @@ public class CoreMarkdownNodeRenderer extends AbstractVisitor implements NodeRen visitChildren(bulletList); listHolder = listHolder.parent; writer.setTight(oldTight); + writer.block(); + } + + @Override + public void visit(OrderedList orderedList) { + boolean oldTight = writer.getTight(); + writer.setTight(orderedList.isTight()); + listHolder = new OrderedListHolder(listHolder, orderedList); + visitChildren(orderedList); + listHolder = listHolder.parent; + writer.setTight(oldTight); + writer.block(); + } + + @Override + public void visit(ListItem listItem) { + int contentIndent = listItem.getContentIndent(); + boolean pushedPrefix = false; + if (listHolder instanceof BulletListHolder) { + BulletListHolder bulletListHolder = (BulletListHolder) listHolder; + String marker = repeat(" ", listItem.getMarkerIndent()) + bulletListHolder.bulletMarker; + writer.write(marker); + writer.write(repeat(" ", contentIndent - marker.length())); + writer.pushPrefix(repeat(" ", contentIndent)); + pushedPrefix = true; + } else if (listHolder instanceof OrderedListHolder) { + OrderedListHolder orderedListHolder = (OrderedListHolder) listHolder; + String marker = repeat(" ", listItem.getMarkerIndent()) + orderedListHolder.number + orderedListHolder.delimiter; + orderedListHolder.number++; + writer.write(marker); + writer.write(repeat(" ", contentIndent - marker.length())); + writer.pushPrefix(repeat(" ", contentIndent)); + pushedPrefix = true; + } + if (listItem.getFirstChild() == null) { + // Empty list item + writer.block(); + } else { + visitChildren(listItem); + } + if (pushedPrefix) { + writer.popPrefix(); + } } @Override @@ -221,55 +264,10 @@ public class CoreMarkdownNodeRenderer extends AbstractVisitor implements NodeRen writeLinkLike(link.getTitle(), link.getDestination(), link, "["); } - @Override - public void visit(ListItem listItem) { - int contentIndent = listItem.getContentIndent(); - boolean pushedPrefix = false; - if (listHolder instanceof BulletListHolder) { - BulletListHolder bulletListHolder = (BulletListHolder) listHolder; - String marker = repeat(" ", listItem.getMarkerIndent()) + bulletListHolder.bulletMarker; - writer.write(marker); - writer.write(repeat(" ", contentIndent - marker.length())); - writer.pushPrefix(repeat(" ", contentIndent)); - pushedPrefix = true; - } else if (listHolder instanceof OrderedListHolder) { - OrderedListHolder orderedListHolder = (OrderedListHolder) listHolder; - String marker = repeat(" ", listItem.getMarkerIndent()) + orderedListHolder.number + orderedListHolder.delimiter; - orderedListHolder.number++; - writer.write(marker); - writer.write(repeat(" ", contentIndent - marker.length())); - writer.pushPrefix(repeat(" ", contentIndent)); - pushedPrefix = true; - } - if (listItem.getFirstChild() == null) { - // Empty list item - writer.block(); - } else { - visitChildren(listItem); - } - if (pushedPrefix) { - writer.popPrefix(); - } - } - - @Override - public void visit(OrderedList orderedList) { - boolean oldTight = writer.getTight(); - writer.setTight(orderedList.isTight()); - listHolder = new OrderedListHolder(listHolder, orderedList); - visitChildren(orderedList); - listHolder = listHolder.parent; - writer.setTight(oldTight); - } - @Override public void visit(Paragraph paragraph) { visitChildren(paragraph); - if (paragraph.getParent() instanceof ListItem) { - writer.block(); - } else { - writer.block(); - } + writer.block(); } @Override diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java index c2c826195e390fa6dd54979d190863079e4d1e7b..ba682d465cbd14ce97fb4c46c38cc737faa8f9b1 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java +++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java @@ -9,7 +9,7 @@ public class MarkdownWriter { private final Appendable buffer; - private boolean finishBlock = false; + private int blockSeparator = 0; private boolean tight; private char lastChar; private final LinkedList<String> prefixes = new LinkedList<>(); @@ -22,22 +22,13 @@ public class MarkdownWriter { return lastChar; } - public void block() { - finishBlock = true; - } - - public void line() { - append('\n'); - writePrefixes(); - } - public void write(String s) { - finishBlockIfNeeded(); + flushBlockSeparator(); append(s); } public void write(char c) { - finishBlockIfNeeded(); + flushBlockSeparator(); append(c); } @@ -45,7 +36,7 @@ public class MarkdownWriter { if (s.isEmpty()) { return; } - finishBlockIfNeeded(); + flushBlockSeparator(); try { for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); @@ -61,6 +52,21 @@ public class MarkdownWriter { lastChar = s.charAt(s.length() - 1); } + public void line() { + append('\n'); + writePrefixes(); + } + + /** + * Enqueue a block separator to be written before the next text is written. Block separators are not written + * straight away because if there are no more blocks to write we don't want a separator (at the end of the document). + */ + public void block() { + // Remember whether this should be a tight or loose separator now because tight could get changed in between + // this and the next flush. + blockSeparator = tight ? 1 : 2; + } + public void pushPrefix(String prefix) { prefixes.addLast(prefix); } @@ -92,18 +98,6 @@ public class MarkdownWriter { lastChar = c; } - private void finishBlockIfNeeded() { - if (finishBlock) { - finishBlock = false; - append('\n'); - writePrefixes(); - if (!tight) { - append('\n'); - writePrefixes(); - } - } - } - private void writePrefixes() { if (!prefixes.isEmpty()) { for (String prefix : prefixes) { @@ -112,10 +106,36 @@ public class MarkdownWriter { } } + /** + * If a block separator has been enqueued with {@link #block()} but not yet written, write it now. + */ + private void flushBlockSeparator() { + if (blockSeparator != 0) { + append('\n'); + writePrefixes(); + if (blockSeparator > 1) { + append('\n'); + writePrefixes(); + } + blockSeparator = 0; + } + } + + /** + * @return whether blocks are currently set to tight or loose, see {@link #setTight(boolean)} + */ public boolean getTight() { return tight; } + /** + * Change whether blocks are tight or loose. Loose is the default where blocks are separated by a blank line. Tight + * is where blocks are not separated by a blank line. Tight blocks are used in lists, if there are no blank lines + * within the list. + * <p> + * Note that changing this does not affect block separators that have already been enqueued (with {@link #block()}, + * only future ones. + */ public void setTight(boolean tight) { this.tight = tight; } diff --git a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java index 06ad79c25a211b6dd36ef1eaead180ffc4bcf4fe..f6ce1b4e05f0eb38ab2c66b1d6fa37f425c5370c 100644 --- a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java +++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java @@ -82,6 +82,8 @@ public class MarkdownRendererTest { // Tight list assertRoundTrip("* foo\n* bar\n"); + // Tight list where the second item contains a loose list + assertRoundTrip("- Foo\n - Bar\n \n - Baz\n"); // List item indent. This is a tricky one, but here the amount of space between the list marker and "one" // determines whether "two" is part of the list item or an indented code block. @@ -102,6 +104,8 @@ public class MarkdownRendererTest { // Tight list assertRoundTrip("1. foo\n2. bar\n"); + // Tight list where the second item contains a loose list + assertRoundTrip("1. Foo\n 1. Bar\n \n 2. Baz\n"); assertRoundTrip(" 1. one\n\n two\n"); } diff --git a/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java b/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java index 682fee9d853e84a9a57a3e6dae82f31c17ae1ab3..e21ec8cc756a5b45ccd3e992b2d3b564f60f490e 100644 --- a/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java +++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java @@ -52,7 +52,7 @@ public class SpecMarkdownRendererTest { System.out.println("Failed examples by section (total " + fails.size() + "):"); printCountsBySection(fails); - int expectedPassed = 618; + int expectedPassed = 621; assertTrue("Expected at least " + expectedPassed + " examples to pass but was " + passes.size(), passes.size() >= expectedPassed); }