Repository: raphlinus/pulldown-cmark (rust)

Issue:
GFM tables without leading empty lines inside list items are interpreted incorrectly as plain text
Hello.

As title says, for example, if I run `cargo run - --enable-tables` with the following as input:
``` markdown
| A | table    |
| ------ | ---- |
| not   |  in  |
| a     | list|

* A blockquote:
  > inside a list item
* A Heading:
  # inside a list item
* A table:

  | inside | a       |
  | ------ | ------- |
  | list   | item    |
  | with   | leading |
  | empty  | lines   |
* A table:
  | inside  | a       |
  | ------- | ------- |
  | list    | item    |
  | without | leading |
  | empty   | lines   |
```

I'd expect the output to be [^1]:
> | A | table    |
> | ------ | ---- |
> | not   |  in  |
> | a     | list|
> 
> * A blockquote:
>   > inside a list item
> * A Heading:
>   # inside a list item
> * A table:
> 
>   | inside | a       |
>   | ------ | ------- |
>   | list   | item    |
>   | with   | leading |
>   | empty  | lines   |
> * A table:
>   | inside  | a       |
>   | ------- | ------- |
>   | list    | item    |
>   | without | leading |
>   | empty   | lines   |

But the actual output is:
> <!--begin--><table><thead><tr><th>A</th><th>table</th></tr></thead><tbody>
<tr><td>not</td><td>in</td></tr>
<tr><td>a</td><td>list</td></tr>
</tbody></table>
<ul>
<li>
<p>A blockquote:</p>
<blockquote>
<p>inside a list item</p>
</blockquote>
</li>
<li>
<p>A Heading:</p>
<h1>inside a list item</h1>
</li>
<li>
<p>A table:</p>
<table><thead><tr><th>inside</th><th>a</th></tr></thead><tbody>
<tr><td>list</td><td>item</td></tr>
<tr><td>with</td><td>leading</td></tr>
<tr><td>empty</td><td>lines</td></tr>
</tbody></table>
</li>
<li>
<p>A table:
| inside  | a       |
| ------- | ------- |
| list    | item    |
| without | leading |
| empty   | lines   |</p>
</li>
</ul><!--end--></blockquote>

<details><summary>Raw HTML of the actual output</summary>

``` html
<table><thead><tr><th>A</th><th>table</th></tr></thead><tbody>
<tr><td>not</td><td>in</td></tr>
<tr><td>a</td><td>list</td></tr>
</tbody></table>
<ul>
<li>
<p>A blockquote:</p>
<blockquote>
<p>inside a list item</p>
</blockquote>
</li>
<li>
<p>A Heading:</p>
<h1>inside a list item</h1>
</li>
<li>
<p>A table:</p>
<table><thead><tr><th>inside</th><th>a</th></tr></thead><tbody>
<tr><td>list</td><td>item</td></tr>
<tr><td>with</td><td>leading</td></tr>
<tr><td>empty</td><td>lines</td></tr>
</tbody></table>
</li>
<li>
<p>A table:
| inside  | a       |
| ------- | ------- |
| list    | item    |
| without | leading |
| empty   | lines   |</p>
</li>
</ul>
```
</details>

[^1]: I pasted the exact markdown text above to here and appended `> ` to the beginning of each line, so this is just how GitHub's markdown renderer renders my input.

---

