Skip to content

Commit 968c6c7

Browse files
authored
Block extensions: ensure md_in_html has the correct target element (#2577)
* Block extensions: ensure `md_in_html` has the correct target element Resolves #2572 * Update version
1 parent 868f7e9 commit 968c6c7

File tree

4 files changed

+161
-101
lines changed

4 files changed

+161
-101
lines changed

docs/src/markdown/about/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 10.14.2
4+
5+
- **FIX**: Blocks: Fix some corner cases with `md_in_html`.
6+
37
## 10.14.1
48

59
- **FIX**: MagicLink: Ensure that repo names that start with `.` are handled correctly.

pymdownx/__meta__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,5 @@ def parse_version(ver, pre=False):
185185
return Version(major, minor, micro, release, pre, post, dev)
186186

187187

188-
__version_info__ = Version(10, 14, 1, "final")
188+
__version_info__ = Version(10, 14, 2, "final")
189189
__version__ = __version_info__._get_canonical()

pymdownx/blocks/__init__.py

+108-96
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,22 @@ def __init__(self, parser, md):
192192
self.end = RE_END
193193
self.yaml_line = RE_INDENT_YAML_LINE
194194

195+
def detab_by_length(self, text, length):
196+
"""Remove a tab from the front of each line of the given text."""
197+
198+
newtext = []
199+
lines = text.split('\n')
200+
for line in lines:
201+
if line.startswith(' ' * length):
202+
newtext.append(line[length:])
203+
elif not line.strip():
204+
newtext.append('') # pragma: no cover
205+
else:
206+
break
207+
if newtext:
208+
return '\n'.join(newtext), '\n'.join(lines[len(newtext):])
209+
return '\n'.join(lines[len(newtext):]), ''
210+
195211
def register(self, b, config):
196212
"""Register a block."""
197213

@@ -251,46 +267,37 @@ def _reset(self):
251267
self.working = None
252268
self.trackers = {d: {} for d in self.blocks.keys()}
253269

254-
def split_end(self, blocks, length):
270+
def split_end(self, block, length):
255271
"""Search for end and split the blocks while removing the end."""
256272

257-
good = []
258-
bad = []
273+
good = None
274+
bad = None
259275
end = False
260276

261-
# Split on our end notation for the current Block
262-
for e, block in enumerate(blocks):
263-
264-
# Find the end of the Block
265-
m = None
266-
for match in self.end.finditer(block):
267-
if len(match.group(1)) == length:
268-
m = match
269-
break
270-
271-
# Separate everything from before the "end" and after
272-
if m:
273-
temp = block[:m.start(0)]
274-
if temp:
275-
good.append(temp[:-1] if temp.endswith('\n') else temp)
276-
end = True
277-
278-
# Since we found our end, everything after is unwanted
279-
temp = block[m.end(0):]
280-
if temp:
281-
bad.append(temp)
282-
bad.extend(blocks[e + 1:])
277+
# Find the end of the Block
278+
m = None
279+
for match in self.end.finditer(block):
280+
if len(match.group(1)) == length:
281+
m = match
283282
break
284-
else:
285-
# Gather blocks until we find our end
286-
good.append(block)
287283

288-
# Augment the blocks
289-
blocks.clear()
290-
blocks.extend(bad)
284+
# Separate everything from before the "end" and after
285+
if m:
286+
temp = block[:m.start(0)]
287+
if temp:
288+
good = temp[:-1] if temp.endswith('\n') else temp
289+
end = True
290+
291+
# Since we found our end, everything after is unwanted
292+
temp = block[m.end(0):]
293+
if temp:
294+
bad = temp
295+
else:
296+
# Gather blocks until we find our end
297+
good = block
291298

292299
# Send back the new list of blocks to parse and note whether we found our end
293-
return good, end
300+
return good, bad, end
294301

295302
def split_header(self, block, length):
296303
"""Split, YAML-ish header out."""
@@ -349,52 +356,83 @@ def is_block(self, tag):
349356

350357
return tag.tag in self.block_tags
351358

352-
def parse_blocks(self, blocks, entry):
359+
def parse_blocks(self, blocks):
353360
"""Parse the blocks."""
354361

355362
# Get the target element and parse
363+
while blocks and self.stack:
364+
b = blocks.pop(0)
356365

357-
for b in blocks:
366+
# Get the latest block on the stack
367+
# This is required to avoid some issues with `md_in_html`
368+
entry = self.stack[-1]
358369
target = entry.block.on_add(entry.el)
359370

360-
# The Block does not or no longer accepts more content
361-
if target is None: # pragma: no cover
362-
break
371+
# Since we are juggling the block parsers on the stack, the pipeline
372+
# has not fully adjusted list indentation, so look at how many
373+
# list item parents we have on the stack and adjust the content
374+
# accordingly.
375+
li = [e.parent.tag in ('li', 'dd') for e in self.stack[:-1]]
376+
length = len(li) * self.tab_length
377+
b, a = self.detab_by_length(b, length)
378+
if a:
379+
blocks.insert(0, a)
363380

364-
mode = entry.block.on_markdown()
365-
if mode not in ('block', 'inline', 'raw'):
366-
mode = 'auto'
367-
is_block = mode == 'block' or (mode == 'auto' and self.is_block(target))
368-
is_atomic = mode == 'raw' or (mode == 'auto' and self.is_raw(target))
369-
370-
# We should revert fenced code in spans or atomic tags.
371-
# Make sure atomic tags have content wrapped as `AtomicString`.
372-
if is_atomic or not is_block:
373-
child = list(target)[-1] if len(target) else None
374-
text = target.text if child is None else child.tail
375-
b = '\n\n'.join(unescape_markdown(self.md, [b], is_atomic)).strip('\n')
376-
377-
if text:
378-
text += b if not b else '\n\n' + b
381+
# Split out blocks we care about
382+
b, bad, end = self.split_end(b, entry.block.length)
383+
if bad is not None:
384+
blocks.insert(0, bad)
385+
386+
# Parse the block under the given target
387+
if b is not None and target is not None:
388+
# Resolve modes
389+
mode = entry.block.on_markdown()
390+
if mode not in ('block', 'inline', 'raw'):
391+
mode = 'auto'
392+
is_block = mode == 'block' or (mode == 'auto' and self.is_block(target))
393+
is_atomic = mode == 'raw' or (mode == 'auto' and self.is_raw(target))
394+
395+
# We should revert fenced code in spans or atomic tags.
396+
# Make sure atomic tags have content wrapped as `AtomicString`.
397+
if is_atomic or not is_block:
398+
child = list(target)[-1] if len(target) else None
399+
text = target.text if child is None else child.tail
400+
b = '\n\n'.join(unescape_markdown(self.md, [b], is_atomic)).strip('\n')
401+
402+
if text:
403+
text += b if not b else '\n\n' + b
404+
else:
405+
text = b
406+
407+
if child is None:
408+
target.text = mutil.AtomicString(text) if is_atomic else text
409+
else: # pragma: no cover
410+
# TODO: We would need to build a special plugin to test this,
411+
# as none of the default ones do this, but we have verified this
412+
# locally. Once we've written a test, we can remove this.
413+
child.tail = mutil.AtomicString(text) if is_atomic else text
414+
415+
# Block tags should have content go through the normal block processor
379416
else:
380-
text = b
417+
self.parser.state.set('blocks')
418+
working = self.working
419+
self.working = entry
420+
self.parser.parseChunk(target, b)
421+
self.parser.state.reset()
422+
self.working = working
423+
424+
# Run "on end" event when we finish a block
425+
if end:
426+
entry.block._end(entry.el)
427+
self.inline_stack.append(entry)
428+
del self.stack[-1]
381429

382-
if child is None:
383-
target.text = mutil.AtomicString(text) if is_atomic else text
384-
else: # pragma: no cover
385-
# TODO: We would need to build a special plugin to test this,
386-
# as none of the default ones do this, but we have verified this
387-
# locally. Once we've written a test, we can remove this.
388-
child.tail = mutil.AtomicString(text) if is_atomic else text
430+
# The Block does not or no longer accepts more content
431+
if target is None: # pragma: no cover
432+
break
389433

390-
# Block tags should have content go through the normal block processor
391-
else:
392-
self.parser.state.set('blocks')
393-
working = self.working
394-
self.working = entry
395-
self.parser.parseChunk(target, b)
396-
self.parser.state.reset()
397-
self.working = working
434+
if self.stack:
435+
self.stack[-1].hungry = True
398436

399437
def run(self, parent, blocks):
400438
"""Convert to details/summary block."""
@@ -429,42 +467,16 @@ def run(self, parent, blocks):
429467
# Push a Block entry on the stack.
430468
self.stack.append(BlockEntry(generic_block, el, parent))
431469

432-
# Split out blocks we care about
433-
ours, end = self.split_end(blocks, generic_block.length)
434-
435470
# Parse the text blocks under the Block
436-
index = len(self.stack) - 1
437-
self.parse_blocks(ours, self.stack[-1])
438-
439-
# Remove Block from the stack if we are at the end
440-
# or add it to the hungry list.
441-
if end:
442-
# Run the "on end" event
443-
generic_block._end(el)
444-
self.inline_stack.append(self.stack[index])
445-
del self.stack[index]
446-
else:
447-
self.stack[index].hungry = True
471+
self.parse_blocks(blocks)
448472

449473
else:
450474
for r in range(len(self.stack)):
451475
entry = self.stack[r]
452476
if entry.hungry and parent is entry.parent:
453-
# Find and remove end from the blocks
454-
ours, end = self.split_end(blocks, entry.block.length)
455-
456477
# Get the target element and parse
457478
entry.hungry = False
458-
self.parse_blocks(ours, entry)
459-
460-
# Clean up if we completed the Block
461-
if end:
462-
# Run "on end" event
463-
entry.block._end(entry.el)
464-
self.inline_stack.append(entry)
465-
del self.stack[r]
466-
else:
467-
entry.hungry = True
479+
self.parse_blocks(blocks)
468480

469481
break
470482

tests/test_extensions/test_blocks/test_general_blocks.py

+48-4
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,47 @@ def test_bad_attributes(self):
384384
)
385385

