File tree Expand file tree Collapse file tree
tests/test_syntax/extensions Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -22,6 +22,8 @@ See the [Contributing Guide](contributing.md) for details.
2222 with the closer, dropping text.
2323* Fix the unused part of a partially consumed closing delimiter run being classified using the replacement
2424 placeholder instead of the text that preceded it.
25+ * Fix quadratic processing time in the ` fenced_code ` extension for documents
26+ with many fenced code blocks (#1660 ).
2527
2628## [ 3.11.3] - 2026-11-05
2729
Original file line number Diff line number Diff line change @@ -102,6 +102,9 @@ def run(self, lines: list[str]) -> list[str]:
102102 self .checked_for_deps = True
103103
104104 text = "\n " .join (lines )
105+ # Collect the output in pieces, as rebuilding the full text after each block is quadratic.
106+ pieces : list [str ] = []
107+ last = 0
105108 index = 0
106109 while 1 :
107110 m = self .FENCED_BLOCK_RE .search (text , index )
@@ -164,12 +167,13 @@ def run(self, lines: list[str]) -> list[str]:
164167 code = f'<pre{ id_attr } { class_attr } ><code{ lang_attr } { kv_pairs } >{ code } </code></pre>'
165168
166169 placeholder = self .md .htmlStash .store (code )
167- text = f'{ text [:m .start ()]} \n { placeholder } \n { text [ m . end ():] } '
170+ pieces . append ( f'{ text [last :m .start ()]} \n { placeholder } \n ' )
168171 # Continue from after the replaced text in the next iteration.
169- index = m . start () + 1 + len ( placeholder )
172+ index = last = m . end ( )
170173 else :
171174 break
172- return text .split ("\n " )
175+ pieces .append (text [last :])
176+ return "" .join (pieces ).split ("\n " )
173177
174178 def handle_attrs (
175179 self , attrs : Iterable [tuple [str , str | None ]]
Original file line number Diff line number Diff line change @@ -455,6 +455,77 @@ def testFencedCodeMismatchedCurlyInAttrs(self):
455455 extensions = ['fenced_code' , 'attr_list' ]
456456 )
457457
458+ def testMultipleFencedBlocks (self ):
459+ self .assertMarkdownRenders (
460+ self .dedent (
461+ '''
462+ Text before.
463+
464+ ```python
465+ first = 1
466+ ```
467+
468+ Text between.
469+
470+ ~~~
471+ second
472+ ~~~
473+
474+ Text after.
475+ '''
476+ ),
477+ self .dedent (
478+ '''
479+ <p>Text before.</p>
480+ <pre><code class="language-python">first = 1
481+ </code></pre>
482+ <p>Text between.</p>
483+ <pre><code>second
484+ </code></pre>
485+ <p>Text after.</p>
486+ '''
487+ ),
488+ extensions = ['fenced_code' ]
489+ )
490+
491+ def testAdjacentFencedBlocks (self ):
492+ # The second block opens on the line right after the first one closes.
493+ self .assertMarkdownRenders (
494+ self .dedent (
495+ '''
496+ ```
497+ first
498+ ```
499+ ~~~
500+ second
501+ ~~~
502+ '''
503+ ),
504+ self .dedent (
505+ '''
506+ <pre><code>first
507+ </code></pre>
508+ <pre><code>second
509+ </code></pre>
510+ '''
511+ ),
512+ extensions = ['fenced_code' ]
513+ )
514+
515+ def testFencedBlockAtEndWithoutNewline (self ):
516+ self .assertMarkdownRenders (
517+ 'Text.\n \n ```\n code\n ```' ,
518+ '<p>Text.</p>\n <pre><code>code\n </code></pre>' ,
519+ extensions = ['fenced_code' ]
520+ )
521+
522+ def testManyFencedBlocks (self ):
523+ self .assertMarkdownRenders (
524+ '\n \n ' .join (f'Paragraph { i } .\n \n ```\n block { i } \n ```' for i in range (50 )),
525+ '\n ' .join (f'<p>Paragraph { i } .</p>\n <pre><code>block { i } \n </code></pre>' for i in range (50 )),
526+ extensions = ['fenced_code' ]
527+ )
528+
458529
459530class TestFencedCodeWithCodehilite (TestCase ):
460531
You can’t perform that action at this time.
0 commit comments