Repository: facelessuser/pymdown-extensions

Issue:
Nested tabs in the unofficial "steps extension" stopped working in 10.14
### Description

I have been using the CSS trick to enable the Steps functionality as linked [here](https://github.com/squidfunk/mkdocs-material/issues/7760#issuecomment-2574390090).

To be precise, the steps functionality has been copied verbatim, and added to the mkdocs-material via extra css:

```css
:root>* {
    --md-steps-bg-color: var(--md-code-bg-color);
    --md-steps-fg-color: var(--md-code-fg-color);
    --md-steps-border-color: hsla(0, 0%, 75%, 1);
}

:root>*[data-md-color-scheme="slate"] {
    --md-steps-border-color: hsla(225, 15%, 30%, 1);
}

.md-typeset div.steps>ol {
    counter-reset: item;
}

.md-typeset div.steps>ol>li {
    position: relative;
    padding-left: 1em;
    list-style: none;
}

.md-typeset div.steps>ol>li::before {
    position: absolute;
    top: 0;
    left: 0;
    z-index: -1;
    display: block;
    width: 1.75em;
    height: 1.75em;
    margin-left: -1.25em;
    color: var(--md-steps-fg-color);
    font-weight: 600;
    font-size: 0.9em;
    text-align: center;
    background-color: var(--md-steps-bg-color);
    border: 1px solid var(--md-steps-border-color);
    border-radius: 100px;
    content: counter(item);
    counter-increment: item;
}

.md-typeset div.steps>ol>li::after {
    position: absolute;
    top: 0;
    bottom: 0;
    left: -0.36em;
    display: block;
    width: 1px;
    margin-top: 2em;
    border-left: 1px solid var(--md-steps-border-color);
    content: '';
}
```

After upgrading to 10.14 from 10.12 I noticed that nesting tabs under the "step" stopped working.

It used to be rendered like this with 10.12:

![Image](https://github.com/user-attachments/assets/e69e7649-801f-4417-b11f-87cca3556305)

And after upgrading to 10.14.3 the second tab started to be placed outside of the step:

![Image](https://github.com/user-attachments/assets/700696d3-7abe-4c4c-bf33-2747d291deec)


My blocks config:

```yaml
  - pymdownx.blocks.tab:
      alternate_style: true
  - pymdownx.blocks.admonition
  - pymdownx.blocks.definition
  - pymdownx.blocks.details
  - pymdownx.blocks.html:
      custom:
        - tag: td
          mode: block
        - tag: th
          mode: block
```



### Minimal Reproduction

1. Add steps.css as listed above
2. create a steps markdown file like below:

```
/// html | div.steps

1. Step1

    //// tab | First tab
    ////
    //// tab | Second tab
    ////
///
```
3. Observe that the second tab is rendered outside of the step.

### Version(s) & System Info

- Operating System: Debian 12
- Python Version: 3.11
- Package Version: 10.14.3


Discussion:
I suspect 10.14.2 is the problem unfortunately. We were trying to fix some issues with `md_in_html`, but I think the list logic is not quite right. I'll probably need to rethink the approach.
After playing with it more I realised that one of the offenders was the missing newline between the tab elements, which was not a problem before. E.g. with 10.14.3 this started to work:

```
/// html | div.steps

1. Step1

    //// tab | First tab
    ////

    //// tab | Second tab
    ////
///
```

Note the newline between tab blocks.


But then I had a more sophisticated example where a list had nested lists, and this one did not work even with newlines:


```
/// html | div.steps

1. Step1
    1. Substep1-1

        //// tab | First tab
        ////
    
        //// tab | Second tab
        ////
    ///
```

As a workaround I removed nested lists from the docs for the time being.
Yes, you should generally separate blocks from each other, but I think the main problem is how it is parsing content within a block with respect to lists. More specifically, how it is handling indentation.

In your example over on the other repo, you had things like:

````
/// tab | apt

```shell
sudo apt install -y make
```

///
````

But when sent through as

````
/// tab | apt
```shell
sudo apt install -y make
```
///
````

There were no issues.

This is because when there are new lines inside the tab, while being nested under a list, the processing of the content is broken up into separate blocks when there are empty new lines. We need to figure out indentation on these blocks so they get processed correctly as we are running before the list handler dedents them. This is necessary because if we don't, `md_in_html` can make a mess of things for us, if an HTML block is wrapping Block container. It can cause us to target the wrong parent for block content.

We have logic to try and determine how indented these blocks are because we are running before the lists de-indent the blocks. But if we get it wrong, the content won't get parsed right.

Anyway, long story short, Python Markdown has a number of complicated aspects in the parsing pipeline, and we're trying to smooth these out to avoid some weird corner cases. Unfortunately, I don't think it is quite right yet.

interesting, thanks for a brief dive into the inner details. The forced newlines around the code blocks in my case is forced by the md linter, so I always have them in my doc set.
I have a draft PR up. I honestly thought this would take me a lot longer to resolve, as it was a bit complicated to work through.



Patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 419a0c290..594e75a48 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+-   **FIX**: Blocks: Fix some corner cases of nested blocks with lists.
 -   **FIX**: Tab and Tabbed: Fix a case where tabs could fail if `combine_header_slug` was enabled and there was no
     header.
 
diff --git a/pymdownx/blocks/__init__.py b/pymdownx/blocks/__init__.py
index 1608917b1..cda6f0f71 100644
--- a/pymdownx/blocks/__init__.py
+++ b/pymdownx/blocks/__init__.py
@@ -338,10 +338,11 @@ def get_parent(self, parent):
 
         temp = parent
         while temp is not None:
-            for entry in self.stack:
-                if entry.hungry and entry.parent is temp:
-                    self.cached_parent = temp
-                    return temp
+            if not self.stack:
+                break
+            if self.stack[-1].hungry and self.stack[-1].parent is temp:
+                self.cached_parent = temp
+                return temp
             if temp is not None:
                 temp = self.lastChild(temp)
         return None
@@ -356,7 +357,7 @@ def is_block(self, tag):
 
         return tag.tag in self.block_tags
 
-    def parse_blocks(self, blocks):
+    def parse_blocks(self, blocks, current_parent):
         """Parse the blocks."""
 
         # Get the target element and parse
@@ -372,9 +373,19 @@ def parse_blocks(self, blocks):
             # has not fully adjusted list indentation, so look at how many
             # list item parents we have on the stack and adjust the content
             # accordingly.
-            li = [e.parent.tag in ('li', 'dd') for e in self.stack[:-1]]
-            length = len(li) * self.tab_length
-            b, a = self.detab_by_length(b, length)
+            parent_map = {c: p for p in current_parent.iter() for c in p}
+            # Only need to count lists between nested blocks
+            parent = self.stack[-1].el if len(self.stack) > 1 else None
+            li = 0
+            while parent is not None:
+                parent = parent_map.get(parent, None)
+                if parent is not None:
+                    if parent.tag in ('li', 'dd'):
+                        li += 1
+                    continue
+                break
+
+            b, a = self.detab_by_length(b, li * self.tab_length)
             if a:
                 blocks.insert(0, a)
 
@@ -468,7 +479,7 @@ def run(self, parent, blocks):
             self.stack.append(BlockEntry(generic_block, el, parent))
 
             # Parse the text blocks under the Block
-            self.parse_blocks(blocks)
+            self.parse_blocks(blocks, parent)
 
         else:
             for r in range(len(self.stack)):
@@ -476,7 +487,7 @@ def run(self, parent, blocks):
                 if entry.hungry and parent is entry.parent:
                     # Get the target element and parse
                     entry.hungry = False
-                    self.parse_blocks(blocks)
+                    self.parse_blocks(blocks, parent)
 
                     break
 


Test patch:
diff --git a/tests/test_extensions/test_blocks/test_general_blocks.py b/tests/test_extensions/test_blocks/test_general_blocks.py
index 57d0c604f..1939e0d51 100644
--- a/tests/test_extensions/test_blocks/test_general_blocks.py
+++ b/tests/test_extensions/test_blocks/test_general_blocks.py
@@ -384,6 +384,78 @@ def test_bad_attributes(self):
         )
 
 
+class TestNestedBlocksAndLists(util.MdCase):
+    """Test Nested blocks and lists."""
+
+    extension = ['pymdownx.blocks.tab', 'pymdownx.blocks.html']
+    extension_configs = {
+        'pymdownx.blocks.tab': {'alternate_style': True}
+    }
+
+    def test_nested_blocks_in_lists(self):
+        """Test a nested blocks case with lists."""
+
+        self.check_markdown(
+            R"""
+            //// html | div.my-div
+
+            - List
+
+                - List
+
+                    /// tab | TEST1
+
+                    Content
+
+                    ///
+
+                    /// tab | TEST2
+
+                    - A list
+
+                        Paragraph
+
+                            Code
+
+                    ///
+
+            ////
+            """,
+            """
+            <div class="my-div">
+            <ul>
+            <li>
+            <p>List</p>
+            <ul>
+            <li>
+            <p>List</p>
+            <div class="tabbed-set tabbed-alternate" data-tabs="1:2"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">TEST1</label><label for="__tabbed_1_2">TEST2</label></div>
+            <div class="tabbed-content">
+            <div class="tabbed-block">
+            <p>Content</p>
+            </div>
+            <div class="tabbed-block">
+            <ul>
+            <li>
+            <p>A list</p>
+            <p>Paragraph</p>
+            <pre><code>Code
+            </code></pre>
+            </li>
+            </ul>
+            </div>
+            </div>
+            </div>
+            </li>
+            </ul>
+            </li>
+            </ul>
+            </div>
+            """,  # noqa: E501
+            True
+        )
+
+
 class TestBlocksMdInHTML(util.MdCase):
     """Test blocks with `md_in_html`."""
 
@@ -392,7 +464,6 @@ class TestBlocksMdInHTML(util.MdCase):
         'pymdownx.blocks.tab': {'alternate_style': True}
     }
 
-
     def test_md_in_html_inserted_correctly(self):
         """Test that `md_in_html` inserts under the correct target."""