diff --git a/docs/changelog.md b/docs/changelog.md index 6bf91f4eb..7926ab4aa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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 diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index c537a6bcd..1a230db38 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -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 @@ -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 diff --git a/tests/test_syntax/extensions/test_tables.py b/tests/test_syntax/extensions/test_tables.py index 4e33d1aa3..f07118e1a 100644 --- a/tests/test_syntax/extensions/test_tables.py +++ b/tests/test_syntax/extensions/test_tables.py @@ -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, + '
| A | +B | +
|---|---|
| a | +b | +
Not a table + | A | B | + |---|---| + | a | b |
+ """ + ), + extensions=['tables'] + ) + + def test_single_column_table_after_header_in_same_block(self): + self.assertMarkdownRenders( + self.dedent( + """ + # Table + | A | + |---| + | a | + """ + ), + self.dedent( + """ +| A | +
|---|
| a | +
| A | + |---| + | a | + not a row
+ """ + ), + extensions=['tables'] + ) + def test_align_columns_legacy(self): self.assertMarkdownRenders( self.dedent(