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   (#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   (#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   (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
 
+ 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(" "), 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(" "), result);
+ }
+
@Test
public void testHandlerTypeParsingMarkdown() {
assertEquals(BasicContentHandlerFactory.HANDLER_TYPE.MARKDOWN,