From 724619b0efc5937b4fba974769cea7fa6049dffc Mon Sep 17 00:00:00 2001
From: Robin Stocker <robin@nibor.org>
Date: Mon, 22 Jan 2024 22:46:33 +1100
Subject: [PATCH] Use Setext heading if necessary

---
 .../markdown/CoreMarkdownNodeRenderer.java    | 47 +++++++++++++++++++
 .../renderer/markdown/MarkdownRenderer.java   |  2 +-
 .../markdown/MarkdownRendererTest.java        |  3 ++
 .../markdown/SpecMarkdownRendererTest.java    |  2 +-
 4 files changed, 52 insertions(+), 2 deletions(-)

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 a135d73e..dbaa9a47 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java
@@ -13,6 +13,10 @@ import java.util.Set;
 
 /**
  * The node renderer that renders all the core nodes (comes last in the order of node renderers).
+ * <p>
+ * Note that while sometimes it would be easier to record what kind of syntax was used on parsing (e.g. ATX vs Setext
+ * heading), this renderer is intended to also work for documents that were created by directly creating
+ * {@link Node Nodes} instead. So in order to support that, it sometimes needs to do a bit more work.
  */
 public class CoreMarkdownNodeRenderer extends AbstractVisitor implements NodeRenderer {
 
@@ -211,11 +215,34 @@ public class CoreMarkdownNodeRenderer extends AbstractVisitor implements NodeRen
 
     @Override
     public void visit(Heading heading) {
+        if (heading.getLevel() <= 2) {
+            LineBreakVisitor lineBreakVisitor = new LineBreakVisitor();
+            heading.accept(lineBreakVisitor);
+            boolean isMultipleLines = lineBreakVisitor.hasLineBreak();
+
+            if (isMultipleLines) {
+                // Setext headings: Can have multiple lines, but only level 1 or 2
+                visitChildren(heading);
+                writer.line();
+                if (heading.getLevel() == 1) {
+                    // Note that it would be nice to match the length of the contents instead of just using 3, but that's
+                    // not easy.
+                    writer.write("===");
+                } else {
+                    writer.write("---");
+                }
+                writer.block();
+                return;
+            }
+        }
+
+        // ATX headings: Can't have multiple lines, but up to level 6.
         for (int i = 0; i < heading.getLevel(); i++) {
             writer.write('#');
         }
         writer.write(' ');
         visitChildren(heading);
+
         writer.block();
     }
 
@@ -392,4 +419,24 @@ public class CoreMarkdownNodeRenderer extends AbstractVisitor implements NodeRen
             number = orderedList.getStartNumber();
         }
     }
+
+    private static class LineBreakVisitor extends AbstractVisitor {
+        private boolean lineBreak = false;
+
+        public boolean hasLineBreak() {
+            return lineBreak;
+        }
+
+        @Override
+        public void visit(SoftLineBreak softLineBreak) {
+            super.visit(softLineBreak);
+            lineBreak = true;
+        }
+
+        @Override
+        public void visit(HardLineBreak hardLineBreak) {
+            super.visit(hardLineBreak);
+            lineBreak = true;
+        }
+    }
 }
diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java
index 4dc8dbff..4dee17ed 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java
@@ -14,7 +14,7 @@ import java.util.List;
  * <p>
  * Note that it does not currently attempt to preserve the exact syntax of the original input Markdown (if any):
  * <ul>
- *     <li>Headings are always output as ATX headings for simplicity</li>
+ *     <li>Headings are output as ATX headings if possible (multi-line headings need Setext headings)</li>
  *     <li>Escaping might be over-eager, e.g. a plain {@code *} might be escaped
  *     even though it doesn't need to be in that particular context</li>
  * </ul>
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 f6ce1b4e..05a253fd 100644
--- a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java
+++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java
@@ -28,6 +28,9 @@ public class MarkdownRendererTest {
         assertRoundTrip("##### foo\n");
         assertRoundTrip("###### foo\n");
 
+        assertRoundTrip("Foo\nbar\n===\n");
+        assertRoundTrip("[foo\nbar](/url)\n===\n");
+
         assertRoundTrip("# foo\n\nbar\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 4e9396e7..1dd06441 100644
--- a/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java
+++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java
@@ -62,7 +62,7 @@ public class SpecMarkdownRendererTest {
             System.out.println();
         }
 
-        int expectedPassed = 622;
+        int expectedPassed = 625;
         assertTrue("Expected at least " + expectedPassed + " examples to pass but was " + passes.size(), passes.size() >= expectedPassed);
     }
 
-- 
GitLab