Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
venv/
venv-*
env/
ENV/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Misc
.DS_Store
.env
.coverage
htmlcov/
104 changes: 78 additions & 26 deletions markdown_code_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
```bash markdown-code-runner
echo "Hello, world!"
```
Which will similarly print the output of the code block between next to the output markers.
Which will similarly print the output of the code block between the next output markers.

"""

Expand Down Expand Up @@ -163,22 +163,17 @@ def _bold(text: str) -> str:

def _extract_backtick_options(line: str) -> dict[str, str]:
"""Extract extra information from a line."""
if "```" not in line:
match = re.search(r"```(?P<language>\w+)", line)
if not match:
return {}
language_pattern = r"```(?P<language>\w+) markdown-code-runner"
extra_pattern = r"(?P<key>\w+)=(?P<value>\S+)"

language_match = re.search(language_pattern, line)
assert language_match is not None
language = language_match.group("language")
result = {"language": language}
result = {"language": match.group("language")}

extra_str = line[language_match.end() :]
extra_matches = re.finditer(extra_pattern, extra_str)

for match in extra_matches:
key, value = match.group("key"), match.group("value")
result[key] = value
# Extract options after markdown-code-runner
if "markdown-code-runner" in line:
extra_str = line[match.end() :]
for option_match in re.finditer(r"(?P<key>\w+)=(?P<value>\S+)", extra_str):
result[option_match.group("key")] = option_match.group("value")

return result

Expand All @@ -203,6 +198,7 @@ class ProcessingState:
output: list[str] | None = None
new_lines: list[str] = field(default_factory=list)
backtick_options: dict[str, Any] = field(default_factory=dict)
backtick_standardize: bool = True

def process_line(self, line: str, *, verbose: bool = False) -> None:
"""Process a line of the Markdown file."""
Expand All @@ -219,19 +215,34 @@ def process_line(self, line: str, *, verbose: bool = False) -> None:
elif self.section == "output":
self.original_output.append(line)
else:
self._process_start_markers(line)
processed_line = self._process_start_markers(line, verbose=verbose)
if processed_line is not None:
line = processed_line

if self.section != "output":
self.new_lines.append(line)

def _process_start_markers(self, line: str) -> None:
for marker in MARKERS:
if marker.endswith(":start") and is_marker(line, marker):
def _process_start_markers(
self,
line: str,
verbose: bool = False, # noqa: FBT001, FBT002, ARG002
) -> str | None:
for marker_name in MARKERS:
if marker_name.endswith(":start") and is_marker(line, marker_name):
# reset output in case previous output wasn't displayed
self.output = None
self.backtick_options = _extract_backtick_options(line)
self.section, _ = marker.rsplit(":", 1) # type: ignore[assignment]
return
self.section, _ = marker_name.rsplit(":", 1) # type: ignore[assignment]

# Standardize backticks if needed
if (
marker_name == "code:backticks:start"
and self.backtick_standardize
and "markdown-code-runner" in line
):
return re.sub(r"\smarkdown-code-runner.*", "", line)
return line
return None

def _process_output_start(self, line: str) -> None:
self.section = "output"
Expand Down Expand Up @@ -292,7 +303,12 @@ def _process_backtick_code(self, line: str, *, verbose: bool) -> None:
self._process_code(line, "code:backticks:end", language, verbose=verbose)


def process_markdown(content: list[str], *, verbose: bool = False) -> list[str]:
def process_markdown(
content: list[str],
*,
verbose: bool = False,
backtick_standardize: bool = True,
) -> list[str]:
"""Executes code blocks in a list of Markdown-formatted strings and returns the modified list.

Parameters
Expand All @@ -301,6 +317,8 @@ def process_markdown(content: list[str], *, verbose: bool = False) -> list[str]:
A list of Markdown-formatted strings.
verbose
If True, print every line that is processed.
backtick_standardize
If True, clean up markdown-code-runner string from backtick code blocks.

Returns
-------
Expand All @@ -309,14 +327,13 @@ def process_markdown(content: list[str], *, verbose: bool = False) -> list[str]:

"""
assert isinstance(content, list), "Input must be a list"
state = ProcessingState()
state = ProcessingState(backtick_standardize=backtick_standardize)

for i, line in enumerate(content):
if verbose:
nr = _bold(f"line {i:4d}")
print(f"{nr}: {line}")
state.process_line(line, verbose=verbose)

return state.new_lines


Expand All @@ -325,15 +342,33 @@ def update_markdown_file(
output_filepath: Path | str | None = None,
*,
verbose: bool = False,
backtick_standardize: bool = True,
) -> None:
"""Rewrite a Markdown file by executing and updating code blocks."""
"""Rewrite a Markdown file by executing and updating code blocks.

Parameters
----------
input_filepath : Path | str
Path to the input Markdown file.
output_filepath : Path | str | None
Path to the output Markdown file. If None, overwrites input file.
verbose : bool
If True, print every line that is processed.
backtick_standardize : bool
If True, clean up markdown-code-runner string from backtick code blocks.

"""
if isinstance(input_filepath, str): # pragma: no cover
input_filepath = Path(input_filepath)
with input_filepath.open() as f:
original_lines = [line.rstrip("\n") for line in f.readlines()]
if verbose:
print(f"Processing input file: {input_filepath}")
new_lines = process_markdown(original_lines, verbose=verbose)
new_lines = process_markdown(
original_lines,
verbose=verbose,
backtick_standardize=backtick_standardize,
)
updated_content = "\n".join(new_lines).rstrip() + "\n"
if verbose:
print(f"Writing output to: {output_filepath}")
Expand Down Expand Up @@ -375,12 +410,29 @@ def main() -> None:
action="version",
version=f"%(prog)s {__version__}",
)
parser.add_argument(
"--no-backtick-standardize",
action="store_true",
help="Disable backtick standardization (default: enabled for separate output files, disabled for in-place)",
default=False,
)

args = parser.parse_args()

input_filepath = Path(args.input)
output_filepath = Path(args.output) if args.output is not None else input_filepath
update_markdown_file(input_filepath, output_filepath, verbose=args.verbose)

# Determine backtick standardization
backtick_standardize = (
False if args.no_backtick_standardize else args.output is not None
)

update_markdown_file(
input_filepath,
output_filepath,
verbose=args.verbose,
backtick_standardize=backtick_standardize,
)


if __name__ == "__main__":
Expand Down
48 changes: 48 additions & 0 deletions tests/test_backtick_stdz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Backtick Standardization Test

This file tests various backtick code block scenarios.

## Basic Code Block
A simple Python code block with markdown-code-runner:
Currently no options are supported for backtick code blocks. May be used in the future.

```python markdown-code-runner filename=test1.py
print("Basic test")
```

## Code Block with Multiple Options
Testing multiple options in the backtick header:

```javascript markdown-code-runner filename=test2.js debug=true skip=false
console.log("Multiple options test")
```

## Language-only Block
This block should remain unchanged during standardization:

```rust
fn main() {
println!("No markdown-code-runner");
}
```

## Complex Options Block
Testing complex options and spacing:

```python markdown-code-runner filename=test3.py debug=true skip=false
print("Testing spaces in options")
```

## Empty Language Block
Testing block with no language:

```markdown-code-runner filename=test4.txt
Just some plain text
```

## Mixed Content Block
Testing with additional content after options:

```python markdown-code-runner filename=test5.py some random text here
print("Mixed content test")
```
Loading
Loading