Skip to content

Commit 08fd1d6

Browse files
committed
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.
1 parent 9cad8b7 commit 08fd1d6

File tree

2 files changed

+116
-18
lines changed

2 files changed

+116
-18
lines changed

markdown_code_runner.py

Lines changed: 40 additions & 18 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."""
@@ -264,20 +265,23 @@ def _process_start_markers(
264265
verbose: bool = False, # noqa: FBT001, FBT002, ARG002
265266
) -> str | None:
266267
for marker_name in MARKERS:
267-
if marker_name.endswith(":start") and is_marker(line, marker_name):
268-
# reset output in case previous output wasn't displayed
269-
self.output = None
270-
self.backtick_options = _extract_backtick_options(line)
271-
self.section, _ = marker_name.rsplit(":", 1) # type: ignore[assignment]
272-
273-
# Standardize backticks if needed
274-
if (
275-
marker_name == "code:backticks:start"
276-
and self.backtick_standardize
277-
and "markdown-code-runner" in line
278-
):
279-
return re.sub(r"\smarkdown-code-runner.*", "", line)
280-
return line
268+
if marker_name.endswith(":start"):
269+
match = is_marker(line, marker_name)
270+
if match:
271+
# reset output in case previous output wasn't displayed
272+
self.output = None
273+
self.backtick_options = _extract_backtick_options(line)
274+
self.section, _ = marker_name.rsplit(":", 1) # type: ignore[assignment]
275+
self.indent = match.group("spaces")
276+
277+
# Standardize backticks if needed
278+
if (
279+
marker_name == "code:backticks:start"
280+
and self.backtick_standardize
281+
and "markdown-code-runner" in line
282+
):
283+
return re.sub(r"\smarkdown-code-runner.*", "", line)
284+
return line
281285
return None
282286

283287
def _process_output_start(self, line: str) -> None:
@@ -287,9 +291,16 @@ def _process_output_start(self, line: str) -> None:
287291
self.output,
288292
list,
289293
), 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])
294+
# Extract indent from OUTPUT:START line
295+
output_indent = line[: len(line) - len(line.lstrip())]
296+
297+
def _add_indent(s: str) -> str:
298+
stripped = s.rstrip()
299+
return output_indent + stripped if stripped else ""
300+
301+
trimmed_output = [_add_indent(ol) for ol in self.output]
302+
indented_warning = output_indent + MARKERS["warning"]
303+
self.new_lines.extend([line, indented_warning, *trimmed_output])
293304
else:
294305
self.original_output.append(line)
295306

@@ -301,6 +312,12 @@ def _process_output_end(self) -> None:
301312
self.original_output = []
302313
self.output = None # Reset output after processing end of the output section
303314

315+
def _strip_indent(self, line: str) -> str:
316+
"""Strip the code block's indentation prefix from a line."""
317+
if self.indent and line.startswith(self.indent):
318+
return line[len(self.indent) :]
319+
return line
320+
304321
def _process_code(
305322
self,
306323
line: str,
@@ -322,8 +339,13 @@ def _process_code(
322339
self.section = "normal"
323340
self.code = []
324341
self.backtick_options = {}
342+
self.indent = ""
325343
else:
326-
self.code.append(remove_md_comment(line) if remove_comment else line)
344+
# remove_md_comment already strips whitespace; for backticks, strip indent
345+
code_line = (
346+
remove_md_comment(line) if remove_comment else self._strip_indent(line)
347+
)
348+
self.code.append(code_line)
327349

328350
def _process_comment_code(self, line: str, *, verbose: bool) -> None:
329351
_, 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)