Repository: jackdewinter/pymarkdown

Issue:
Rule Md027: Bad Block Quote Index count with text
Found: https://github.com/jackdewinter/pymarkdown/issues/92

test_md027_bad_block_quote_unordered_list_block_quote_text_first and test_md027_bad_block_quote_unordered_list_block_quote_text_last uncovered a case where the block quote count for the scenarios is off by 1.


Discussion:

Patch:
diff --git a/publish/coverage.json b/publish/coverage.json
index 2c481cfc..ae2c7d17 100644
--- a/publish/coverage.json
+++ b/publish/coverage.json
@@ -2,12 +2,12 @@
     "projectName": "pymarkdown",
     "reportSource": "pytest",
     "branchLevel": {
-        "totalMeasured": 3024,
-        "totalCovered": 3024
+        "totalMeasured": 3026,
+        "totalCovered": 3026
     },
     "lineLevel": {
-        "totalMeasured": 10560,
-        "totalCovered": 10560
+        "totalMeasured": 10576,
+        "totalCovered": 10576
     }
 }
 
diff --git a/pymarkdown/container_block_processor.py b/pymarkdown/container_block_processor.py
index 94680682..fea11531 100644
--- a/pymarkdown/container_block_processor.py
+++ b/pymarkdown/container_block_processor.py
@@ -1728,7 +1728,7 @@ class ContainerBlockProcessor:
         )
         if found_block_quote_token:
             leading_spaces = found_block_quote_token.calculate_next_leading_space_part(
-                increment_index=False, delta=-1
+                increment_index=False, delta=-1, allow_overflow=True
             )
             POGGER.debug("PLFCB>>leading_spaces>>:$:", leading_spaces)
             old_start_index = len(leading_spaces)
diff --git a/pymarkdown/container_markdown_token.py b/pymarkdown/container_markdown_token.py
index 1c45e5bf..968d7db6 100644
--- a/pymarkdown/container_markdown_token.py
+++ b/pymarkdown/container_markdown_token.py
@@ -370,13 +370,19 @@ class BlockQuoteMarkdownToken(ContainerMarkdownToken):
         item_list = [self.__extracted_whitespace, self.__leading_spaces]
         self._set_extra_data(MarkdownToken.extra_data_separator.join(item_list))
 
-    def calculate_next_leading_space_part(self, increment_index=True, delta=0):
+    def calculate_next_leading_space_part(
+        self, increment_index=True, delta=0, allow_overflow=False
+    ):
         """
         Calculate the next leading space based on the leading_text_index,
         optonally incrementing it as well.
         """
         split_leading_spaces = self.leading_spaces.split(ParserHelper.newline_character)
-        leading_text = split_leading_spaces[self.leading_text_index + delta]
-        if increment_index:
-            self.leading_text_index += 1
+        absolute_index = self.leading_text_index + delta
+        if allow_overflow and absolute_index >= len(split_leading_spaces):
+            leading_text = ""
+        else:
+            leading_text = split_leading_spaces[self.leading_text_index + delta]
+            if increment_index:
+                self.leading_text_index += 1
         return leading_text
diff --git a/pymarkdown/list_block_processor.py b/pymarkdown/list_block_processor.py
index 2b6d718c..b211f8a2 100644
--- a/pymarkdown/list_block_processor.py
+++ b/pymarkdown/list_block_processor.py
@@ -455,7 +455,8 @@ class ListBlockProcessor:
                 extracted_whitespace,
                 number_of_digits,
                 block_quote_data,
