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 282ee760df TIKA-5015 (#3464)
282ee760df is described below

commit 282ee760df3bd93e495379f887292f0cc0547898
Author: Tim Allison <[email protected]>
AuthorDate: Fri Oct 30 20:33:48 2026 -0400

    TIKA-5015 (#3464)
    
    * TIKA-5012: Markdown writer keeps table rows intact on <br> and stops 
escaping leading spaces as &#32;
    
    * TIKA-5015: Markdown writer writes tables with merged cells as HTML; 
separate nested-table text in pipe-table cells
    
      Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 CHANGES.txt                                        |   5 +
 .../pages/configuration/inference-recipes.adoc     |   3 +-
 .../ROOT/pages/using-tika/java-api/index.adoc      |   3 +-
 .../org/apache/tika/sax/HtmlTableRecorder.java     | 208 +++++++++++++++++++++
 .../apache/tika/sax/ToMarkdownContentHandler.java  |  85 ++++++++-
 .../tika/sax/ToMarkdownContentHandlerTest.java     |  90 ++++++++-
 .../parser/vlm/MarkdownToXHTMLEmitterTest.java     |  17 ++
 7 files changed, 399 insertions(+), 12 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 1846044090..4b6e4ebaf6 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -88,6 +88,11 @@ Release 4.3.0 - ???
      longer splits, and no longer prefixes the text after a line break or at 
the start
      of a paragraph with &#32; (TIKA-5012).
 
+   * Markdown output writes a table with merged cells (colspan/rowspan) as an 
HTML table
+     instead of a pipe table, which dropped the spans. A nested table is still 
flattened
+     into its cell, now with <br> between its rows and a space between its 
cells; its text
+     was fused with the cell's before (TIKA-5015).
+
    * 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/docs/modules/ROOT/pages/configuration/inference-recipes.adoc 
b/docs/modules/ROOT/pages/configuration/inference-recipes.adoc
index 2acebc7204..bdbc77e4af 100644
--- a/docs/modules/ROOT/pages/configuration/inference-recipes.adoc
+++ b/docs/modules/ROOT/pages/configuration/inference-recipes.adoc
@@ -64,7 +64,8 @@ text is poor. The key comes from the environment
 ----
 
 The model returns markdown with tables as raw HTML, whatever the prompt says; 
Tika parses both
-into XHTML, so a table is a `<table>` in every output format and a pipe table 
in markdown. Body
+into XHTML, so a table is a `<table>` in every output format and a pipe table 
in markdown (an
+HTML table if it has merged cells). Body
 cells come back as `<th>` and are passed through as such. Math comes back as 
LaTeX; Tika keeps it
 verbatim, delimiters included, in `<span class="math inline">` or `<span 
class="math display">`,
 and writes it unescaped in markdown. The load-time health check reads the
diff --git a/docs/modules/ROOT/pages/using-tika/java-api/index.adoc 
b/docs/modules/ROOT/pages/using-tika/java-api/index.adoc
index b0072298e6..e071763376 100644
--- a/docs/modules/ROOT/pages/using-tika/java-api/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/java-api/index.adoc
@@ -156,7 +156,8 @@ The content handler controls the output format:
 |XHTML/XML.
 
 |`ToMarkdownContentHandler`
-|Markdown, preserving headings, lists, tables, code blocks, emphasis and links.
+|Markdown, preserving headings, lists, tables, code blocks, emphasis and 
links. Tables are
+GFM pipe tables, or HTML tables when they have merged cells.
 
 |`ParsingReader`
 |Extracted text as a character stream, filled by a background thread.
diff --git a/tika-core/src/main/java/org/apache/tika/sax/HtmlTableRecorder.java 
b/tika-core/src/main/java/org/apache/tika/sax/HtmlTableRecorder.java
new file mode 100644
index 0000000000..057fc6ec91
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/sax/HtmlTableRecorder.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.sax;
+
+import java.util.Set;
+
+import org.xml.sax.Attributes;
+
+/**
+ * Records a table's SAX events as an HTML literal, for tables a GFM pipe table
+ * cannot express (merged cells).
+ * <p>
+ * The literal becomes a CommonMark HTML block, which ends at the first blank 
line,
+ * so it never contains one: whitespace collapses to a space, and newlines in
+ * {@code <pre>} are written as {@code &#10;}.
+ */
+final class HtmlTableRecorder {
+
+    /** Elements written by name; the rest contribute their text. */
+    private static final Set<String> KEPT = Set.of(
+            "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption",
+            "p", "br", "ul", "ol", "li", "blockquote", "pre", "code",
+            "h1", "h2", "h3", "h4", "h5", "h6", "b", "strong", "i", "em",
+            "s", "strike", "del", "sup", "sub", "a", "img");
+
+    private static final Set<String> VOID = Set.of("br", "img");
+
+    /** Table structure: whitespace next to these tags is formatting, not 
content. */
+    private static final Set<String> STRUCTURE = Set.of(
+            "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption");
+
+    /** Ends after which a newline keeps the literal readable. */
+    private static final Set<String> LINE_ENDS = Set.of(
+            "table", "thead", "tbody", "tfoot", "tr", "caption");
+
+    /** Dropped elements whose end still separates words. */
+    private static final Set<String> SEPARATING = Set.of("div", "dt", "dd", 
"dl", "section");
+
+    private final StringBuilder html = new StringBuilder();
+    private int preDepth;
+    private int cellDepth;
+    private boolean pendingSpace;
+    private boolean spaceSuppressed = true;
+
+    void start(String name, Attributes atts) {
+        if (!KEPT.contains(name)) {
+            return;
+        }
+        boolean structure = STRUCTURE.contains(name);
+        if (structure) {
+            pendingSpace = false;
+        } else {
+            flushSpace();
+        }
+        html.append('<').append(name);
+        switch (name) {
+            case "th":
+            case "td":
+                span(atts, "colspan");
+                span(atts, "rowspan");
+                break;
+            case "a":
+                attr(atts, "href");
+                break;
+            case "img":
+                attr(atts, "src");
+                attr(atts, "alt");
+                break;
+            default:
+                break;
+        }
+        html.append('>');
+        spaceSuppressed = structure;
+        if (name.equals("pre")) {
+            preDepth++;
+        } else if (name.equals("td") || name.equals("th")) {
+            cellDepth++;
+        }
+    }
+
+    void end(String name) {
+        if (SEPARATING.contains(name)) {
+            pendingSpace = true;
+            return;
+        }
+        if (!KEPT.contains(name) || VOID.contains(name)) {
+            return;
+        }
+        if (name.equals("pre")) {
+            preDepth = Math.max(0, preDepth - 1);
+        } else if (name.equals("td") || name.equals("th")) {
+            cellDepth = Math.max(0, cellDepth - 1);
+        }
+        boolean structure = STRUCTURE.contains(name);
+        if (structure) {
+            pendingSpace = false;
+        }
+        html.append("</").append(name).append('>');
+        spaceSuppressed = structure;
+        // inside a cell a newline would be cell text
+        if (cellDepth == 0 && LINE_ENDS.contains(name)) {
+            html.append('\n');
+        }
+    }
+
+    void characters(char[] ch, int start, int length) {
+        for (int i = start; i < start + length; i++) {
+            char c = ch[i];
+            if (preDepth > 0) {
+                if (c == '\n') {
+                    html.append("&#10;");
+                } else if (c != '\r') {
+                    escape(c);
+                }
+                continue;
+            }
+            if (Character.isWhitespace(c)) {
+                pendingSpace = true;
+                continue;
+            }
+            flushSpace();
+            escape(c);
+            spaceSuppressed = false;
+        }
+    }
+
+    String html() {
+        int end = html.length();
+        while (end > 0 && html.charAt(end - 1) == '\n') {
+            end--;
+        }
+        return html.substring(0, end);
+    }
+
+    private void flushSpace() {
+        if (pendingSpace && !spaceSuppressed) {
+            html.append(' ');
+        }
+        pendingSpace = false;
+    }
+
+    private void span(Attributes atts, String name) {
+        String v = atts.getValue(name);
+        if (v != null && v.trim().matches("[1-9][0-9]{0,3}") && 
!v.trim().equals("1")) {
+            html.append(' 
').append(name).append("=\"").append(v.trim()).append('"');
+        }
+    }
+
+    private void attr(Attributes atts, String name) {
+        String v = atts.getValue(name);
+        if (v == null || v.isEmpty()) {
+            return;
+        }
+        html.append(' ').append(name).append("=\"");
+        for (int i = 0; i < v.length(); i++) {
+            char c = v.charAt(i);
+            if (c == '"') {
+                html.append("&quot;");
+            } else if (c == '\n' || c == '\r') {
+                html.append(' ');
+            } else {
+                escape(c);
+            }
+        }
+        html.append('"');
+    }
+
+    private void escape(char c) {
+        switch (c) {
+            case '&':
+                html.append("&amp;");
+                break;
+            case '<':
+                html.append("&lt;");
+                break;
+            case '>':
+                html.append("&gt;");
+                break;
+            default:
+                html.append(c);
+        }
+    }
+
+    /** True when a cell's attributes ask for a merged cell. */
+    static boolean isSpanned(Attributes atts) {
+        for (String name : new String[] {"colspan", "rowspan"}) {
+            String v = atts.getValue(name);
+            if (v != null && v.trim().matches("[1-9][0-9]{0,3}") && 
!v.trim().equals("1")) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
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 010b3de38c..4b3559d0af 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
@@ -45,6 +45,7 @@ import org.commonmark.node.Emphasis;
 import org.commonmark.node.FencedCodeBlock;
 import org.commonmark.node.HardLineBreak;
 import org.commonmark.node.Heading;
+import org.commonmark.node.HtmlBlock;
 import org.commonmark.node.HtmlInline;
 import org.commonmark.node.Image;
 import org.commonmark.node.Link;
@@ -64,7 +65,8 @@ import org.xml.sax.helpers.DefaultHandler;
 /**
  * SAX event handler that writes content as Markdown.
  * Supports headings, paragraphs, bold, italic, strikethrough, links, images,
- * lists (ordered and unordered, including nested), tables (GFM pipe tables),
+ * lists (ordered and unordered, including nested), tables (GFM pipe tables, 
or HTML
+ * when a table has merged cells, which a pipe table cannot express),
  * code blocks, inline code, blockquotes, horizontal rules, and definition
  * lists.
  * <p>
@@ -122,6 +124,11 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
     private TableBody tableBody;
     private int rowIndex;
     private boolean inHeaderRow;
+    // The outermost table as HTML, used instead of the pipe table when it has 
merged cells.
+    private HtmlTableRecorder tableHtml;
+    private boolean tableNeedsHtml;
+    // Text after a nested table's end, in the same outer cell, starts a new 
line.
+    private boolean breakAfterNestedTable;
 
     // <script>/<style> content is dropped.
     private int suppressDepth;
@@ -155,6 +162,7 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         if (suppressDepth > 0) {
             return;
         }
+        recordTableStart(name, atts);
 
         switch (name) {
             // --- block-level ---
@@ -209,12 +217,15 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
                 startTable();
                 break;
             case "tr":
+                separateNested(true);
                 startRow();
                 break;
             case "th":
+                separateNested(false);
                 startCell(true);
                 break;
             case "td":
+                separateNested(false);
                 startCell(false);
                 break;
             case "dt":
@@ -308,6 +319,9 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         if (suppressDepth > 0) {
             return;
         }
+        if (tableHtml != null) {
+            tableHtml.end(name);
+        }
 
         switch (name) {
             case "h1":
@@ -369,6 +383,7 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
                 }
                 break;
             case "table":
+                breakAfterNestedTable = tableDepth > 1;
                 endTable();
                 break;
             case "tr":
@@ -381,6 +396,7 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
             case "td":
                 if (tableDepth == 1) {
                     pop();
+                    breakAfterNestedTable = false;
                 }
                 break;
             default:
@@ -393,6 +409,9 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         if (suppressDepth > 0) {
             return;
         }
+        if (tableHtml != null) {
+            tableHtml.characters(ch, start, length);
+        }
         if (codeBlockText != null) {
             codeBlockText.append(ch, start, length);
             return;
@@ -419,6 +438,10 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
             // Stray text inside a list/table wrapper — not a valid inline 
position; drop.
             return;
         }
+        if (breakAfterNestedTable) {
+            breakAfterNestedTable = false;
+            separateInCell(true);
+        }
         ensureInlineContainer();
         if (atLineStart()) {
             // insignificant in Markdown, and the renderer would escape it as 
&#32;
@@ -644,14 +667,74 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         }
     }
 
+    private void recordTableStart(String name, Attributes atts) {
+        if (name.equals("table")) {
+            if (tableDepth == 0) {
+                tableHtml = new HtmlTableRecorder();
+                tableNeedsHtml = false;
+            }
+        } else if (tableHtml != null && (name.equals("td") || 
name.equals("th"))
+                && HtmlTableRecorder.isSpanned(atts)) {
+            tableNeedsHtml = true;
+        }
+        if (tableHtml != null) {
+            tableHtml.start(name, atts);
+        }
+    }
+
     private void endTable() {
         if (tableDepth == 1) {
+            if (tableNeedsHtml && tableHtml != null && tableBlock != null) {
+                replaceWithHtml(tableBlock, tableHtml.html());
+            }
+            tableHtml = null;
             tableBlock = null;
             tableBody = null;
         }
         tableDepth = Math.max(0, tableDepth - 1);
     }
 
+    /**
+     * A nested table is flattened into its outer cell; keep its rows and 
cells apart
+     * there ({@code <br>} between rows, a space between cells).
+     */
+    private void separateNested(boolean lineBreak) {
+        if (tableDepth >= 2) {
+            separateInCell(lineBreak);
+        }
+    }
+
+    private void separateInCell(boolean lineBreak) {
+        if (!inTableCell()) {
+            return;
+        }
+        Node top = stack.peek();
+        if (top.getLastChild() == null || atLineStart()) {
+            return;
+        }
+        top.appendChild(lineBreak ? newBrInline() : new Text(" "));
+    }
+
+    /** Swaps the pipe table for an HTML block, closing anything still open 
inside it. */
+    private void replaceWithHtml(TableBlock table, String html) {
+        while (stack.size() > 1 && isWithin(stack.peek(), table)) {
+            pop();
+        }
+        HtmlBlock block = new HtmlBlock();
+        block.setLiteral(html);
+        table.insertBefore(block);
+        table.unlink();
+    }
+
+    private static boolean isWithin(Node n, Node ancestor) {
+        for (Node p = n; p != null; p = p.getParent()) {
+            if (p == ancestor) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void startRow() {
         if (tableDepth != 1) {
             return;
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 717456e7fb..2486254754 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
@@ -107,6 +107,15 @@ public class ToMarkdownContentHandlerTest {
                 atts.addAttribute("", "src", "src", "CDATA", "i.png");
                 atts.addAttribute("", "alt", "alt", "CDATA", "alt");
                 startElement(h, "img", atts);
+            } else if (op.contains(" ")) {
+                // "td colspan=2 rowspan=3"
+                String[] parts = op.split(" ");
+                AttributesImpl atts = new AttributesImpl();
+                for (int i = 1; i < parts.length; i++) {
+                    String[] kv = parts[i].split("=", 2);
+                    atts.addAttribute("", kv[0], kv[0], "CDATA", kv[1]);
+                }
+                startElement(h, parts[0], atts);
             } else {
                 startElement(h, op);
             }
@@ -969,6 +978,73 @@ public class ToMarkdownContentHandlerTest {
         assertFalse(result.contains("&#32;"), result);
     }
 
+    @Test
+    public void testMergedCellsWriteHtmlTable() throws Exception {
+        String result = renderNoThrow("colspan", h -> script(h,
+                "p", "#before", "/p",
+                "table", "thead", "tr", "th colspan=2", "#Region", "/th", 
"/tr", "/thead",
+                "tbody", "tr", "td rowspan=2", "#North", "/td", "td", "#1 < 2 
& *x*", "/td", "/tr",
+                "tr", "td colspan=1", "#Q", "b", "#2", "/b", "/td", "/tr", 
"/tbody", "/table",
+                "p", "#after", "/p"));
+        assertEquals("before\n\n"
+                + "<table><thead><tr><th 
colspan=\"2\">Region</th></tr>\n</thead>\n"
+                + "<tbody><tr><td rowspan=\"2\">North</td><td>1 &lt; 2 &amp; 
*x*</td></tr>\n"
+                + "<tr><td>Q<b>2</b></td></tr>\n</tbody>\n</table>\n\n"
+                + "after\n", result);
+    }
+
+    @Test
+    public void testNestedTableRowsAndCellsSeparated() throws Exception {
+        String result = renderNoThrow("nested", h -> script(h,
+                "table", "tr", "th", "#outer", "/th", "/tr",
+                "tr", "td", "table", "tr", "td", "#a", "/td", "td", "#b", 
"/td", "/tr",
+                "tr", "td", "#c", "/td", "/tr", "/table", "#tail", "/td", 
"/tr",
+                "/table", "p", "#after", "/p"));
+        assertFalse(result.contains("<table"), result);
+        assertContains("|a b<br>c<br>tail|", result);
+    }
+
+    @Test
+    public void testNestedTableInMergedCellTableKeptInHtml() throws Exception {
+        String result = renderNoThrow("nested in spans", h -> script(h,
+                "table", "tr", "td colspan=2", "table", "tr", "td", "#inner", 
"/td", "/tr",
+                "/table", "/td", "/tr", "/table"));
+        assertContains("<td 
colspan=\"2\"><table><tr><td>inner</td></tr></table></td>", result);
+    }
+
+    @Test
+    public void testHtmlTableHasNoBlankLine() throws Exception {
+        // a CommonMark HTML block ends at the first blank line
+        String result = renderNoThrow("blank lines", h -> script(h,
+                "table", "tr", "td colspan=2", "#a\n\n\nb", "/td", "/tr",
+                "tr", "td", "pre", "#x\n\ny", "/pre", "/td", "td", "p", 
"#one", "/p",
+                "p", "#two", "/p", "/td", "/tr", "/table"));
+        String table = result.substring(0, result.indexOf("</table>"));
+        assertFalse(table.contains("\n\n"), result);
+        assertContains("<td colspan=\"2\">a b</td>", result);
+        assertContains("<pre>x&#10;&#10;y</pre>", result);
+        assertContains("<td><p>one</p><p>two</p></td>", result);
+    }
+
+    @Test
+    public void testPlainTableStaysPipeTable() throws Exception {
+        String result = renderNoThrow("plain", h -> script(h,
+                "table", "tr", "th colspan=1", "#a", "/th", "th", "#b", "/th", 
"/tr",
+                "tr", "td", "p", "#one", "/p", "p", "#two", "/p", "/td", "td", 
"#2", "/td", "/tr",
+                "/table"));
+        assertFalse(result.contains("<table"), result);
+        assertContains("|a|b|", result);
+    }
+
+    @Test
+    public void testHtmlTableInListItem() throws Exception {
+        String result = renderNoThrow("in list", h -> script(h,
+                "ul", "li", "#item", "table", "tr", "td colspan=2", "#x", 
"/td", "/tr", "/table",
+                "/li", "/ul", "p", "#after", "/p"));
+        assertContains("<td colspan=\"2\">x</td>", result);
+        assertContains("after", result);
+    }
+
     @Test
     public void testNoEscapedSpaceAtLineStart() throws Exception {
         String result = renderNoThrow("whitespace after br", h -> script(h,
@@ -1041,7 +1117,7 @@ public class ToMarkdownContentHandlerTest {
     }
 
     @Test
-    public void testNestedTablesIgnored() throws Exception {
+    public void testNestedTableSeparatedInCell() throws Exception {
         ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
         handler.startDocument();
 
@@ -1065,7 +1141,6 @@ public class ToMarkdownContentHandlerTest {
         startElement(handler, "td");
         chars(handler, "B");
 
-        // Nested table -- should be ignored
         startElement(handler, "table");
         startElement(handler, "tr");
         startElement(handler, "td");
@@ -1082,13 +1157,10 @@ public class ToMarkdownContentHandlerTest {
         handler.endDocument();
 
         String result = handler.toString();
-        // Outer table should be rendered
-        assertTrue(result.contains("|Outer1|Outer2|"));
-        assertTrue(result.contains("|---|---|"));
-        // Inner cell text gets folded into the outer cell ("B" + "Inner" = 
"BInner")
-        assertTrue(result.contains("|A|BInner|"));
-        // Inner table structure should not appear as a separate table
-        assertFalse(result.contains("|Inner|"));
+        assertTrue(result.contains("|Outer1|Outer2|"), result);
+        // the nested table's text is kept apart from the cell's ("BInner" 
before)
+        assertTrue(result.contains("|A|B<br>Inner|"), result);
+        assertFalse(result.contains("<table"), result);
     }
 
     private static final String[] ALL_ELEMENTS = {
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/java/org/apache/tika/parser/vlm/MarkdownToXHTMLEmitterTest.java
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/java/org/apache/tika/parser/vlm/MarkdownToXHTMLEmitterTest.java
index 06dd59897b..44ed387fdb 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/java/org/apache/tika/parser/vlm/MarkdownToXHTMLEmitterTest.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/java/org/apache/tika/parser/vlm/MarkdownToXHTMLEmitterTest.java
@@ -416,6 +416,23 @@ public class MarkdownToXHTMLEmitterTest {
         assertContains("|$\\\\|x\\\\|$|2|", written);
     }
 
+    @Test
+    void testMergedCellTableRoundTripsThroughMarkdownWriter() throws Exception 
{
+        String md = "<table><thead><tr><th 
colspan=\"2\">Region</th></tr></thead>\n"
+                + "<tbody><tr><td rowspan=\"2\">North</td><td>1 &lt; 2, 
$\\|x\\|$</td></tr>\n"
+                + 
"<tr><td><b>Q</b>2<table><tr><td>inner</td></tr></table></td></tr>"
+                + "</tbody></table>\n\nafter\n";
+        String first = emit(md);
+        assertContains("<th colspan=\"2\">", first);
+        ToMarkdownContentHandler out = new ToMarkdownContentHandler();
+        out.startDocument();
+        MarkdownToXHTMLEmitter.emit(md, out);
+        out.endDocument();
+        String written = out.toString();
+        assertContains("<th colspan=\"2\">Region</th>", written);
+        assertEquals(first, emit(written), written);
+    }
+
     private static final String XHTML_NS = "http://www.w3.org/1999/xhtml";;
 
     /**

Reply via email to