As the above shows:
* Table extension is enabled (hence the first table above is rendered).
* Generally, block elements inside list items are rendered exactly as what they would be in other places, no matter if there are leading empty lines. The [CommonMark dingus](https://spec.commonmark.org/dingus/?text=*%20A%20blockquote%3A%0A%20%20%3E%20inside%20a%20list%20item%0A*%20A%20Heading%3A%0A%20%20%23%20inside%20a%20list%20item) also works this way (hence the blockquote and h1 above is rendered).
* However, unlike other block elements, `pulldown-cmark` interprets tables inside list items without leading empty lines as paragraphs, not tables.

Commit I built the binary from: 5088b21d09ef94b424c4d852db7648c9c94fb630 (The current head commit)

Pull request description:
Fix, and document, some table/paragraph interrupt corner cases
Fixes #540

<details><summary>interrupting paragraphs with tables</summary>

Pulldown-cmark gives "heavy" tables (with a leading `|`) higher priority than "light" tables for paragraph interruption. This is compatible with [older] versions of GitHub, but 2023-06-21 GitHub treats them the same.

[older]: https://github.com/github/cmark-gfm/issues/180

A quick run through <https://babelmark.github.io> shows, of the implementations that support pipe tables at all[^1]:

| Implementation     -> | a | b | c | d | e | f | g | h | i | j |
| --------------------- | - | - | - | - | - | - | - | - | - | - |
| **pulldown-cmark**    | ✓ | x | x | x | x | ✓ | x | x | ✓ | x |
| github/cmark-gfm      | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | ✓ | ✓ | x |
| pycmarkgfm            | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | ✓ | ✓ | x |
| DFM[^2]               | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | - | x |
| league/commonmark GFM | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | x | ✓ | x |
| s9e/TextFormatter     | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | x | x | x |
| markdown-it[^3][^4]   | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | x | - | - |
| php-markdown-extra    | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | x | x | x |
| markdig               | x | x | x | x | x | x | x | x | x | x |
| MD4C                  | x | x | x | x | x | x | x | x | x | x |
| parsedown             | x | x | x | x | x | x | x | x | x | x |
| maruku[^3]            | x | x | x | x | x | x | x | x | x | - |
| cebe                  | x | x | x | x | x | x | x | x | x | x |
| pandoc                | x | x | x | x | x | x | x | x | x | x |
| multimarkdown         | x | x | x | x | x | x | x | x | x | x |

* A lot of Extended Markdown parsers don't allow tables to interrupt paragraphs at all, even if they support pipe tables in general.

* Out of the ones that do, about half of them do what GitHub does and disallow it when the table is indented by four or more spaces.

[^1]:
    Implementations that "support pipe tables at all" were identified by
    including "table a" without a preceding paragraph and seeing if it
    produced an HTML table for *that*.

[^2]:
    On "table h" and "table i", DFM sees a table with no cells, followed
    by a paragraph with `b` in it.

[^3]:
    On "table j", maruku and markdown-it see a table with no cells, followed
    by a paragraph with `b` in it.

[^4]:
    On "table i", markdown-it sees a table with no cells, followed
    by a paragraph with `b` in it.

</details>

<details><summary>ambiguity between "light" tables and h2</summary>

This PR also changes `scan_table_head` to refuse any table with zero pipes at all in its header line. This resolves an ambiguity with settext-style `<h2>`, and matches every other Markdown implementation I could fine in Babelmark (see *interrupting paragraphs with tables*, "table j"), except `maruku` and `markdown-it`, which behave very strangely.

</details>

<details><summary>ambiguity with "light" tables and bulleted lists with <code>-</code></summary>

This PR adds a test case for the existing behavior on tables like this:

```markdown
a | b
- | -
1 | 2
```

It doesn't change the behavior, but it does document it.

| Implementation     -> | parsed as |
| --------------------- | --------- |
| **pulldown-cmark**    | table     |
| github/cmark-gfm      | list      |
| pycmarkgfm            | list      |
| markdown-it           | table     |
| php-markdown-extra    | table     |
| DFM                   | table     |
| league/commonmark GFM | list      |
| s9e/TextFormatter     | table     |
| markdig               | table     |
| MD4C                  | list      |
| parsedown             | table     |
| maruku                | table     |
| cebe                  | table     |
| pandoc                | table     |
| multimarkdown         | table     |

</details>

<details><summary>hard line breaks are not allowed at the end of table header lines</summary>

Hard line breaks are not allowed at the end of table header lines. It's about a 50:50 split on babelmark, but this change makes pulldown-cmark consistent with GFM and Pandoc.

| Implementation     -> | parsed as |
| --------------------- | --------- |
| **pulldown-cmark**    | paragraph |
| github/cmark-gfm      | paragraph |
| pycmarkgfm            | paragraph |
| markdown-it           | table     |
| php-markdown-extra    | table     |
| DFM                   | table     |
| league/commonmark GFM | paragraph |
| s9e/TextFormatter     | table     |
| markdig               | paragraph |
| MD4C                  | paragraph |
| parsedown             | table     |
| maruku                | table     |
| cebe                  | paragraph |
| pandoc                | paragraph |
| multimarkdown         | table     |

</details>

Patch:
diff --git a/specs/regression.txt b/specs/regression.txt
index b3f3642..e006d1a 100644
--- a/specs/regression.txt
+++ b/specs/regression.txt
@@ -1063,3 +1063,64 @@ ISSUE 659
 .
 <!doctype html>
 ````````````````````````````````
+
+
+ISSUE 540
+
+```````````````````````````````` example
+| A | table    |
+| ------ | ---- |
+| not   |  in  |
+| a     | list|
+
+* A blockquote:
+  > inside a list item
+* A Heading:
+  # inside a list item
+* A table:
+
+  | inside | a       |
+  | ------ | ------- |
+  | list   | item    |
+  | with   | leading |
+  | empty  | lines   |
+* A table:
+  | inside  | a       |
+  | ------- | ------- |
+  | list    | item    |
+  | without | leading |
+  | empty   | lines   |
+.
+<table><thead><tr><th>A</th><th>table</th></tr></thead><tbody>
+<tr><td>not</td><td>in</td></tr>
+<tr><td>a</td><td>list</td></tr>
+</tbody></table>
+<ul>
+<li>
+<p>A blockquote:</p>
+<blockquote>
+<p>inside a list item</p>
+</blockquote>
+</li>
+<li>
+<p>A Heading:</p>
+<h1>inside a list item</h1>
+</li>
+<li>
+<p>A table:</p>
+<table><thead><tr><th>inside</th><th>a</th></tr></thead><tbody>
+<tr><td>list</td><td>item</td></tr>
+<tr><td>with</td><td>leading</td></tr>
+<tr><td>empty</td><td>lines</td></tr>
+</tbody></table>
+</li>
+<li>
+<p>A table:</p>
+<table><thead><tr><th>inside</th><th>a</th></tr></thead><tbody>
+<tr><td>list</td><td>item</td></tr>
+<tr><td>without</td><td>leading</td></tr>
+<tr><td>empty</td><td>lines</td></tr>
+</tbody></table>
+</li>
+</ul>
+````````````````````````````````
diff --git a/specs/table.txt b/specs/table.txt
index 5e4d4b3..e68137d 100644
--- a/specs/table.txt
+++ b/specs/table.txt
@@ -215,3 +215,221 @@ Test russian symbols.
 <tr><td>Ячейка 1 </td><td>Ячейка 2 </td></tr>
 </table>
 ````````````````````````````````
+
+
+Test cases based on <https://github.com/github/cmark-gfm/issues/180>
+and <https://github.com/raphlinus/pulldown-cmark/issues/540>.
+
+Pulldown-cmark gives "heavy" tables (with a leading `|`) higher priority than
+"light" tables for paragraph interruption. This is compatible with *older*
+versions of GitHub, but 2023-06-21 GitHub treats them the same.
+
+A quick run through <https://babelmark.github.io> shows, of the
+implementations that support pipe tables at all[^1]:
+
+| Implementation     -> | a | b | c | d | e | f | g | h | i | j |
+| --------------------- | - | - | - | - | - | - | - | - | - | - |
+| **pulldown-cmark**    | ✓ | x | x | x | x | ✓ | x | x | ✓ | x |
+| github/cmark-gfm      | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | ✓ | ✓ | x |
+| pycmarkgfm            | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | ✓ | ✓ | x |
+| DFM[^2]               | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | - | x |
+| league/commonmark GFM | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | x | ✓ | x |
+| s9e/TextFormatter     | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | x | x | x |
+| markdown-it[^3][^4]   | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | x | - | - |
+| php-markdown-extra    | ✓ | x | ✓ | x | ✓ | ✓ | ✓ | x | x | x |
+| markdig               | x | x | x | x | x | x | x | x | x | x |
+| MD4C                  | x | x | x | x | x | x | x | x | x | x |
+| parsedown             | x | x | x | x | x | x | x | x | x | x |
+| maruku[^3]            | x | x | x | x | x | x | x | x | x | - |
+| cebe                  | x | x | x | x | x | x | x | x | x | x |
+| pandoc                | x | x | x | x | x | x | x | x | x | x |
+| multimarkdown         | x | x | x | x | x | x | x | x | x | x |
+
+While pulldown-cmark could probably afford to align itself with GFM, tables
+interrupting paragraphs is not a portable feature, and anyone trying to write
+widely-compatible markdown syntax can't rely on it.
+
+[^1]:
+    Implementations that "support pipe tables at all" were identified by
+    including "table a" without a preceding paragraph and seeing if it
+    produced an HTML table for *that*.
+
+[^2]:
+    On "table h" and "table i", DFM sees a table with no cells, followed
+    by a paragraph with `b` in it.
+
+[^3]:
+    On "table j", maruku and markdown-it see a table with no cells, followed
+    by a paragraph with `b` in it.
+
+[^4]:
+    On "table i", markdown-it sees a table with no cells, followed
+    by a paragraph with `b` in it.
+
+```````````````````````````````` example
+table a
+|  a  |  b  |
+| --- | --- |
+|  c  |  d  |
+
+
+table b
+    |  a  |  b  |
+    | --- | --- |
+    |  c  |  d  |
+
+
+table c
+ a  |  b
+--- | ---
+ c  |  d
+
+
+table d
+    a | b
+    --|--
+    c | d
+
+
+table e
+a | b
+--|--
+c | d
+
+table f
+  |  a  |  b  |
+  | --- | --- |
+  |  c  |  d  |
+
+
+table g
+   a  |  b
+  --- | ---
+   c  |  d
+
+table h
+a
+|-|
+b
+
+table i
+| a
+|-
+b
+
+table j
+| a
+-
+b
+.
+<p>table a</p>
+<table><thead><tr><th>a</th><th>b</th></tr></thead>
+<tr><td>c</td><td>d</td></tr>
+</table>
+<p>table b
+|  a  |  b  |
+| --- | --- |
+|  c  |  d  |</p>
+<p>table c
+a  |  b
+--- | ---
+c  |  d</p>
+<p>table d
+a | b
+--|--
+c | d</p>
+<p>table e
+a | b
+--|--
+c | d</p>
+<p>table f</p>
+<table><thead><tr><th>a</th><th>b</th></tr></thead>
+<tr><td>c</td><td>d</td></tr>
+</table>
+<p>table g
+a  |  b
+--- | ---
+c  |  d</p>
+<p>table h
+a
+|-|
+b</p>
+<p>table i</p>
+<table><thead><tr><th>a</th></tr></thead><tbody>
+<tr><td>b</td></tr>
+</tbody></table>
+<p>table j</p>
+<h2>| a</h2>
+<p>b</p>
+````````````````````````````````
+
+
+Test case based on <https://github.com/github/cmark-gfm/issues/333>.
+Normally, we'd follow GFM's lead, but this one seems like a bug,
+and Pandoc, the other big, authoritative Extended Markdown parser,
+treats it as a table.
+
+Like the example above showing interruption, this ambiguity is tested:
+
+| Implementation     -> | parsed as |
+| --------------------- | --------- |
+| **pulldown-cmark**    | table     |
+| github/cmark-gfm      | list      |
+| pycmarkgfm            | list      |
+| markdown-it           | table     |
+| php-markdown-extra    | table     |
+| DFM                   | table     |
+| league/commonmark GFM | list      |
+| s9e/TextFormatter     | table     |
+| markdig               | table     |
+| MD4C                  | list      |
+| parsedown             | table     |
+| maruku                | table     |
+| cebe                  | table     |
+| pandoc                | table     |
+| multimarkdown         | table     |
+
+```````````````````````````````` example
+a | b
+- | -
+1 | 2
+.
+<table><thead><tr><th>a</th><th>b</th></tr></thead><tbody>
+<tr><td>1</td><td>2</td></tr>
+</tbody></table>
+````````````````````````````````
+
+
+Hard line breaks are not allowed at the end of table header lines.
+It's about a 50:50 split on babelmark, but this change makes pulldown-cmark
+consistent with GFM and Pandoc.
+
+| Implementation     -> | parsed as |
+| --------------------- | --------- |
+| **pulldown-cmark**    | paragraph |
+| github/cmark-gfm      | paragraph |
+| pycmarkgfm            | paragraph |
+| markdown-it           | table     |
+| php-markdown-extra    | table     |
+| DFM                   | table     |
+| league/commonmark GFM | paragraph |
+| s9e/TextFormatter     | table     |
+| markdig               | paragraph |
+| MD4C                  | paragraph |
+| parsedown             | table     |
+| maruku                | table     |
+| cebe                  | paragraph |
+| pandoc                | paragraph |
+| multimarkdown         | table     |
+
+
+```````````````````````````````` example
+a | b\
+- | -
+1 | 2
+.
+<p>a | b</p>
+<ul>
+<li>| - 1 | 2</li>
+</ul>
+````````````````````````````````
diff --git a/src/firstpass.rs b/src/firstpass.rs
index 96c73f1..2b6e9d3 100644
--- a/src/firstpass.rs
+++ b/src/firstpass.rs
@@ -493,6 +493,24 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         return LoopInstruction::BreakAtWith(ix, None);
                     }
 
