diff --git a/markdown_code_runner.py b/markdown_code_runner.py index cac33da..ebcb51e 100644 --- a/markdown_code_runner.py +++ b/markdown_code_runner.py @@ -235,6 +235,7 @@ class ProcessingState: new_lines: list[str] = field(default_factory=list) backtick_options: dict[str, Any] = field(default_factory=dict) backtick_standardize: bool = True + indent: str = "" # Indentation prefix of current code block def process_line(self, line: str, *, verbose: bool = False) -> None: """Process a line of the Markdown file.""" @@ -269,6 +270,7 @@ def _process_start_markers( self.output = None self.backtick_options = _extract_backtick_options(line) self.section, _ = marker_name.rsplit(":", 1) # type: ignore[assignment] + self.indent = self._get_indent(line) # Standardize backticks if needed if ( @@ -280,6 +282,11 @@ def _process_start_markers( return line return None + @staticmethod + def _get_indent(line: str) -> str: + """Extract leading whitespace from a line.""" + return line[: len(line) - len(line.lstrip())] + def _process_output_start(self, line: str) -> None: self.section = "output" if not self.skip_code_block: @@ -287,9 +294,11 @@ def _process_output_start(self, line: str) -> None: self.output, list, ), f"Output must be a list, not {type(self.output)}, line: {line}" - # Trim trailing whitespace from output lines - trimmed_output = [line.rstrip() for line in self.output] - self.new_lines.extend([line, MARKERS["warning"], *trimmed_output]) + indent = self._get_indent(line) + trimmed_output = [ + indent + ol.rstrip() if ol.strip() else "" for ol in self.output + ] + self.new_lines.extend([line, indent + MARKERS["warning"], *trimmed_output]) else: self.original_output.append(line) @@ -301,6 +310,12 @@ def _process_output_end(self) -> None: self.original_output = [] self.output = None # Reset output after processing end of the output section + def _strip_indent(self, line: str) -> str: + """Strip the code block's indentation prefix from a line.""" + if self.indent and line.startswith(self.indent): + return line[len(self.indent) :] + return line + def _process_code( self, line: str, @@ -322,8 +337,13 @@ def _process_code( self.section = "normal" self.code = [] self.backtick_options = {} + self.indent = "" else: - self.code.append(remove_md_comment(line) if remove_comment else line) + # remove_md_comment already strips whitespace; for backticks, strip indent + code_line = ( + remove_md_comment(line) if remove_comment else self._strip_indent(line) + ) + self.code.append(code_line) def _process_comment_code(self, line: str, *, verbose: bool) -> None: _, language = self.section.rsplit(":", 1) diff --git a/tests/test_main_app.py b/tests/test_main_app.py index 2aebdf2..81dd9d3 100644 --- a/tests/test_main_app.py +++ b/tests/test_main_app.py @@ -881,3 +881,79 @@ def test_trailing_whitespace_trimming() -> None: assert output_section_hidden[3] == "Final line" assert output_section_hidden[4] == "" # Line with only spaces becomes empty assert output_section_hidden[5] == "" # Trailing empty line preserved + + +def test_indented_code_blocks() -> None: + """Test that indented code blocks (e.g., in list items) work correctly.""" + # Test case 1: Indented backtick code block (4 spaces, like in a list) + input_lines = [ + "1. List item:", + "", + " ```python markdown-code-runner", + " print('hello')", + " ```", + " ", + " old output", + " ", + ] + expected_output = [ + "1. List item:", + "", + " ```python markdown-code-runner", + " print('hello')", + " ```", + " ", + " " + MARKERS["warning"], + " hello", + "", + " ", + ] + assert_process(input_lines, expected_output, backtick_standardize=False) + + # Test case 2: Indented code with internal indentation (Python function) + input_lines = [ + "1. Example:", + "", + " ```python markdown-code-runner", + " def foo():", + " return 42", + " print(foo())", + " ```", + " ", + " ", + ] + expected_output = [ + "1. Example:", + "", + " ```python markdown-code-runner", + " def foo():", + " return 42", + " print(foo())", + " ```", + " ", + " " + MARKERS["warning"], + " 42", + "", + " ", + ] + assert_process(input_lines, expected_output, backtick_standardize=False) + + # Test case 3: Indented bash code block + input_lines = [ + " ```bash markdown-code-runner", + ' echo "indented bash"', + " ```", + " ", + " ", + ] + expected_output = [ + " ```bash markdown-code-runner", + ' echo "indented bash"', + " ```", + " ", + " " + MARKERS["warning"], + " indented bash", + "", + " ", + ] + assert_process(input_lines, expected_output, backtick_standardize=False)