-                adj_ws=adj_ws,
+                adj_ws,
+                position_marker,
             )
 
             POGGER.debug(
@@ -1053,6 +1054,7 @@ class ListBlockProcessor:
         marker_width_minus_one,
         block_quote_data,
         adj_ws,
+        position_marker,
     ):
         """
         Handle the processing of the first part of the list.
@@ -1093,7 +1095,9 @@ class ListBlockProcessor:
         (
             container_level_tokens,
             block_quote_data,
-        ) = ListBlockProcessor.__handle_list_nesting(parser_state, block_quote_data)
+        ) = ListBlockProcessor.__handle_list_nesting(
+            parser_state, block_quote_data, position_marker
+        )
 
         return (
             indent_level,
@@ -1178,7 +1182,7 @@ class ListBlockProcessor:
     # pylint: enable=too-many-arguments
 
     @staticmethod
-    def __handle_list_nesting(parser_state, block_quote_data):
+    def __handle_list_nesting(parser_state, block_quote_data, position_marker):
         """
         Resolve any nesting issues with block quotes.
         """
@@ -1206,8 +1210,47 @@ class ListBlockProcessor:
                 include_lists=True,
             )
             POGGER.debug("container_level_tokens>>$", container_level_tokens)
+            POGGER.debug("stack>>$", parser_state.token_stack)
+            POGGER.debug(
+                "last token>>$", parser_state.token_stack[-1].matching_markdown_token
+            )
+            POGGER.debug("position_marker.line_number>>$", position_marker.line_number)
             last_block_index = parser_state.find_last_block_quote_on_stack()
-            if last_block_index:
+            POGGER.debug("last_block_index>>$", last_block_index)
+            POGGER.debug(
+                "parser_state.token_stack[-1]>>$", parser_state.token_stack[-1]
+            )
+            POGGER.debug(
+                "parser_state.token_stack[-1].matching_markdown_token>>$",
+                parser_state.token_stack[-1].matching_markdown_token,
+            )
+
+            first_conditional = not parser_state.token_stack[-1].matching_markdown_token
+            second_conditional = (not first_conditional) and (
+                position_marker.line_number
+                == parser_state.token_stack[-1].matching_markdown_token.line_number
+            )
+            third_conditional = parser_state.token_stack[-1].is_block_quote
+
+            secondary_conditionals = (
+                first_conditional or second_conditional or third_conditional
+            )
+            POGGER.debug(
+                "secondary_conditionals>>$ = first_conditional:$ or second_conditional:$ or "
+                + "third_conditional:$",
+                secondary_conditionals,
+                first_conditional,
+                second_conditional,
+                third_conditional,
+            )
+            all_conditionals = last_block_index and secondary_conditionals
+            POGGER.debug(
+                "all_conditionals>>$ = last_block_index:$ or a2:$",
+                all_conditionals,
+                last_block_index,
+                secondary_conditionals,
+            )
+            if all_conditionals:
                 current_last_block_token = parser_state.token_stack[
                     last_block_index
                 ].matching_markdown_token
diff --git a/pymarkdown/plugins/rule_md_027.py b/pymarkdown/plugins/rule_md_027.py
index 416c475d..7c6ab05f 100644
--- a/pymarkdown/plugins/rule_md_027.py
+++ b/pymarkdown/plugins/rule_md_027.py
@@ -6,6 +6,7 @@ from pymarkdown.plugin_details import PluginDetails
 from pymarkdown.rule_plugin import RulePlugin
 
 
+# pylint: disable=too-many-instance-attributes
 class RuleMd027(RulePlugin):
     """
     Class to implement a plugin that looks for excessive spaces after the block quote character.
@@ -20,6 +21,7 @@ class RuleMd027(RulePlugin):
         self.__is_paragraph_end_delayed = None
         self.__delayed_blank_line = None
         self.__have_incremented_for_this_line = None
+        self.__last_token = None
         # self.__debug_on = False
 
     def get_details(self):
@@ -47,6 +49,7 @@ class RuleMd027(RulePlugin):
         self.__is_paragraph_end_delayed = False
         self.__delayed_blank_line = None
         self.__have_incremented_for_this_line = False
+        self.__last_token = None
 
     def __process_delayed_paragraph_end(self, token, num_container_tokens):
         if self.__is_paragraph_end_delayed:
@@ -127,6 +130,8 @@ class RuleMd027(RulePlugin):
         elif num_container_tokens:
             self.__handle_within_block_quotes(context, token)
 
+        self.__last_token = token
+
     def __handle_block_quote_start(self, token):
         self.__container_tokens.append(token)
 
@@ -175,30 +180,32 @@ class RuleMd027(RulePlugin):
         ):
             self.__bq_line_index[num_container_tokens] += 1
 
-        assert newlines_in_container == self.__bq_line_index[num_container_tokens], (
-            str(newlines_in_container)
-            + " == "
-            + str(self.__bq_line_index[num_container_tokens])
-        )
+        # assert newlines_in_container == self.__bq_line_index[num_container_tokens], (
+        #     str(newlines_in_container)
+        #     + " == "
+        #     + str(self.__bq_line_index[num_container_tokens])
+        # )
         del self.__bq_line_index[num_container_tokens]
         del self.__container_tokens[-1]
 
     def __get_last_block_quote(self):