+                    let mut i = ix;
+                    let eol_bytes = scan_eol(&bytes[ix..]).unwrap();
+
+                    let end_ix = ix + eol_bytes;
+                    let trailing_backslashes = scan_rev_while(&bytes[..ix], |b| b == b'\\');
+                    if trailing_backslashes % 2 == 1 && end_ix < bytes_len {
+                        i -= 1;
+                        self.tree.append_text(begin_text, i);
+                        return LoopInstruction::BreakAtWith(
+                            end_ix,
+                            Some(Item {
+                                start: i,
+                                end: end_ix,
+                                body: ItemBody::HardBreak,
+                            }),
+                        );
+                    }
+
                     // If tables are not enabled yet and the next char is a pipe,
                     // and there was content before then a table begins here but
                     // there was content before the table:
@@ -502,15 +520,13 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                     // ```
                     if TableParseMode::Scan == mode
                         && bytes.len() > (ix + 1)
-                        && bytes[ix + 1] == b'|'
                         && begin_text < ix
                         && pipes == 0
+                        && line_starts_with_pipe(&bytes[ix + 1..])
                     {
                         return LoopInstruction::BreakAtWith(ix, None);
                     }
 
-                    let mut i = ix;
-                    let eol_bytes = scan_eol(&bytes[ix..]).unwrap();
                     if mode == TableParseMode::Scan && pipes > 0 {
                         // check if we may be parsing a table
                         let next_line_ix = ix + eol_bytes;
@@ -542,20 +558,6 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         }
                     }
 
