Skip to content

Commit b56bf5c

Browse files
authored
Support indented code blocks (fixes #11) (#47)
* Support indented code blocks (fixes #11) Code blocks inside list items or other indented contexts now work correctly. The indentation is stripped from code before execution and re-added to output. * Simplify: extract _get_indent method, inline list comprehension * Reuse _get_indent in _process_start_markers
1 parent 9cad8b7 commit b56bf5c

File tree

2 files changed

+100
-4
lines changed

2 files changed

+100
-4
lines changed

markdown_code_runner.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ class ProcessingState:
235235
new_lines: list[str] = field(default_factory=list)
236236
backtick_options: dict[str, Any] = field(default_factory=dict)
237237
backtick_standardize: bool = True
238+
indent: str = "" # Indentation prefix of current code block
238239

239240
def process_line(self, line: str, *, verbose: bool = False) -> None:
240241
"""Process a line of the Markdown file."""
@@ -269,6 +270,7 @@ def _process_start_markers(
269270
self.output = None
270271
self.backtick_options = _extract_backtick_options(line)
271272
self.section, _ = marker_name.rsplit(":", 1) # type: ignore[assignment]
273+
self.indent = self._get_indent(line)
272274

273275
# Standardize backticks if needed
274276
if (
@@ -280,16 +282,23 @@ def _process_start_markers(
280282
return line
281283
return None
282284

285+
@staticmethod
286+
def _get_indent(line: str) -> str:
287+
"""Extract leading whitespace from a line."""
288+
return line[: len(line) - len(line.lstrip())]
289+
283290
def _process_output_start(self, line: str) -> None:
284291
self.section = "output"
285292
if not self.skip_code_block:
286293
assert isinstance(
287294
self.output,
288295
list,
289296
), f"Output must be a list, not {type(self.output)}, line: {line}"
290-
# Trim trailing whitespace from output lines
291-
trimmed_output = [line.rstrip() for line in self.output]
292-
self.new_lines.extend([line, MARKERS["warning"], *trimmed_output])
297+
indent = self._get_indent(line)
298+
trimmed_output = [
299+
indent + ol.rstrip() if ol.strip() else "" for ol in self.output
300+
]
301+
self.new_lines.extend([line, indent + MARKERS["warning"], *trimmed_output])
293302
else:
294303
self.original_output.append(line)
295304

@@ -301,6 +310,12 @@ def _process_output_end(self) -> None:
301310
self.original_output = []
302311
self.output = None # Reset output after processing end of the output section
303312

313+
def _strip_indent(self, line: str) -> str:
314+
"""Strip the code block's indentation prefix from a line."""
315+
if self.indent and line.startswith(self.indent):
316+
return line[len(self.indent) :]
317+
return line
318+
304319
def _process_code(
305320
self,
306321
line: str,
@@ -322,8 +337,13 @@ def _process_code(
322337
self.section = "normal"
323338
self.code = []
324339
self.backtick_options = {}
340+
self.indent = ""
325341
else:
326-
self.code.append(remove_md_comment(line) if remove_comment else line)
342+
# remove_md_comment already strips whitespace; for backticks, strip indent
343+
code_line = (
344+
remove_md_comment(line) if remove_comment else self._strip_indent(line)
345+
)
346+
self.code.append(code_line)
327347

328348
def _process_comment_code(self, line: str, *, verbose: bool) -> None:
329349
_, language = self.section.rsplit(":", 1)

tests/test_main_app.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,3 +881,79 @@ def test_trailing_whitespace_trimming() -> None:
881881
assert output_section_hidden[3] == "Final line"
882882
assert output_section_hidden[4] == "" # Line with only spaces becomes empty
883883
assert output_section_hidden[5] == "" # Trailing empty line preserved
884+
885+
886+
def test_indented_code_blocks() -> None:
887+
"""Test that indented code blocks (e.g., in list items) work correctly."""
888+
# Test case 1: Indented backtick code block (4 spaces, like in a list)
889+
input_lines = [
890+
"1. List item:",
891+
"",
892+
" ```python markdown-code-runner",
893+
" print('hello')",
894+
" ```",
895+
" <!-- OUTPUT:START -->",
896+
" old output",
897+
" <!-- OUTPUT:END -->",
898+
]
899+
expected_output = [
900+
"1. List item:",
901+
"",
902+
" ```python markdown-code-runner",
903+
" print('hello')",
904+
" ```",
905+
" <!-- OUTPUT:START -->",
906+
" " + MARKERS["warning"],
907+
" hello",
908+
"",
909+
" <!-- OUTPUT:END -->",
910+
]
911+
assert_process(input_lines, expected_output, backtick_standardize=False)
912+
913+
# Test case 2: Indented code with internal indentation (Python function)
914+
input_lines = [
915+
"1. Example:",
916+
"",
917+
" ```python markdown-code-runner",
918+
" def foo():",
919+
" return 42",
920+
" print(foo())",
921+
" ```",
922+
" <!-- OUTPUT:START -->",
923+
" <!-- OUTPUT:END -->",
924+
]
925+
expected_output = [
926+
"1. Example:",
927+
"",
928+
" ```python markdown-code-runner",
929+
" def foo():",
930+
" return 42",
931+
" print(foo())",
932+
" ```",
933+
" <!-- OUTPUT:START -->",
934+
" " + MARKERS["warning"],
935+
" 42",
936+
"",
937+
" <!-- OUTPUT:END -->",
938+
]
939+
assert_process(input_lines, expected_output, backtick_standardize=False)
940+
941+
# Test case 3: Indented bash code block
942+
input_lines = [
943+
" ```bash markdown-code-runner",
944+
' echo "indented bash"',
945+
" ```",
946+
" <!-- OUTPUT:START -->",
947+
" <!-- OUTPUT:END -->",
948+
]
949+
expected_output = [
950+
" ```bash markdown-code-runner",
951+
' echo "indented bash"',
952+
" ```",
953+
" <!-- OUTPUT:START -->",
954+
" " + MARKERS["warning"],
955+
" indented bash",
956+
"",
957+
" <!-- OUTPUT:END -->",
958+
]
959+
assert_process(input_lines, expected_output, backtick_standardize=False)

0 commit comments

Comments
 (0)