Repository: jackdewinter/pymarkdown

Issue:
Md031 with multiple levels of lists does not fix properly
## Prerequisites

* [X] Are you running the latest version of this application?
* [X] Have you checked the [Frequently Asked Questions](https://pymarkdown.readthedocs.io/en/latest/faq) document?
* [X] Have you simplified the bug report to the essential details?
  * [X] Do you have a distinct command line to report?
  * [X] Can you clearly state the configuration for this bug report?
  * [X] Do you have a minimal document that highlights this bug?
  * [X] Are any required files (configuration or Markdown document) attached to the issue?
* [X] Did you perform a cursory search of other issues to look for related issues?

## Bug Report

### Bug Type

* [X] Fix/Rule not working as expected

### Description

When encountering text such as in test `bad_fenced_block_in_list_in_list_with_previous_inner_list`, the multiple layers of starting lists confuse the fixing algorithm.

### Specifics

What operating system and version are you running into this behavior on?

Win11

What version are you seeing this behavior in? (Run `pip list` or `pipenv run pip list` and look for the entry beside `pymarkdownlnt`.)

latest, main

Are there any extra steps that need to be taken before executing the application?

No

What is the command line you invoke to get this behavior?

bad_fenced_block_in_list_in_list_with_previous_inner_list

Are you using a configuration file? Either on the command line or one of the implicit configuration files? If so, attach that file to this issue.

No

What Markdown document causes this behavior to manifest?  Attach that file to this issue.

```
+ + list 1
+ + + list 2.1
      list 2.2
    ```block
    A code block
    ```
  + another list
```

### Actual Behavior

The fix algorithm gets the indices into li(2,3) wrong, off by one.

### Expected Behavior

The fix algorithm gets the indices into li(2,3) correct.

Discussion:
sourcery-ai[bot]: <!-- Generated by sourcery-ai[bot]: start review_guide -->

🧙 Sourcery is reviewing your pull request!

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jackdewinter/pymarkdown/pull/1218?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`e479453`)](https://app.codecov.io/gh/jackdewinter/pymarkdown/commit/e479453b3d80125dde67ac5c70fde293c9dbf611?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`41eb15a`)](https://app.codecov.io/gh/jackdewinter/pymarkdown/commit/41eb15aa4c78fa83addbbfbd6516b458b889faee?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main     #1218   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          191       191           
  Lines        20983     20985    +2     
  Branches      2678      2679    +1     
=========================================
+ Hits         20983     20985    +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jackdewinter/pymarkdown/pull/1218?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/newdocs/src/changelog.md b/newdocs/src/changelog.md
index d8749466..efdd7865 100644
--- a/newdocs/src/changelog.md
+++ b/newdocs/src/changelog.md
@@ -40,6 +40,8 @@
     - rehydration/fix issue with blank lines causing off by one error
 - [Issue 1209](https://github.com/jackdewinter/pymarkdown/issues/1209)
     - detection code for MD027 was not using the right container index
+- [Issue 1217](https://github.com/jackdewinter/pymarkdown/issues/1217)
+    - fixed issue with nested list starts not being accounted for properly
 
 <!--- pyml disable-next-line no-duplicate-heading-->
 ### Changed
diff --git a/publish/coverage.json b/publish/coverage.json
index a3b9de82..b3d25733 100644
--- a/publish/coverage.json
+++ b/publish/coverage.json
@@ -2,12 +2,12 @@
     "projectName": "pymarkdown",
     "reportSource": "pytest",
     "branchLevel": {
-        "totalMeasured": 5357,
-        "totalCovered": 5357
+        "totalMeasured": 5359,
+        "totalCovered": 5359
     },
     "lineLevel": {
-        "totalMeasured": 20983,
-        "totalCovered": 20983
+        "totalMeasured": 20985,
+        "totalCovered": 20985
     }
 }
 
diff --git a/pymarkdown/plugins/utils/leading_space_index_tracker.py b/pymarkdown/plugins/utils/leading_space_index_tracker.py
index 0ba346af..d294f48e 100644
--- a/pymarkdown/plugins/utils/leading_space_index_tracker.py
+++ b/pymarkdown/plugins/utils/leading_space_index_tracker.py
@@ -68,7 +68,7 @@ class LeadingSpaceIndexTracker:
         """
 
         if self.__container_token_stack[-1].is_block_quote_start:
-            self.__process_container_end_block_quote(token, self.__end_tokens)
+            self.__process_container_end_block_quote(token)
         else:
             self.__process_container_end_list(token)
 
@@ -121,9 +121,7 @@ class LeadingSpaceIndexTracker:
             return setext_token.original_line_number
         return token.line_number
 
-    def __process_container_end_block_quote(
-        self, token: MarkdownToken, end_tokens: List[EndMarkdownToken]
-    ) -> None:
+    def __process_container_end_block_quote(self, token: MarkdownToken) -> None:
         for stack_index in range(len(self.__container_token_stack) - 2, -1, -1):
             current_stack_token = self.__container_token_stack[stack_index]
             if current_stack_token.is_block_quote_start:
@@ -131,7 +129,7 @@ class LeadingSpaceIndexTracker:
                     LeadingSpaceIndexTracker.calculate_token_line_number(token)
                     - self.__container_token_stack[-1].line_number
                 )
-                if end_tokens[-1].extra_end_data is not None:
+                if self.__end_tokens[-1].extra_end_data is not None:
                     line_number_delta += 1
                 self.__closed_container_adjustments[
                     stack_index
@@ -165,6 +163,11 @@ class LeadingSpaceIndexTracker:
                     LeadingSpaceIndexTracker.calculate_token_line_number(token)
                     - self.__container_token_stack[-1].line_number
                 )
+                if (
+                    self.__end_tokens[-1].start_markdown_token.line_number
+                    == current_stack_token.line_number
+                ):
+                    line_number_delta -= 1
                 self.__closed_container_adjustments[
                     stack_index
                 ].adjustment += line_number_delta


Test patch:
diff --git a/publish/test-results.json b/publish/test-results.json
index 23baa29b..4915f063 100644
--- a/publish/test-results.json
+++ b/publish/test-results.json
@@ -1364,10 +1364,10 @@
         },
         {
             "name": "test.rules.test_md031",
-            "totalTests": 373,
+            "totalTests": 377,
             "failedTests": 0,
             "errorTests": 0,
-            "skippedTests": 13,
+            "skippedTests": 5,
             "elapsedTimeInMilliseconds": 0
         },
         {
@@ -1620,7 +1620,7 @@
         },
         {
             "name": "test.test_markdown_extra",
-            "totalTests": 336,
+            "totalTests": 340,
             "failedTests": 0,
             "errorTests": 0,
             "skippedTests": 1,
diff --git a/test/rules/test_md031.py b/test/rules/test_md031.py
index 9e7fc139..d9fab92f 100644
--- a/test/rules/test_md031.py
+++ b/test/rules/test_md031.py
@@ -802,8 +802,7 @@ This is a blank line and some text.
 > ```
 >This is a blank line and some text.
 """,
-        mark_scan_as_skipped=True,
-        mark_fix_as_skipped=True,
+        mark_fix_as_skipped=temp_disable_fixes,
         scan_expected_return_code=1,
         scan_expected_output="""{temp_source_path}:5:3: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 {temp_source_path}:7:3: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
@@ -3933,7 +3932,6 @@ abc
 {temp_source_path}:9:7: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""> + > -----
 >   > > block 1
 >   > > block 2
@@ -4195,6 +4193,7 @@ abc
 {temp_source_path}:6:7: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032,md027",
+        use_debug=True,
         mark_fix_as_skipped=temp_disable_fixes,
         use_fix_debug=True,
         fix_expected_file_contents="""> + + -----
@@ -4536,7 +4535,7 @@ abc
 """,
     ),
     pluginRuleTest(  # test_extra_047a0 test_extra_047a1
-        "bad_fenced_block_in_list_in_list_with_previous_inner_list",
+        "bad_fenced_block_in_list_in_list_with_previous_inner_listx",
         source_file_contents="""+ + list 1
 + + + list 2.1
       list 2.2
@@ -4550,7 +4549,6 @@ abc
 {temp_source_path}:6:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""+ + list 1
 + + + list 2.1
       list 2.2
@@ -4562,7 +4560,7 @@ abc
   + another list
 """,
     ),
-    pluginRuleTest(
+    pluginRuleTest(  # test_extra_047g0 test_extra_047g1
         "bad_fenced_block_in_list_in_list_with_previous_inner_list_with_thematics",
         source_file_contents="""+ + list 1
 + + + list 2.1
@@ -4578,7 +4576,6 @@ abc
 {temp_source_path}:7:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""+ + list 1
 + + + list 2.1
       list 2.2
@@ -4588,6 +4585,34 @@ abc
     A code block
     ```
 
+  + another list
+""",
+    ),
+    pluginRuleTest(  # test_extra_047g2 test_extra_047g3
+        "bad_fenced_block_in_list_in_list2_with_previous_inner_list_with_thematics",
+        source_file_contents="""+ + list 1
+    + list 2.1
+      list 2.2
+    ----
+    ```block
+    A code block
+    ```
+  + another list
+""",
+        scan_expected_return_code=1,
+        scan_expected_output="""{temp_source_path}:5:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
+{temp_source_path}:7:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
+""",
+        disable_rules="md032",
+        fix_expected_file_contents="""+ + list 1
+    + list 2.1
+      list 2.2
+    ----
+
+    ```block
+    A code block
+    ```
+
   + another list
 """,
     ),
@@ -4611,16 +4636,19 @@ abc
 {temp_source_path}:9:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032,md022,md024",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""+ + list 1
 + + + list 2.1
       list 2.2
+
+    abc
     ----
 
     ```block
     A code block
     ```
 
+    abc
+    ----
   + another list
 """,
     ),
@@ -4639,7 +4667,6 @@ abc
 {temp_source_path}:6:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""+ + list 1
 + + + list 2.1
     list 2.2
@@ -4667,7 +4694,6 @@ abc
 {temp_source_path}:7:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""+ + list 1
 + + + list 2.1
     list 2.2
@@ -4700,16 +4726,19 @@ abc
 {temp_source_path}:9:5: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
 """,
         disable_rules="md032,md022,md024",
-        mark_fix_as_skipped=temp_disable_fixes,
         fix_expected_file_contents="""+ + list 1
 + + + list 2.1
     list 2.2
+
+    abc
     ---
 
     ```block
     A code block
     ```
 
+    abc
+    ---
   + another list
 """,
     ),
@@ -5069,6 +5098,34 @@ abc
       abc
       -----
   + another list
+""",
+    ),
+    pluginRuleTest(
+        "bad_fenced_block_in_list_in_list_in_list_with_previous_inner_list_with_thematics",
+        source_file_contents="""+ + + list 1
++ + + + list 2.1
+        list 2.2
+      ----
+      ```block
+      A code block
+      ```
+    + another list
+""",
+        scan_expected_return_code=1,
+        scan_expected_output="""{temp_source_path}:5:7: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
+{temp_source_path}:7:7: MD031: Fenced code blocks should be surrounded by blank lines (blanks-around-fences)
+""",
+        disable_rules="md032",
+        fix_expected_file_contents="""+ + + list 1
++ + + + list 2.1
+        list 2.2
+      ----
+
+      ```block
+      A code block
+      ```
+
+    + another list
 """,
     ),
     pluginRuleTest(
diff --git a/test/test_markdown_extra.py b/test/test_markdown_extra.py
index bfa8b077..c3572f8e 100644
--- a/test/test_markdown_extra.py
+++ b/test/test_markdown_extra.py
@@ -17086,6 +17086,279 @@ inner block</p>
     act_and_assert(source_markdown, expected_gfm, expected_tokens)
 
 
+@pytest.mark.gfm
+def test_extra_047g0():
+    """
+    TBD
+    bad_fenced_block_in_list_in_list_with_previous_inner_list_with_thematics
+    """
+
+    # Arrange
+    source_markdown = """+ + list 1
++ + + list 2.1
+      list 2.2
+    ----
+    ```block
+    A code block
+    ```
+  + another list
+"""
+    expected_tokens = [
+        "[ulist(1,1):+::2:]",
+        "[ulist(1,3):+::4:  ]",
+        "[para(1,5):]",
+        "[text(1,5):list 1:]",
+        "[end-para:::True]",
+        "[end-ulist:::True]",
+        "[li(2,1):2::]",
+        "[ulist(2,3):+::4:  :    \n    \n    \n    \n]",
+        "[ulist(2,5):+::6:    :      ]",
+        "[para(2,7):\n]",
+        "[text(2,7):list 2.1\nlist 2.2::\n]",
+        "[end-para:::True]",
+        "[end-ulist:::True]",
+        "[tbreak(4,5):-::----]",
+        "[fcode-block(5,5):`:3:block:::::]",
+        "[text(6,5):A code block:]",
+        "[end-fcode-block:::3:False]",
+        "[li(8,3):4:  :]",
+        "[para(8,5):]",
+        "[text(8,5):another list:]",
+        "[end-para:::True]",
+        "[BLANK(9,1):]",
+        "[end-ulist:::True]",
+        "[end-ulist:::True]",
+    ]
+    expected_gfm = """<ul>
+<li>
+<ul>
+<li>list 1</li>
+</ul>
+</li>
+<li>
+<ul>
+<li>
+<ul>
+<li>list 2.1
+list 2.2</li>
+</ul>
+<hr />
+<pre><code class="language-block">A code block
+</code></pre>
+</li>
+<li>another list</li>
+</ul>
+</li>
+</ul>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
+@pytest.mark.gfm
+def test_extra_047g1():
+    """
+    TBD
+    bad_fenced_block_in_list_in_list_with_previous_inner_list_with_thematics
+    """
+
+    # Arrange
+    source_markdown = """+ + list 1
++ + + list 2.1
+      list 2.2
+    ----
+
+    ```block
+    A code block
+    ```
+
+  + another list
+"""
+    expected_tokens = [
+        "[ulist(1,1):+::2:]",
+        "[ulist(1,3):+::4:  ]",
+        "[para(1,5):]",
+        "[text(1,5):list 1:]",
+        "[end-para:::True]",
+        "[end-ulist:::True]",
+        "[li(2,1):2::]",
+        "[ulist(2,3):+::4:  :    \n\n    \n    \n    \n\n]",
+        "[ulist(2,5):+::6:    :      ]",
+        "[para(2,7):\n]",
+        "[text(2,7):list 2.1\nlist 2.2::\n]",
+        "[end-para:::True]",
+        "[end-ulist:::True]",
+        "[tbreak(4,5):-::----]",
+        "[BLANK(5,1):]",
+        "[fcode-block(6,5):`:3:block:::::]",
+        "[text(7,1):A code block:]",
+        "[end-fcode-block:::3:False]",
+        "[BLANK(9,1):]",
+        "[li(10,3):4:  :]",
+        "[para(10,5):]",
+        "[text(10,5):another list:]",
+        "[end-para:::True]",
+        "[BLANK(11,1):]",
+        "[end-ulist:::True]",
+        "[end-ulist:::True]",
+    ]
+    expected_gfm = """<ul>
+<li>
+<ul>
+<li>list 1</li>
+</ul>
+</li>
+<li>
+<ul>
+<li>
+<ul>
+<li>list 2.1
+list 2.2</li>
+</ul>
+<hr />
+<pre><code class="language-block">A code block
+</code></pre>
+</li>
+<li>
+<p>another list</p>
+</li>
+</ul>
+</li>
+</ul>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
+@pytest.mark.gfm
+def test_extra_047g2():
+    """
+    TBD
+    bad_fenced_block_in_list_in_list2_with_previous_inner_list_with_thematics
+    """
+
+    # Arrange
+    source_markdown = """+ + list 1
+    + list 2.1
+      list 2.2
+    ----
+    ```block
+    A code block
+    ```
+  + another list
+"""
+    expected_tokens = [
+        "[ulist(1,1):+::2:]",
+        "[ulist(1,3):+::4:  :    \n    \n    \n    \n]",
+        "[para(1,5):]",
+        "[text(1,5):list 1:]",
+        "[end-para:::True]",
+        "[ulist(2,5):+::6:    :      ]",
+        "[para(2,7):\n]",
+        "[text(2,7):list 2.1\nlist 2.2::\n]",
+        "[end-para:::True]",
+        "[end-ulist:::True]",
+        "[tbreak(4,5):-::----]",
+        "[fcode-block(5,5):`:3:block:::::]",
+        "[text(6,5):A code block:]",
+        "[end-fcode-block:::3:False]",
+        "[li(8,3):4:  :]",
+        "[para(8,5):]",
+        "[text(8,5):another list:]",
+        "[end-para:::True]",
+        "[BLANK(9,1):]",
+        "[end-ulist:::True]",
+        "[end-ulist:::True]",
+    ]
+    expected_gfm = """<ul>
+<li>
+<ul>
+<li>list 1
+<ul>
+<li>list 2.1
+list 2.2</li>
+</ul>
+<hr />
+<pre><code class="language-block">A code block
+</code></pre>
+</li>
+<li>another list</li>
+</ul>
+</li>
+</ul>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
+@pytest.mark.gfm
+def test_extra_047g3():
+    """
+    TBD
+    bad_fenced_block_in_list_in_list2_with_previous_inner_list_with_thematics
+    """
+
+    # Arrange
+    source_markdown = """+ + list 1
+    + list 2.1
+      list 2.2
+    ----
+
+    ```block
+    A code block
+    ```
+
+  + another list
+"""
+    expected_tokens = [
+        "[ulist(1,1):+::2:]",
+        "[ulist(1,3):+::4:  :    \n\n    \n    \n    \n\n]",
+        "[para(1,5):]",
+        "[text(1,5):list 1:]",
+        "[end-para:::True]",
+        "[ulist(2,5):+::6:    :      ]",
+        "[para(2,7):\n]",
+        "[text(2,7):list 2.1\nlist 2.2::\n]",
+        "[end-para:::True]",
+        "[end-ulist:::True]",
+        "[tbreak(4,5):-::----]",
+        "[BLANK(5,1):]",
+        "[fcode-block(6,5):`:3:block:::::]",
+        "[text(7,1):A code block:]",
+        "[end-fcode-block:::3:False]",
+        "[BLANK(9,1):]",
+        "[li(10,3):4:  :]",
+        "[para(10,5):]",
+        "[text(10,5):another list:]",
+        "[end-para:::True]",
+        "[BLANK(11,1):]",
+        "[end-ulist:::True]",
+        "[end-ulist:::True]",
+    ]
+    expected_gfm = """<ul>
+<li>
+<ul>
+<li>
+<p>list 1</p>
+<ul>
+<li>list 2.1
+list 2.2</li>
+</ul>
+<hr />
+<pre><code class="language-block">A code block
+</code></pre>
+</li>
+<li>
+<p>another list</p>
+</li>
+</ul>
+</li>
+</ul>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
 @pytest.mark.gfm
 def test_extra_999():
     """