-                    let end_ix = ix + eol_bytes;
-                    let trailing_backslashes = scan_rev_while(&bytes[..ix], |b| b == b'\\');
-                    if trailing_backslashes % 2 == 1 && end_ix < bytes_len {
-                        i -= 1;
-                        self.tree.append_text(begin_text, i);
-                        return LoopInstruction::BreakAtWith(
-                            end_ix,
-                            Some(Item {
-                                start: i,
-                                end: end_ix,
-                                body: ItemBody::HardBreak,
-                            }),
-                        );
-                    }
                     let trailing_whitespace =
                         scan_rev_while(&bytes[..ix], is_ascii_whitespace_no_nl);
                     if trailing_whitespace >= 2 {
diff --git a/src/scanners.rs b/src/scanners.rs
index 5a04bfb..41f4ab1 100644
--- a/src/scanners.rs
+++ b/src/scanners.rs
@@ -548,6 +548,14 @@ pub(crate) fn scan_setext_heading(data: &[u8]) -> Option<(usize, HeadingLevel)>
     Some((i, level))
 }
 
+pub(crate) fn line_starts_with_pipe(data: &[u8]) -> bool {
+    let (i, spaces) = calc_indent(data, 4);
+    if spaces > 3 || i == data.len() {
+        return false;
+    }
+    data[i] == b'|'
+}
+
 // returns number of bytes in line (including trailing
 // newline) and column alignments
 pub(crate) fn scan_table_head(data: &[u8]) -> (usize, Vec<Alignment>) {
@@ -558,8 +566,10 @@ pub(crate) fn scan_table_head(data: &[u8]) -> (usize, Vec<Alignment>) {
     let mut cols = vec![];
     let mut active_col = Alignment::None;
     let mut start_col = true;
+    let mut found_pipe = false;
     if data[i] == b'|' {
         i += 1;
+        found_pipe = true;
     }
     for c in &data[i..] {
         if let Some(n) = scan_eol(&data[i..]) {
@@ -582,6 +592,7 @@ pub(crate) fn scan_table_head(data: &[u8]) -> (usize, Vec<Alignment>) {
             }
             b'|' => {
                 start_col = true;
+                found_pipe = true;
                 cols.push(active_col);
                 active_col = Alignment::None;
             }
@@ -597,6 +608,11 @@ pub(crate) fn scan_table_head(data: &[u8]) -> (usize, Vec<Alignment>) {
     if !start_col {
         cols.push(active_col);
     }
+    if !found_pipe {
+        // It isn't a table head if it doesn't have a least one pipe.
+        // It's a list, a header, or a thematic break.
+        return (0, vec![]);
+    }
 
     (i, cols)
 }


Test patch:
diff --git a/tests/suite/regression.rs b/tests/suite/regression.rs
index 8411550..3bbda8a 100644
--- a/tests/suite/regression.rs
+++ b/tests/suite/regression.rs
@@ -1251,3 +1251,65 @@ fn regression_test_84() {
 
     test_markdown_html(original, expected, false, false, false);
 }
+
+#[test]
+fn regression_test_85() {
+    let original = r##"| A | table    |
+| ------ | ---- |
+| not   |  in  |
+| a     | list|
+
+* A blockquote:
+  > inside a list item
+* A Heading:
+  # inside a list item
+* A table:
+
+  | inside | a       |
+  | ------ | ------- |
+  | list   | item    |
+  | with   | leading |
+  | empty  | lines   |
+* A table:
+  | inside  | a       |
+  | ------- | ------- |
+  | list    | item    |
+  | without | leading |
+  | empty   | lines   |
+"##;
+    let expected = r##"<table><thead><tr><th>A</th><th>table</th></tr></thead><tbody>
+<tr><td>not</td><td>in</td></tr>
+<tr><td>a</td><td>list</td></tr>
+</tbody></table>
+<ul>
+<li>
+<p>A blockquote:</p>
+<blockquote>
+<p>inside a list item</p>
+</blockquote>
+</li>
+<li>
+<p>A Heading:</p>
+<h1>inside a list item</h1>
+</li>
+<li>
+<p>A table:</p>
+<table><thead><tr><th>inside</th><th>a</th></tr></thead><tbody>
+<tr><td>list</td><td>item</td></tr>
+<tr><td>with</td><td>leading</td></tr>
+<tr><td>empty</td><td>lines</td></tr>
+</tbody></table>
+</li>
+<li>
+<p>A table:</p>
+<table><thead><tr><th>inside</th><th>a</th></tr></thead><tbody>
+<tr><td>list</td><td>item</td></tr>
+<tr><td>without</td><td>leading</td></tr>
+<tr><td>empty</td><td>lines</td></tr>
+</tbody></table>
+</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
diff --git a/tests/suite/table.rs b/tests/suite/table.rs
index 5339414..1aa58e5 100644
--- a/tests/suite/table.rs
+++ b/tests/suite/table.rs
@@ -203,3 +203,133 @@ fn table_test_12() {
 
     test_markdown_html(original, expected, false, false, false);
 }
+
+#[test]
+fn table_test_13() {
+    let original = r##"table a
+|  a  |  b  |
+| --- | --- |
+|  c  |  d  |
+
+
+table b
+    |  a  |  b  |
+    | --- | --- |
+    |  c  |  d  |
+
+
+table c
+ a  |  b
+--- | ---
+ c  |  d
+
+
+table d
+    a | b
+    --|--
+    c | d
+
+
+table e
+a | b
+--|--
+c | d
+
+table f
+  |  a  |  b  |
+  | --- | --- |
+  |  c  |  d  |
+
+
+table g
+   a  |  b
+  --- | ---
+   c  |  d
+
+table h
+a
+|-|
+b
+
+table i
+| a
+|-
+b
+
+table j
+| a
+-
+b
+"##;
+    let expected = r##"<p>table a</p>
+<table><thead><tr><th>a</th><th>b</th></tr></thead>
+<tr><td>c</td><td>d</td></tr>
+</table>
+<p>table b
+|  a  |  b  |
+| --- | --- |
+|  c  |  d  |</p>
+<p>table c
+a  |  b
+--- | ---
+c  |  d</p>
+<p>table d
+a | b
+--|--
+c | d</p>
+<p>table e
+a | b
+--|--
+c | d</p>
+<p>table f</p>
+<table><thead><tr><th>a</th><th>b</th></tr></thead>
+<tr><td>c</td><td>d</td></tr>
+</table>
+<p>table g
+a  |  b
+--- | ---
+c  |  d</p>
+<p>table h
+a
+|-|
+b</p>
+<p>table i</p>
+<table><thead><tr><th>a</th></tr></thead><tbody>
+<tr><td>b</td></tr>
+</tbody></table>
+<p>table j</p>
+<h2>| a</h2>
+<p>b</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn table_test_14() {
+    let original = r##"a | b
+- | -
+1 | 2
+"##;
+    let expected = r##"<table><thead><tr><th>a</th><th>b</th></tr></thead><tbody>
+<tr><td>1</td><td>2</td></tr>
+</tbody></table>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn table_test_15() {
+    let original = r##"a | b\
+- | -
+1 | 2
+"##;
+    let expected = r##"<p>a | b</p>
+<ul>
+<li>| - 1 | 2</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}