-        found_block_quote_token = None
-        for i in range(len(self.__container_tokens) - 1, -1, -1):
-            if self.__container_tokens[i].is_block_quote_start:
-                found_block_quote_token = self.__container_tokens[i]
-                break
-        return found_block_quote_token
+        return next(
+            (
+                self.__container_tokens[i]
+                for i in range(len(self.__container_tokens) - 1, -1, -1)
+                if self.__container_tokens[i].is_block_quote_start
+            ),
+            None,
+        )
 
     def __check_list_starts(
         self, context, token, num_container_tokens, is_new_list_item
     ):
         # if self.__debug_on:
         #     print(f"num_container_tokens={num_container_tokens};")
+        #     print(f"container_tokens={ParserHelper.make_value_visible(self.__container_tokens)};")
         _ = num_container_tokens
-        found_block_quote_token = self.__get_last_block_quote()
-        if found_block_quote_token:
+        if found_block_quote_token := self.__get_last_block_quote():
             is_start_properly_scoped = False
             if is_new_list_item:
                 is_start_properly_scoped = (
@@ -213,12 +220,39 @@ class RuleMd027(RulePlugin):
             #         f"is_start_properly_scoped={is_start_properly_scoped};"
             #         + f"found_block_quote_token={ParserHelper.make_value_visible(found_block_quote_token)}"
             #     )
-            if is_start_properly_scoped and token.extracted_whitespace:
+            #     print(f"token.extracted_whitespace>:{token.extracted_whitespace}:")
+            #     print(f"self.__last_token>:{ParserHelper.make_value_visible(self.__last_token)}:")
+
+            # In rare cases, a block quote is ended in the middle of a line.
+            # This is one of those cases.
+            whitespace_to_use = token.extracted_whitespace
+            if (
+                self.__last_token
+                and self.__last_token.is_end_token
+                and self.__last_token.start_markdown_token.is_block_quote_start
+            ):
                 # if self.__debug_on:
-                #     print("list-error")
-                column_number_delta = -(
-                    token.column_number - len(token.extracted_whitespace)
+                #     print(f"self.__last_token.start_markdown_token>:{ParserHelper.make_value_visible(\
+                #       self.__last_token.start_markdown_token)}:")
+                #     print("BOOM")
+                split_line_length = (
+                    self.__last_token.start_markdown_token.leading_spaces.split("\n")[
+                        -1
+                    ]
                 )
+                # if self.__debug_on:
+                #     print(f"BOOM:{split_line_length}:")
+                whitespace_to_use = whitespace_to_use[len(split_line_length) :]
+
+            if is_start_properly_scoped and whitespace_to_use:
+                column_number_delta = -(token.column_number - len(whitespace_to_use))
+                # if self.__debug_on:
+                #     print("list-error")
+                #     line_index = (
+                #         self.__bq_line_index[num_container_tokens] + 0
+                #     )
+                #     print(f"5>{line_index}")
+                #     print(f"column-delta>{column_number_delta}")
                 self.report_next_token_error(
                     context, token, column_number_delta=column_number_delta
                 )
@@ -624,3 +658,6 @@ class RuleMd027(RulePlugin):
             self.__last_leaf_token = token
         elif token.is_html_block_end or token.is_indented_code_block_end:
             self.__last_leaf_token = None
+
+
+# pylint: enable=too-many-instance-attributes


Test patch:
diff --git a/publish/test-results.json b/publish/test-results.json
index db9d1e27..1a9d89d9 100644
--- a/publish/test-results.json
+++ b/publish/test-results.json
@@ -364,10 +364,10 @@
         },
         {
             "name": "test.test_markdown_extra",
-            "totalTests": 66,
+            "totalTests": 70,
             "failedTests": 0,
             "errorTests": 0,
-            "skippedTests": 4,
+            "skippedTests": 0,
             "elapsedTimeInMilliseconds": 0
         },
         {
@@ -967,7 +967,7 @@
             "totalTests": 66,
             "failedTests": 0,
             "errorTests": 0,
-            "skippedTests": 2,
+            "skippedTests": 0,
             "elapsedTimeInMilliseconds": 0
         },
         {
diff --git a/test/test_markdown_extra.py b/test/test_markdown_extra.py
index 6ff47e7b..f93a4140 100644
--- a/test/test_markdown_extra.py
+++ b/test/test_markdown_extra.py
@@ -2568,8 +2568,7 @@ def test_extra_024a():
 
 
 @pytest.mark.gfm
-@pytest.mark.skip
-def test_extra_025x():
+def test_extra_025xx():
     """
     TBD
     """
@@ -2582,34 +2581,66 @@ def test_extra_025x():
 > + that
 """
     expected_tokens = [
-        "[block-quote(1,1)::> \n> \n> \n> \n> \n> ]",
-        "[ulist(1,3):*::4:]",
-        "[para(1,5):]",
-        "[text(1,5):Item 1:]",
+        "[block-quote(1,1)::> \n> \n> ]",
+        "[ulist(1,3):+::4::  \n\n\n]",
+        "[para(1,5):\n]",
+        "[text(1,5):list\nthis::\n]",
         "[end-para:::True]",
-        "[ulist(2,5):*::6:  ]",
-        "[para(2,7):]",
-        "[text(2,7):Item 1a:]",
+        "[block-quote(3,5)::> \n>   > \n> ]",
+        "[para(3,8): \n]",
+        "[text(3,8):good\nitem::\n]",
         "[end-para:::True]",
-        "[li(3,5):6:  :]",
-        "[para(3,7):]",
-        "[text(3,7):Item 1b:]",
+        "[end-block-quote:::True]",
+        "[li(5,3):4:  :]",
+        "[para(5,5):]",
+        "[text(5,5):that:]",
         "[end-para:::True]",
+        "[BLANK(6,1):]",
         "[end-ulist:::True]",
-        "[li(4,4):5: :]",
-        "[para(4,6):]",
-        "[text(4,6):Item 2:]",
-        "[end-para:::True]",
-        "[ulist(5,6):*::7:   :]",
-        "[para(5,8):]",
-        "[text(5,8):Item 2a:]",
+        "[end-block-quote:::True]",
+    ]
+    expected_gfm = """<blockquote>
+<ul>
+<li>list
+this
+<blockquote>
+<p>good
+item</p>
+</blockquote>
+</li>
+<li>that</li>
+</ul>
+</blockquote>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
+@pytest.mark.gfm
+def test_extra_025xa():
+    """
+    TBD
+    """
+
+    # Arrange
+    source_markdown = """> + list
+>   this
+>   >  good
+>   > item
+>   that
+"""
+    expected_tokens = [
+        "[block-quote(1,1)::> \n> \n> ]",
+        "[ulist(1,3):+::4::  \n\n\n\n]",
+        "[para(1,5):\n]",
+        "[text(1,5):list\nthis::\n]",
         "[end-para:::True]",
-        "[li(6,6):7:   :]",
-        "[para(6,8):]",
-        "[text(6,8):Item 2b:]",
+        "[block-quote(3,5)::> \n>   > \n> \n]",
+        "[para(3,8): \n\n  ]",
+        "[text(3,8):good\nitem\nthat::\n\n]",
         "[end-para:::True]",
-        "[BLANK(7,1):]",
-        "[end-ulist:::True]",
+        "[end-block-quote:::True]",
+        "[BLANK(6,1):]",
         "[end-ulist:::True]",
         "[end-block-quote:::True]",
     ]
@@ -2619,10 +2650,10 @@ def test_extra_025x():
 this
 <blockquote>
 <p>good
-item</p>
+item
+that</p>
 </blockquote>
 </li>
-<li>that</li>
 </ul>
 </blockquote>"""
 
@@ -2631,8 +2662,7 @@ item</p>
 
 
 @pytest.mark.gfm
-@pytest.mark.skip
-def test_extra_025a():
+def test_extra_025ax():
     """
     TBD
     """
@@ -2645,34 +2675,66 @@ def test_extra_025a():
 > + that
 """
     expected_tokens = [
-        "[block-quote(1,1)::> \n> \n> \n> \n> \n> ]",
-        "[ulist(1,3):*::4:]",
-        "[para(1,5):]",
-        "[text(1,5):Item 1:]",
+        "[block-quote(1,1)::> \n> \n> ]",
+        "[ulist(1,3):+::4::  \n\n\n]",
+        "[para(1,5):\n]",
+        "[text(1,5):list\nthis::\n]",
         "[end-para:::True]",
-        "[ulist(2,5):*::6:  ]",
-        "[para(2,7):]",
-        "[text(2,7):Item 1a:]",
+        "[block-quote(3,5)::> \n>   > \n> ]",
+        "[para(3,7):\n]",
+        "[text(3,7):good\nitem::\n]",
         "[end-para:::True]",
-        "[li(3,5):6:  :]",
-        "[para(3,7):]",
-        "[text(3,7):Item 1b:]",
+        "[end-block-quote:::True]",
+        "[li(5,3):4:  :]",
+        "[para(5,5):]",
+        "[text(5,5):that:]",
         "[end-para:::True]",
+        "[BLANK(6,1):]",
         "[end-ulist:::True]",
-        "[li(4,4):5: :]",
-        "[para(4,6):]",
-        "[text(4,6):Item 2:]",
-        "[end-para:::True]",
-        "[ulist(5,6):*::7:   :]",
-        "[para(5,8):]",
-        "[text(5,8):Item 2a:]",
+        "[end-block-quote:::True]",
+    ]
+    expected_gfm = """<blockquote>
+<ul>
+<li>list
+this
+<blockquote>
+<p>good
+item</p>
+</blockquote>
+</li>
+<li>that</li>
+</ul>
+</blockquote>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
+@pytest.mark.gfm
+def test_extra_025aa():
+    """
+    TBD
+    """
+
+    # Arrange
+    source_markdown = """> + list
+>   this
+>   > good
+>   > item
+>   that
+"""
+    expected_tokens = [
+        "[block-quote(1,1)::> \n> \n> ]",
+        "[ulist(1,3):+::4::  \n\n\n\n]",
+        "[para(1,5):\n]",
+        "[text(1,5):list\nthis::\n]",
         "[end-para:::True]",
-        "[li(6,6):7:   :]",
-        "[para(6,8):]",
-        "[text(6,8):Item 2b:]",
+        "[block-quote(3,5)::> \n>   > \n> \n]",
+        "[para(3,7):\n\n  ]",
+        "[text(3,7):good\nitem\nthat::\n\n]",
         "[end-para:::True]",
-        "[BLANK(7,1):]",
-        "[end-ulist:::True]",
+        "[end-block-quote:::True]",
+        "[BLANK(6,1):]",
         "[end-ulist:::True]",
         "[end-block-quote:::True]",
     ]
@@ -2682,10 +2744,10 @@ def test_extra_025a():
 this
 <blockquote>
 <p>good
-item</p>
+item
+that</p>
 </blockquote>
 </li>
-<li>that</li>
 </ul>
 </blockquote>"""
 
@@ -2694,8 +2756,7 @@ item</p>
 
 
 @pytest.mark.gfm
-@pytest.mark.skip
-def test_extra_025b():
+def test_extra_025bx():
     """
     TBD
     """
@@ -2742,30 +2803,72 @@ this
 
 
 @pytest.mark.gfm
-@pytest.mark.skip
-def test_extra_025c():
+def test_extra_025ba():
     """
     TBD
     """
 
     # Arrange
     source_markdown = """> + list
->   > good
+>   this
 >   > item
-> + that
+>   that
 """
     expected_tokens = [
         "[block-quote(1,1)::> \n> \n> ]",
-        "[ulist(1,3):+::4::  \n\n]",
+        "[ulist(1,3):+::4::  \n\n\n]",
         "[para(1,5):\n]",
         "[text(1,5):list\nthis::\n]",
         "[end-para:::True]",
-        "[block-quote(3,5)::> \n> ]",
-        "[para(3,7):]",
-        "[text(3,7):item:]",
+        "[block-quote(3,5)::> \n> \n]",
+        "[para(3,7):\n  ]",
+        "[text(3,7):item\nthat::\n]",
         "[end-para:::True]",
         "[end-block-quote:::True]",
-        "[li(4,3):4::]",
+        "[BLANK(5,1):]",
+        "[end-ulist:::True]",
+        "[end-block-quote:::True]",
+    ]
+    expected_gfm = """<blockquote>
+<ul>
+<li>list
+this
+<blockquote>
+<p>item
+that</p>
+</blockquote>
+</li>
+</ul>
+</blockquote>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens)
+
+
+@pytest.mark.gfm
+def test_extra_025cx():
+    """
+    TBD
+    """
+
+    # Arrange
+    source_markdown = """> + list
+>   > good
+>   > item
+> + that
+"""
+    expected_tokens = [
+        "[block-quote(1,1)::> \n> ]",
+        "[ulist(1,3):+::4::\n\n]",
+        "[para(1,5):]",
+        "[text(1,5):list:]",
+        "[end-para:::True]",
+        "[block-quote(2,5)::> \n>   > \n> ]",
+        "[para(2,7):\n]",
+        "[text(2,7):good\nitem::\n]",
+        "[end-para:::True]",
+        "[end-block-quote:::True]",
+        "[li(4,3):4:  :]",
         "[para(4,5):]",
         "[text(4,5):that:]",
         "[end-para:::True]",
@@ -2776,13 +2879,56 @@ def test_extra_025c():
     expected_gfm = """<blockquote>
 <ul>
 <li>list
-this
 <blockquote>
-<p>item</p>
+<p>good
+item</p>
 </blockquote>
 </li>
 <li>that</li>
 </ul>
+</blockquote>"""
+
+    # Act & Assert
+    act_and_assert(source_markdown, expected_gfm, expected_tokens, show_debug=False)
+
+
+@pytest.mark.gfm
+def test_extra_025ca():
+    """
+    TBD
+    """
+
+    # Arrange
+    source_markdown = """> + list
+>   > good
+>   > item
+>   that
+"""
+    expected_tokens = [
+        "[block-quote(1,1)::> \n> ]",
+        "[ulist(1,3):+::4::\n\n\n]",
+        "[para(1,5):]",
+        "[text(1,5):list:]",
+        "[end-para:::True]",
+        "[block-quote(2,5)::> \n>   > \n> \n]",
+        "[para(2,7):\n\n  ]",
+        "[text(2,7):good\nitem\nthat::\n\n]",
+        "[end-para:::True]",
+        "[end-block-quote:::True]",
+        "[BLANK(5,1):]",
+        "[end-ulist:::True]",
+        "[end-block-quote:::True]",
+    ]
+    expected_gfm = """<blockquote>
+<ul>
+<li>list
+<blockquote>
+<p>good
+item
+that</p>
+</blockquote>
+</li>
+</ul>
 </blockquote>"""
 
     # Act & Assert
diff --git a/test/test_md027.py b/test/test_md027.py
index 583baaf2..aa3c7b85 100644
--- a/test/test_md027.py
+++ b/test/test_md027.py
@@ -1243,7 +1243,6 @@ def test_md027_good_block_quote_unordered_list_block_quote_text():
 
 
 @pytest.mark.rules
-@pytest.mark.skip
 def test_md027_bad_block_quote_unordered_list_block_quote_text_first():
     """
     Block quote with an aligned multiline list and an aligned block
@@ -1260,8 +1259,11 @@ def test_md027_bad_block_quote_unordered_list_block_quote_text_first():
         "test/resources/rules/md027/bad_block_quote_unordered_list_block_quote_text_first.md",
     ]
 
-    expected_return_code = 0
-    expected_output = ""
+    expected_return_code = 1
+    expected_output = (
+        "test/resources/rules/md027/bad_block_quote_unordered_list_block_quote_text_first.md:3:3: "
+        + "MD027: Multiple spaces after blockquote symbol (no-multiple-space-blockquote)"
+    )
     expected_error = ""
 
     # Act
@@ -1274,7 +1276,6 @@ def test_md027_bad_block_quote_unordered_list_block_quote_text_first():
 
 
 @pytest.mark.rules
-@pytest.mark.skip
 def test_md027_bad_block_quote_unordered_list_block_quote_text_last():
     """
     Block quote with an aligned multiline list and an aligned block
@@ -1290,8 +1291,11 @@ def test_md027_bad_block_quote_unordered_list_block_quote_text_last():
         "test/resources/rules/md027/bad_block_quote_unordered_list_block_quote_text_last.md",
     ]
 
-    expected_return_code = 0
-    expected_output = ""
+    expected_return_code = 1
+    expected_output = (
+        "test/resources/rules/md027/bad_block_quote_unordered_list_block_quote_text_last.md:4:7: "
+        + "MD027: Multiple spaces after blockquote symbol (no-multiple-space-blockquote)"
+    )
     expected_error = ""
 
     # Act
diff --git a/test/transform_to_markdown.py b/test/transform_to_markdown.py
index 12c283c7..bcbbbe21 100644
--- a/test/transform_to_markdown.py
+++ b/test/transform_to_markdown.py
@@ -1109,6 +1109,7 @@ class TransformToMarkdown:
         print(
             f"rls>>previous_token>>{ParserHelper.make_value_visible(previous_token)}<<"
         )
+
         print(
             f"rls>>self.container_token_stack>>{ParserHelper.make_value_visible(self.container_token_stack)}<<"
         )
@@ -1143,6 +1144,7 @@ class TransformToMarkdown:
             deeper_containing_block_quote_token = containing_block_quote_token
             containing_block_quote_token = None
 
+        did_container_start_midline = False
         if previous_token.is_list_start:
             print("rlspt>>is_list_start")
             (
@@ -1175,17 +1177,20 @@ class TransformToMarkdown:
                 previous_indent,
                 extracted_whitespace,
                 post_adjust_whitespace,
+                did_container_start_midline,
             ) = self.__rehydrate_list_start_contained_in_list(
                 current_token,
                 containing_list_token,
                 deeper_containing_block_quote_token,
                 extracted_whitespace,
+                previous_token,
             )
         return (
             previous_indent,
             extracted_whitespace,
             was_within_block_token,
             post_adjust_whitespace,
+            did_container_start_midline,
         )
 
     @classmethod
@@ -1247,6 +1252,7 @@ class TransformToMarkdown:
 
         return True, previous_indent
 
+    # pylint: disable=too-many-arguments
     @classmethod
     def __rehydrate_list_start_contained_in_list(
         cls,
@@ -1254,6 +1260,7 @@ class TransformToMarkdown:
         containing_list_token,
         deeper_containing_block_quote_token,
         extracted_whitespace,
+        previous_token,
     ):
 
         print(
@@ -1267,39 +1274,63 @@ class TransformToMarkdown:
             f"adj->extracted_whitespace>>:{ParserHelper.make_value_visible(extracted_whitespace)}:<<"
         )
         block_quote_leading_space_length = 0
+        starting_whitespace = ""
+        did_container_start_midline = False
         if deeper_containing_block_quote_token:
-            print(
-                "adj->deeper_containing_block_quote_token.line_number>>:"
-                + f"{deeper_containing_block_quote_token.line_number}:<<"
-            )
-            print(
-                "adj->current_token.line_number>>:" + f"{current_token.line_number}:<<"
-            )
-            line_number_delta = (
-                current_token.line_number
-                - deeper_containing_block_quote_token.line_number
-            )
-            print("index:" + str(line_number_delta))
-            split_leading_spaces = (
-                deeper_containing_block_quote_token.leading_spaces.split(
-                    ParserHelper.newline_character
+            if (
+                previous_token
+                and previous_token.is_end_token
+                and previous_token.start_markdown_token.is_block_quote_start
+            ):
+                print(
+                    ">>"
+                    + ParserHelper.make_value_visible(
+                        previous_token.start_markdown_token
+                    )
                 )
-            )
-            print(
-                "split_leading_spaces:"
-                + ParserHelper.make_value_visible(split_leading_spaces)
-            )
-            block_quote_leading_space = split_leading_spaces[line_number_delta]
+                split_leading_spaces = (
+                    previous_token.start_markdown_token.leading_spaces.split(
+                        ParserHelper.newline_character
+                    )
+                )
+                block_quote_leading_space = split_leading_spaces[-1]
+                starting_whitespace = block_quote_leading_space
+                did_container_start_midline = True
+            else:
+                print(
+                    "adj->deeper_containing_block_quote_token.line_number>>:"
+                    + f"{deeper_containing_block_quote_token.line_number}:<<"
+                )
+                print(
+                    "adj->current_token.line_number>>:"
+                    + f"{current_token.line_number}:<<"
+                )
+                line_number_delta = (
+                    current_token.line_number
+                    - deeper_containing_block_quote_token.line_number
+                )
+                print("index:" + str(line_number_delta))
+                split_leading_spaces = (
+                    deeper_containing_block_quote_token.leading_spaces.split(
+                        ParserHelper.newline_character
+                    )
+                )
+                print(
+                    "split_leading_spaces:"
+                    + ParserHelper.make_value_visible(split_leading_spaces)
+                )
+                block_quote_leading_space = split_leading_spaces[line_number_delta]
             print(
                 "block_quote_leading_space:"
                 + ParserHelper.make_value_visible(block_quote_leading_space)
+                + ":"
             )
 
             block_quote_leading_space_length = len(block_quote_leading_space)
         print(
             f"adj->block_quote_leading_space_length>>:{block_quote_leading_space_length}:<<"
         )
-        post_adjust_whitespace = "".ljust(
+        post_adjust_whitespace = starting_whitespace.rjust(
             containing_list_token.indent_level - block_quote_leading_space_length, " "
         )
 
@@ -1308,7 +1339,6 @@ class TransformToMarkdown:
             len(current_token.extracted_whitespace) + block_quote_leading_space_length
         )
         print(f"adj->len(ws)>>:{white_space_length}:<<")
-        # assert len(current_token.extracted_whitespace) == previous_indent
         extracted_whitespace = (
             "".ljust(white_space_length - previous_indent, " ")
             if white_space_length > previous_indent
@@ -1319,7 +1349,14 @@ class TransformToMarkdown:
             f"adj->extracted_whitespace>>:{ParserHelper.make_value_visible(extracted_whitespace)}:<<"
         )
         print(f"adj->post_adjust_whitespace>>:{post_adjust_whitespace}:<<")
-        return previous_indent, extracted_whitespace, post_adjust_whitespace
+        return (
+            previous_indent,
+            extracted_whitespace,
+            post_adjust_whitespace,
+            did_container_start_midline,
+        )
+
+    # pylint: enable=too-many-arguments
 
     def __rehydrate_list_start(
         self, current_token, previous_token, next_token, transformed_data
@@ -1336,6 +1373,7 @@ class TransformToMarkdown:
                 extracted_whitespace,
                 was_within_block_token,
                 post_adjust_whitespace,
+                _,
             ) = self.__rehydrate_list_start_previous_token(
                 current_token, previous_token, extracted_whitespace
             )
@@ -1498,6 +1536,42 @@ class TransformToMarkdown:
                 adjustment_since_newline = len(leading_space)
         return adjustment_since_newline
 
+    @classmethod
+    def __rehydrate_next_list_item_blank_line(
+        cls, start_sequence, current_token, next_token
+    ):
+        print(f">>next_token.column_number>>:{next_token.column_number}:<<")
+        print(f">>current_token.column_number>>:{current_token.column_number}:<<")
+        start_content_length = 1
+        if current_token.list_start_content:
+            start_content_length += len(current_token.list_start_content)
+        new_column_number = (
+            next_token.column_number
+            - current_token.column_number
+            - start_content_length
+        )
+        start_sequence += ParserHelper.repeat_string(" ", new_column_number)
+        return start_sequence
+
+    def __rehydrate_next_list_item_not_blank_line(
+        self, start_sequence, did_container_start_midline, adjustment_since_newline
+    ):
+        if did_container_start_midline:
+            start_sequence = start_sequence.ljust(
+                self.container_token_stack[-1].indent_level, " "
+            )
+        else:
+            calculated_indent = (
+                self.container_token_stack[-1].indent_level - adjustment_since_newline
+            )
+            print(
+                f"rnli->calculated_indent>{calculated_indent} = "
+                + f"indent_level={self.container_token_stack[-1].indent_level} - "
+                + f"adjustment_since_newline{adjustment_since_newline}"
+            )
+            start_sequence = start_sequence.ljust(calculated_indent, " ")
+        return start_sequence
+
     def __rehydrate_next_list_item(
         self, current_token, previous_token, next_token, transformed_data
     ):
@@ -1520,12 +1594,14 @@ class TransformToMarkdown:
         print(f"rnli->adjustment_since_newline>:{adjustment_since_newline}:")
         print(f"rnli->extracted_whitespace>:{extracted_whitespace}:")
 
+        did_container_start_midline = False
         if previous_token:
             (
                 previous_indent,
                 extracted_whitespace2,
                 _,
                 post_adjust_whitespace,
+                did_container_start_midline,
             ) = self.__rehydrate_list_start_previous_token(
                 current_token, previous_token, extracted_whitespace
             )
@@ -1538,32 +1614,31 @@ class TransformToMarkdown:
         adjustment_since_newline = self.__recalc_adjustment_since_newline(
             adjustment_since_newline
         )
+        # assert len(post_adjust_whitespace) == adjustment_since_newline
+
+        whitespace_to_use = (
+            post_adjust_whitespace
+            if did_container_start_midline
+            else extracted_whitespace
+        )
+        print(f"rnli->whitespace_to_use>:{whitespace_to_use}:")
 
         print(f"rnli->adjustment_since_newline>:{adjustment_since_newline}:")
         print(f"rnli->extracted_whitespace>:{extracted_whitespace}:")
         start_sequence = (
-            f"{extracted_whitespace}{current_token.list_start_content}"
+            f"{whitespace_to_use}{current_token.list_start_content}"
             + f"{self.container_token_stack[-1].list_start_sequence}"
         )
 
         print(f"rnli->start_sequence>:{start_sequence}:")
-        if not next_token.is_blank_line:
-            start_sequence = start_sequence.ljust(
-                self.container_token_stack[-1].indent_level - adjustment_since_newline,
-                " ",
+        if next_token.is_blank_line:
+            start_sequence = self.__rehydrate_next_list_item_blank_line(
+                start_sequence, current_token, next_token
             )
         else:
-            print(f">>next_token.column_number>>:{next_token.column_number}:<<")
-            print(f">>current_token.column_number>>:{current_token.column_number}:<<")
-            start_content_length = 1
-            if current_token.list_start_content:
-                start_content_length += len(current_token.list_start_content)
-            new_column_number = (
-                next_token.column_number
-                - current_token.column_number
-                - start_content_length
+            start_sequence = self.__rehydrate_next_list_item_not_blank_line(
+                start_sequence, did_container_start_midline, adjustment_since_newline
             )
-            start_sequence += ParserHelper.repeat_string(" ", new_column_number)
         print(f"rnli->start_sequence>:{start_sequence}:")
 
         return start_sequence