This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new a2aebd19e5 TIKA-5012: Markdown writer keeps table rows intact on <br> 
and stops escaping leading spaces as &#32; (#3456)
a2aebd19e5 is described below

commit a2aebd19e50ea00fffa48826d072549b76acf30f
Author: Tim Allison <[email protected]>
AuthorDate: Fri Oct 30 16:29:26 2026 -0400

    TIKA-5012: Markdown writer keeps table rows intact on <br> and stops 
escaping leading spaces as &#32; (#3456)
---
 CHANGES.txt                                        |  4 ++
 .../apache/tika/sax/ToMarkdownContentHandler.java  | 45 +++++++++++++++++++++-
 .../tika/sax/ToMarkdownContentHandlerTest.java     | 39 +++++++++++++++++++
 3 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 2c9db51240..106325cfad 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -63,6 +63,10 @@ Release 4.3.0 - ???
      DER: Pkcs7Parser and the opt-in pkcs-7-detector report the smime-type and 
extract
      signed content; -----BEGIN CMS----- is now detected (TIKA-5002).
 
+   * Markdown output writes a line break inside a table cell as <br>, so the 
row no
+     longer splits, and no longer prefixes the text after a line break or at 
the start
+     of a paragraph with &#32; (TIKA-5012).
+
    * A PDF with pages that pages.ocr.maxPages kept from the OCR engine records
      skipped:max-pages in tk:ocr-warnings; before, the cap left no trace and
      pdf:ocr-page-count still counted those pages (TIKA-5013).
diff --git 
a/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java 
b/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java
index 42469f6cd2..beb8a42e94 100644
--- a/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java
+++ b/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java
@@ -256,7 +256,8 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
                 break;
             case "br":
                 ensureInlineContainer();
-                stack.peek().appendChild(new HardLineBreak());
+                // a hard break would end a GFM table row
+                stack.peek().appendChild(inTableCell() ? newBrInline() : new 
HardLineBreak());
                 break;
             case "span":
                 if (mathText != null) {
@@ -391,7 +392,7 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         String text = new String(ch, start, length);
         if (text.trim().isEmpty()) {
             // Inter-element whitespace: drop between blocks, collapse within 
inline runs.
-            if (!isBlockContainer(stack.peek())) {
+            if (!isBlockContainer(stack.peek()) && !atLineStart()) {
                 stack.peek().appendChild(new Text(" "));
             }
             return;
@@ -402,6 +403,10 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
             return;
         }
         ensureInlineContainer();
+        if (atLineStart()) {
+            // insignificant in Markdown, and the renderer would escape it as 
&#32;
+            text = stripLeadingWhitespace(text);
+        }
         // Newlines inside prose would otherwise survive as literal line 
breaks.
         stack.peek().appendChild(new Text(collapseLineBreaks(text)));
     }
@@ -661,6 +666,15 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         stack.push(cell);
     }
 
+    private boolean inTableCell() {
+        for (Node n : stack) {
+            if (n instanceof TableCell) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * TeX goes out raw: the renderer's escaping ({@code \\mathcal}, {@code 
u\\_0}) breaks
      * math-aware readers. In a table cell the renderer still escapes {@code 
|}, which GFM
@@ -688,6 +702,33 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         return false;
     }
 
+    private static HtmlInline newBrInline() {
+        HtmlInline br = new HtmlInline();
+        br.setLiteral("<br>");
+        return br;
+    }
+
+    /** True when the next inline text starts a rendered line (or a cell). */
+    private boolean atLineStart() {
+        Node top = stack.peek();
+        Node last = top.getLastChild();
+        if (last == null) {
+            return top instanceof Paragraph || top instanceof Heading
+                    || top instanceof TableCell;
+        }
+        return last instanceof HardLineBreak
+                || (last instanceof HtmlInline && "<br>".equals(((HtmlInline) 
last).getLiteral()));
+    }
+
+    private static String stripLeadingWhitespace(String s) {
+        int i = 0;
+        while (i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'
+                || s.charAt(i) == '\n' || s.charAt(i) == '\r' || s.charAt(i) 
== '\f')) {
+            i++;
+        }
+        return s.substring(i);
+    }
+
     private static BulletList newBulletList() {
         BulletList list = new BulletList();
         list.setMarker("-");
diff --git 
a/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java 
b/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java
index 90456834e4..610cec5eca 100644
--- 
a/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java
+++ 
b/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java
@@ -545,6 +545,20 @@ public class ToMarkdownContentHandlerTest {
         assertContains("u\\_0\\*", handler.toString());
     }
 
+    @Test
+    public void testTextAfterMathSpanKeepsLeadingSpace() throws Exception {
+        ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
+        handler.startDocument();
+        startElement(handler, "p");
+        startElement(handler, "span", "class", "math inline");
+        chars(handler, "$x$");
+        endElement(handler, "span");
+        chars(handler, " is small");
+        endElement(handler, "p");
+        handler.endDocument();
+        assertContains("$x$ is small", handler.toString());
+    }
+
     @Test
     public void testBoldInsideListItem() throws Exception {
         ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
@@ -898,6 +912,31 @@ public class ToMarkdownContentHandlerTest {
         assertTrue(result.contains("|a b|"), result);
     }
 
+    @Test
+    public void testTableCellLineBreak() throws Exception {
+        // the shape a VLM table with multi-line cells arrives in
+        String result = renderNoThrow("br in cells", h -> script(h,
+                "table", "tr", "th", "#Nandina", "br", "/br", "#\n(berrying 
varieties)", "/th",
+                "th", "#Name", "/th", "/tr",
+                "tr", "td", "#Bush Germander,", "br", "/br", "# ", "#Barbados 
Cherry", "/td",
+                "td", "#x", "/td", "/tr", "/table"));
+        assertTrue(result.contains("|Nandina<br>(berrying 
varieties)|Name|\n"), result);
+        assertTrue(result.contains("|Bush Germander,<br>Barbados 
Cherry|x|\n"), result);
+        assertFalse(result.contains("&#32;"), result);
+    }
+
+    @Test
+    public void testNoEscapedSpaceAtLineStart() throws Exception {
+        String result = renderNoThrow("whitespace after br", h -> script(h,
+                "p", "#Com mon", "br", "/br", "#\n Nam", "/p",
+                "p", "#one", "br", "/br", "# ", "#two", "/p",
+                "p", "# ", "# lead", "/p"));
+        assertTrue(result.contains("Com mon  \nNam"), result);
+        assertTrue(result.contains("one  \ntwo"), result);
+        assertTrue(result.contains("\nlead"), result);
+        assertFalse(result.contains("&#32;"), result);
+    }
+
     @Test
     public void testHandlerTypeParsingMarkdown() {
         assertEquals(BasicContentHandlerFactory.HANDLER_TYPE.MARKDOWN,

Reply via email to