Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ See the [Contributing Guide](contributing.md) for details.
* Stop definition list nesting short of the recursion limit, so deeply nested
definitions are kept as text instead of raising an uncaught
`RecursionError` (#1700).
* Fix quadratic processing time in the `tables` extension for long blocks which
are split into many smaller blocks, such as many headers with no blank lines
between them (#1704).

## [3.11.3] - 2026-11-05

Expand Down
11 changes: 7 additions & 4 deletions markdown/extensions/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def test(self, parent: etree.Element, block: str) -> bool:
Keep border check and separator row do avoid repeating the work.
"""
is_table = False
rows = [row.strip(' ') for row in block.split('\n')]
# Only the first two rows are needed in most cases. Splitting the whole
# block here would make every test linear in the size of the block.
rows = [row.strip(' ') for row in block.split('\n', 2)[:2]]
if len(rows) > 1:
header0 = rows[0]
self.border = PIPE_NONE
Expand All @@ -69,10 +71,11 @@ def test(self, parent: etree.Element, block: str) -> bool:

# Each row in a single column table needs at least one pipe.
if not is_table and row0_len == 1 and self.border:
for index in range(1, len(rows)):
is_table = rows[index].startswith('|')
for row in block.split('\n')[1:]:
row = row.strip(' ')
is_table = row.startswith('|')
if not is_table:
is_table = self.RE_END_BORDER.search(rows[index]) is not None
is_table = self.RE_END_BORDER.search(row) is not None
if not is_table:
break

Expand Down
116 changes: 116 additions & 0 deletions tests/test_syntax/extensions/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,122 @@ def test_single_column_tables(self):
extensions=['tables']
)

def test_many_lines_in_one_block(self):
"""
Testing a block for a table should only look at the first rows, as the
remainder of a long block can be tested once per line.
"""
count = 5000
self.assertMarkdownRenders(
'# Header\n' * count,
'<h1>Header</h1>\n' * (count - 1) + '<h1>Header</h1>',
extensions=['tables']
)

def test_table_after_header_in_same_block(self):
self.assertMarkdownRenders(
self.dedent(
"""
# Table
| A | B |
|---|---|
| a | b |
"""
),
self.dedent(
"""
<h1>Table</h1>
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
</tr>
</thead>
<tbody>
<tr>
<td>a</td>
<td>b</td>
</tr>
</tbody>
</table>
"""
),
extensions=['tables']
)

def test_no_table_after_paragraph_line_in_same_block(self):
self.assertMarkdownRenders(
self.dedent(
"""
Not a table
| A | B |
|---|---|
| a | b |
"""
),
self.dedent(
"""
<p>Not a table
| A | B |
|---|---|
| a | b |</p>
"""
),
extensions=['tables']
)

def test_single_column_table_after_header_in_same_block(self):
self.assertMarkdownRenders(
self.dedent(
"""
# Table
| A |
|---|
| a |
"""
),
self.dedent(
"""
<h1>Table</h1>
<table>
<thead>
<tr>
<th>A</th>
</tr>
</thead>
<tbody>
<tr>
<td>a</td>
</tr>
</tbody>
</table>
"""
),
extensions=['tables']
)

def test_no_single_column_table_with_row_without_pipe(self):
self.assertMarkdownRenders(
self.dedent(
"""
| A |
|---|
| a |
not a row
"""
),
self.dedent(
"""
<p>| A |
|---|
| a |
not a row</p>
"""
),
extensions=['tables']
)

def test_align_columns_legacy(self):
self.assertMarkdownRenders(
self.dedent(
Expand Down
Loading