From a012c39afdddb22196b16af22d55d0811bc5b499 Mon Sep 17 00:00:00 2001 From: deadstrobe5 Date: Tue, 10 Nov 2026 15:19:50 +0000 Subject: [PATCH 1/3] Fix quadratic processing time in the tables extension for long blocks --- docs/changelog.md | 3 +++ markdown/extensions/tables.py | 11 +++++++---- tests/test_syntax/extensions/test_tables.py | 12 ++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 6bf91f4eb..4d31c2588 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. ## [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..ffdae5efc 100644 --- a/tests/test_syntax/extensions/test_tables.py +++ b/tests/test_syntax/extensions/test_tables.py @@ -843,6 +843,18 @@ 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, + '

Header

\n' * (count - 1) + '

Header

', + extensions=['tables'] + ) + def test_align_columns_legacy(self): self.assertMarkdownRenders( self.dedent( From 585ccdaed91deb49a9c4b978afe65b67d85a5027 Mon Sep 17 00:00:00 2001 From: deadstrobe5 Date: Wed, 11 Nov 2026 18:36:03 +0000 Subject: [PATCH 2/3] Test tables that follow other lines in the same block --- tests/test_syntax/extensions/test_tables.py | 104 ++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/test_syntax/extensions/test_tables.py b/tests/test_syntax/extensions/test_tables.py index ffdae5efc..f07118e1a 100644 --- a/tests/test_syntax/extensions/test_tables.py +++ b/tests/test_syntax/extensions/test_tables.py @@ -855,6 +855,110 @@ def test_many_lines_in_one_block(self): extensions=['tables'] ) + def test_table_after_header_in_same_block(self): + self.assertMarkdownRenders( + self.dedent( + """ + # Table + | A | B | + |---|---| + | a | b | + """ + ), + self.dedent( + """ +

Table

+ + + + + + + + + + + + + +
AB
ab
+ """ + ), + 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( + """ +

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( + """ +

Table

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

| A | + |---| + | a | + not a row

+ """ + ), + extensions=['tables'] + ) + def test_align_columns_legacy(self): self.assertMarkdownRenders( self.dedent( From d6a389e1b30b583cb747cf740684ff2268014737 Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Thu, 12 Nov 2026 15:02:05 -0500 Subject: [PATCH 3/3] Add issue ref to changelog entry --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 4d31c2588..7926ab4aa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -38,7 +38,7 @@ See the [Contributing Guide](contributing.md) for details. `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. + between them (#1704). ## [3.11.3] - 2026-11-05