386386

387+
class TestBlocksMdInHTML(util.MdCase):
388+
"""Test blocks with `md_in_html`."""
389+
390+
extension = ['pymdownx.blocks.tab', 'pymdownx.blocks.html', 'markdown.extensions.md_in_html']
391+
extension_configs = {
392+
'pymdownx.blocks.tab': {'alternate_style': True}
393+
}
394+
395+
396+
def test_md_in_html_inserted_correctly(self):
397+
"""Test that `md_in_html` inserts under the correct target."""
398+
399+
self.check_markdown(
400+
R"""
401+
//// html | div.my-div
402+
403+
/// tab | TEST
404+
<div class="mf-generated" markdown>
405+
Hello I'm in a div which can contain **markdown**!
406+
</div>
407+
///
408+
409+
////
410+
""",
411+
"""
412+
<div class="my-div">
413+
<div class="tabbed-set tabbed-alternate" data-tabs="1:1"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">TEST</label></div>
414+
<div class="tabbed-content">
415+
<div class="tabbed-block">
416+
<div class="mf-generated">
417+
<p>Hello I'm in a div which can contain <strong>markdown</strong>!</p>
418+
</div>
419+
</div>
420+
</div>
421+
</div>
422+
</div>
423+
""", # noqa: E501
424+
True
425+
)
426+
427+
387428
class TestBlocksTab(util.MdCase):
388429
"""Test Blocks tab cases."""
389430

@@ -594,13 +635,15 @@ def test_with_complex_lists(self):
594635
- List
595636
596637
/// tab | Tab
597-
- Paragraph
638+
- Paragraph
598639
599-
/// tab | Tab
600-
1. Paragraph
640+
//// tab | Tab
641+
1. Paragraph
601642
602643
Paragraph
603-
///
644+
645+
Paragraph
646+
////
604647
///
605648
''',
606649
'''
@@ -620,6 +663,7 @@ def test_with_complex_lists(self):
620663
<li>
621664
<p>Paragraph</p>
622665
<p>Paragraph</p>
666+
<p>Paragraph</p>
623667
</li>
624668
</ol>
625669
</div>

0 commit comments

Comments
 (0)