Restore best-effort Markdown shell-fence formatting (#796)
This commit is contained in:
+1
-1
@@ -28,4 +28,4 @@
|
||||
# ├── test_summarize_pr.py
|
||||
# └── ...
|
||||
|
||||
__version__ = "0.2.22"
|
||||
__version__ = "0.2.23"
|
||||
|
||||
@@ -11,10 +11,16 @@ from pathlib import Path
|
||||
|
||||
|
||||
def extract_code_blocks(markdown_content):
|
||||
"""Extract Python code blocks from Markdown content using regex pattern matching."""
|
||||
"""Extracts Python and Bash code blocks from Markdown content using regex pattern matching."""
|
||||
# Python code blocks
|
||||
py_pattern = r"^( *)```(?:python|py|\{[ ]*\.py[ ]*\.annotate[ ]*\})\n(.*?)\n\1```"
|
||||
py_code_blocks = re.compile(py_pattern, re.DOTALL | re.MULTILINE).findall(markdown_content)
|
||||
return {"python": py_code_blocks}
|
||||
|
||||
# Bash code blocks
|
||||
bash_pattern = r"^( *)```(?:bash|sh|shell)\n(.*?)\n\1```"
|
||||
bash_code_blocks = re.compile(bash_pattern, re.DOTALL | re.MULTILINE).findall(markdown_content)
|
||||
|
||||
return {"python": py_code_blocks, "bash": bash_code_blocks}
|
||||
|
||||
|
||||
def remove_indentation(code_block, num_spaces):
|
||||
@@ -90,24 +96,51 @@ def format_code_with_ruff(temp_dir):
|
||||
print(f"ERROR running Python docstring formatter ❌ {e}")
|
||||
|
||||
|
||||
def format_bash_with_prettier(temp_dir):
|
||||
"""Formats bash script files in the specified directory using prettier."""
|
||||
if not next(Path(temp_dir).rglob("*.sh"), None):
|
||||
return
|
||||
|
||||
try:
|
||||
# Run prettier with explicit config path
|
||||
result = subprocess.run(
|
||||
"npx prettier --write --print-width=120 --plugin=$(npm root -g)/prettier-plugin-sh/lib/index.cjs ./**/*.sh",
|
||||
shell=True, # must use shell=True to expand internal $(cmd)
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"ERROR running prettier-plugin-sh ❌ {result.stderr}")
|
||||
else:
|
||||
print("Completed bash formatting ✅")
|
||||
except Exception as e:
|
||||
print(f"ERROR running prettier-plugin-sh ❌ {e}")
|
||||
|
||||
|
||||
def process_markdown_string(
|
||||
markdown_content: str,
|
||||
process_python: bool = True,
|
||||
process_bash: bool = True,
|
||||
verbose: bool = False,
|
||||
) -> str:
|
||||
"""Format Python code blocks inside a Markdown string."""
|
||||
"""Formats Python/Bash code blocks inside a Markdown string."""
|
||||
formatted_markdown = markdown_content
|
||||
with tempfile.TemporaryDirectory() as temp_dir_name:
|
||||
temp_dir = Path(temp_dir_name)
|
||||
temp_md = temp_dir / "input.md"
|
||||
temp_md.write_text(markdown_content, encoding="utf-8")
|
||||
|
||||
markdown_snapshot, temp_files = process_markdown_file(temp_md, temp_dir, process_python, verbose)
|
||||
markdown_snapshot, temp_files = process_markdown_file(temp_md, temp_dir, process_python, process_bash, verbose)
|
||||
if markdown_snapshot is None or temp_files is None:
|
||||
return formatted_markdown
|
||||
|
||||
if process_python and temp_files:
|
||||
python_files_exist = process_python and any(code_type == "python" for _, _, _, code_type in temp_files)
|
||||
bash_files_exist = process_bash and any(code_type == "bash" for _, _, _, code_type in temp_files)
|
||||
|
||||
if python_files_exist:
|
||||
format_code_with_ruff(temp_dir)
|
||||
if bash_files_exist:
|
||||
format_bash_with_prettier(temp_dir)
|
||||
|
||||
update_markdown_file(temp_md, markdown_snapshot, temp_files)
|
||||
formatted_markdown = temp_md.read_text(encoding="utf-8")
|
||||
@@ -115,34 +148,44 @@ def process_markdown_string(
|
||||
return formatted_markdown
|
||||
|
||||
|
||||
def generate_temp_filename(file_path, index):
|
||||
def generate_temp_filename(file_path, index, code_type):
|
||||
"""Creates unique temp filename with full path info for debugging."""
|
||||
stem = file_path.stem
|
||||
code_letter = code_type[0] # 'p' for python, 'b' for bash
|
||||
path_part = str(file_path.parent).replace("/", "_").replace("\\", "_").replace(" ", "-")
|
||||
hash_val = hashlib.md5(f"{file_path}_{index}".encode()).hexdigest()[:6]
|
||||
filename = f"{stem}_{path_part}_p{index}_{hash_val}.py"
|
||||
ext = ".py" if code_type == "python" else ".sh"
|
||||
filename = f"{stem}_{path_part}_{code_letter}{index}_{hash_val}{ext}"
|
||||
return re.sub(r"[^\w\-.]", "_", filename)
|
||||
|
||||
|
||||
def process_markdown_file(file_path, temp_dir, process_python=True, verbose=False):
|
||||
def process_markdown_file(file_path, temp_dir, process_python=True, process_bash=True, verbose=False):
|
||||
"""Processes a Markdown file, extracting code blocks into temp files and returning the content and file info."""
|
||||
try:
|
||||
markdown_content = Path(file_path).read_text(encoding="utf-8")
|
||||
code_blocks_by_type = extract_code_blocks(markdown_content)
|
||||
temp_files = []
|
||||
|
||||
# Process all code block types based on flags
|
||||
code_types = []
|
||||
if process_python:
|
||||
for i, (num_spaces, code_block) in enumerate(code_blocks_by_type["python"]):
|
||||
code_types.append(("python", 0))
|
||||
if process_bash:
|
||||
code_types.append(("bash", 1000))
|
||||
|
||||
for code_type, offset in code_types:
|
||||
for i, (num_spaces, code_block) in enumerate(code_blocks_by_type[code_type]):
|
||||
if verbose:
|
||||
print(f"Extracting python code block {i} from {file_path}")
|
||||
print(f"Extracting {code_type} code block {i} from {file_path}")
|
||||
|
||||
num_spaces = len(num_spaces)
|
||||
code_without_indentation = remove_indentation(code_block, num_spaces)
|
||||
temp_file_path = temp_dir / generate_temp_filename(file_path, i)
|
||||
temp_file_path = temp_dir / generate_temp_filename(file_path, i + offset, code_type)
|
||||
|
||||
with open(temp_file_path, "w", encoding="utf-8") as temp_file:
|
||||
temp_file.write(code_without_indentation)
|
||||
|
||||
temp_files.append((num_spaces, code_block, temp_file_path))
|
||||
temp_files.append((num_spaces, code_block, temp_file_path, code_type))
|
||||
|
||||
return markdown_content, temp_files
|
||||
|
||||
@@ -153,13 +196,17 @@ def process_markdown_file(file_path, temp_dir, process_python=True, verbose=Fals
|
||||
|
||||
def update_markdown_file(file_path, markdown_content, temp_files):
|
||||
"""Updates a Markdown file with formatted code blocks."""
|
||||
for num_spaces, original_code_block, temp_file_path in temp_files:
|
||||
for num_spaces, original_code_block, temp_file_path, code_type in temp_files:
|
||||
try:
|
||||
with open(temp_file_path, encoding="utf-8") as temp_file:
|
||||
formatted_code = temp_file.read().rstrip("\n") # Strip trailing newlines
|
||||
formatted_code_with_indentation = add_indentation(formatted_code, num_spaces)
|
||||
|
||||
for lang in ("python", "py", "{ .py .annotate }"):
|
||||
# Define the language tags for each code type
|
||||
lang_tags = {"python": ["python", "py", "{ .py .annotate }"], "bash": ["bash", "sh", "shell"]}
|
||||
|
||||
# Replace the code blocks with the formatted version
|
||||
for lang in lang_tags[code_type]:
|
||||
markdown_content = markdown_content.replace(
|
||||
f"{' ' * num_spaces}```{lang}\n{original_code_block}\n{' ' * num_spaces}```",
|
||||
f"{' ' * num_spaces}```{lang}\n{formatted_code_with_indentation}\n{' ' * num_spaces}```",
|
||||
@@ -174,8 +221,8 @@ def update_markdown_file(file_path, markdown_content, temp_files):
|
||||
print(f"Error writing file {file_path}: {e}")
|
||||
|
||||
|
||||
def main(root_dir=Path.cwd(), process_python=True, verbose=False):
|
||||
"""Process Markdown files, format selected code blocks, and update the original files."""
|
||||
def main(root_dir=Path.cwd(), process_python=True, process_bash=True, verbose=False):
|
||||
"""Processes Markdown files, extracts and formats code blocks, and updates the original files."""
|
||||
root_path = Path(root_dir)
|
||||
markdown_files = [markdown_file for markdown_file in root_path.rglob("*.md") if not markdown_file.is_symlink()]
|
||||
temp_dir = Path("temp_code_blocks")
|
||||
@@ -186,13 +233,18 @@ def main(root_dir=Path.cwd(), process_python=True, verbose=False):
|
||||
for markdown_file in markdown_files:
|
||||
if verbose:
|
||||
print(f"Processing {markdown_file}")
|
||||
markdown_content, temp_files = process_markdown_file(markdown_file, temp_dir, process_python, verbose)
|
||||
markdown_content, temp_files = process_markdown_file(
|
||||
markdown_file, temp_dir, process_python, process_bash, verbose
|
||||
)
|
||||
if markdown_content and temp_files:
|
||||
all_temp_files.append((markdown_file, markdown_content, temp_files))
|
||||
|
||||
# Format code blocks based on flags
|
||||
if process_python:
|
||||
format_code_with_ruff(temp_dir) # Format Python files
|
||||
if process_bash:
|
||||
format_bash_with_prettier(temp_dir) # Format Bash files
|
||||
|
||||
# Update Markdown files with formatted code blocks
|
||||
for markdown_file, markdown_content, temp_files in all_temp_files:
|
||||
update_markdown_file(markdown_file, markdown_content, temp_files)
|
||||
@@ -202,4 +254,4 @@ def main(root_dir=Path.cwd(), process_python=True, verbose=False):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(process_python=True)
|
||||
main(process_python=True, process_bash=True)
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import mock_open, patch
|
||||
from actions.update_markdown_code_blocks import (
|
||||
add_indentation,
|
||||
extract_code_blocks,
|
||||
format_bash_with_prettier,
|
||||
generate_temp_filename,
|
||||
main,
|
||||
process_markdown_file,
|
||||
@@ -24,12 +25,20 @@ def test():
|
||||
return True
|
||||
```
|
||||
|
||||
And some bash code:
|
||||
|
||||
```bash
|
||||
echo "Hello World"
|
||||
```
|
||||
"""
|
||||
code_blocks = extract_code_blocks(markdown_content)
|
||||
|
||||
assert len(code_blocks["python"]) == 1
|
||||
assert code_blocks["python"][0][1] == "def test():\n return True"
|
||||
|
||||
assert len(code_blocks["bash"]) == 1
|
||||
assert code_blocks["bash"][0][1] == 'echo "Hello World"'
|
||||
|
||||
|
||||
def test_remove_indentation():
|
||||
"""Test removing indentation from code blocks."""
|
||||
@@ -63,11 +72,16 @@ def test_generate_temp_filename():
|
||||
"""Test generating temporary filenames."""
|
||||
file_path = Path("docs/guide.md")
|
||||
|
||||
filename = generate_temp_filename(file_path, 0)
|
||||
filename = generate_temp_filename(file_path, 0, "python")
|
||||
|
||||
assert "guide_docs_p0_" in filename
|
||||
assert filename.endswith(".py")
|
||||
|
||||
filename = generate_temp_filename(file_path, 1, "bash")
|
||||
|
||||
assert "guide_docs_b1_" in filename
|
||||
assert filename.endswith(".sh")
|
||||
|
||||
|
||||
@patch("pathlib.Path.read_text")
|
||||
@patch("pathlib.Path.write_text")
|
||||
@@ -94,6 +108,16 @@ def test():
|
||||
mock_file.assert_called_once()
|
||||
|
||||
|
||||
def test_format_bash_skips_when_no_shell_files(tmp_path):
|
||||
"""Test bash formatter skips Prettier when no shell snippets were extracted."""
|
||||
(tmp_path / "snippet.py").write_text("print('ok')", encoding="utf-8")
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
format_bash_with_prettier(tmp_path)
|
||||
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_main_skips_symlinked_markdown(tmp_path):
|
||||
"""Test Markdown formatter skips symlinks to avoid formatting the same content twice."""
|
||||
target = tmp_path / "AGENTS.md"
|
||||
@@ -101,34 +125,14 @@ def test_main_skips_symlinked_markdown(tmp_path):
|
||||
(tmp_path / "CLAUDE.md").symlink_to(target)
|
||||
|
||||
with patch("actions.update_markdown_code_blocks.process_markdown_file", return_value=("", [])) as mock_process:
|
||||
main(root_dir=tmp_path, process_python=False)
|
||||
main(root_dir=tmp_path, process_python=False, process_bash=False)
|
||||
|
||||
mock_process.assert_called_once()
|
||||
assert mock_process.call_args.args[0] == target
|
||||
|
||||
|
||||
def test_main_ignores_bash_blocks(tmp_path):
|
||||
"""Test Markdown formatting ignores shell fences because docs often use them for terminal output."""
|
||||
markdown = tmp_path / "example.md"
|
||||
markdown.write_text(
|
||||
"""# Example
|
||||
|
||||
```bash
|
||||
Firmware Version: 4.23.0 (release,app,extended context switch buffer)
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
main(root_dir=tmp_path, process_python=False)
|
||||
|
||||
mock_run.assert_not_called()
|
||||
assert markdown.read_text(encoding="utf-8").endswith("```\n")
|
||||
|
||||
|
||||
def test_main_real_files():
|
||||
"""Test main function on actual repository Markdown files."""
|
||||
# Run main on current directory which contains README.md and other Markdown files
|
||||
# This provides real-world test coverage of the entire pipeline
|
||||
main(process_python=True, verbose=False)
|
||||
main(process_python=True, process_bash=True, verbose=False)
|
||||
|
||||
Reference in New Issue
Block a user