fix-markdown-fences
Repair malformed markdown code fence closings. Use when markdown files have closing fences with language identifiers (```text instead of ```) or when generating markdown with code blocks to ensure proper fence closure.
$ 安裝
git clone https://github.com/rjmurillo/ai-agents /tmp/ai-agents && cp -r /tmp/ai-agents/.claude/skills/fix-markdown-fences ~/.claude/skills/ai-agents// tip: Run this command in your terminal to install the skill
name: fix-markdown-fences
description: "Repair malformed markdown code fence closings. Use when markdown files have closing fences with language identifiers (text instead of ) or when generating markdown with code blocks to ensure proper fence closure."
license: MIT
metadata:
version: 1.0.0
model: claude-haiku-4-5
Fix Markdown Code Fence Closings
Problem
When generating markdown with code blocks, closing fences sometimes include language identifiers:
<!-- Wrong -->
```python
def hello():
print("world")
```python <!-- Should be just ``` -->
The closing fence should never have a language identifier. This breaks markdown parsers and causes rendering issues.
When to Use
- After generating markdown with multiple code blocks
- When fixing existing markdown files with rendering issues
- When code blocks appear to "bleed" into surrounding content
- As a validation step before committing markdown documentation
Algorithm
Track fence state while scanning line by line:
-
Opening fence: Line matches
^\s*```\w+and not inside a block. Record indent level. Enter "inside block" state. -
Malformed closing fence: Line matches
^\s*```\w+while inside a block. This is a closing fence with a language identifier. Fix by inserting proper closing fence before this line. -
Valid closing fence: Line matches
^\s*```\s*$. Exit "inside block" state. -
End of file: If still inside a block, append closing fence.
Implementation
Python (Recommended)
import re
from pathlib import Path
def fix_markdown_fences(content: str) -> str:
"""Fix malformed code fence closings in markdown content."""
lines = content.splitlines()
result = []
in_code_block = False
block_indent = ""
opening_pattern = re.compile(r'^(\s*)```(\w+)')
closing_pattern = re.compile(r'^(\s*)```\s*$')
for line in lines:
opening_match = opening_pattern.match(line)
closing_match = closing_pattern.match(line)
if opening_match:
if in_code_block:
# Malformed closing fence with language identifier
# Insert proper closing fence before this line
result.append(f"{block_indent}```")
# Start new block
result.append(line)
block_indent = opening_match.group(1)
in_code_block = True
elif closing_match:
result.append(line)
in_code_block = False
block_indent = ""
else:
result.append(line)
# Handle file ending inside code block
if in_code_block:
result.append(f"{block_indent}```")
return '\n'.join(result)
def fix_markdown_files(directory: Path, pattern: str = "**/*.md") -> list[str]:
"""Fix all markdown files in directory. Returns list of fixed files."""
fixed = []
for file_path in directory.glob(pattern):
content = file_path.read_text()
fixed_content = fix_markdown_fences(content)
if content != fixed_content:
file_path.write_text(fixed_content)
fixed.append(str(file_path))
return fixed
Bash (Quick Check)
# Find files with potential issues (opening fence pattern at end of block)
grep -rn '```[a-zA-Z]' --include="*.md" | grep -v "^[^:]*:[0-9]*:\s*```[a-zA-Z]*$"
PowerShell
$directories = @('docs', 'src')
foreach ($dir in $directories) {
Get-ChildItem -Path $dir -Filter '*.md' -Recurse | ForEach-Object {
$file = $_.FullName
$content = Get-Content $file -Raw
$lines = $content -split "`r?`n"
$result = @()
$inCodeBlock = $false
$codeBlockIndent = ""
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -match '^(\s*)```(\w+)') {
if ($inCodeBlock) {
$result += $codeBlockIndent + '```'
$result += $line
$codeBlockIndent = $Matches[1]
} else {
$result += $line
$codeBlockIndent = $Matches[1]
$inCodeBlock = $true
}
}
elseif ($line -match '^(\s*)```\s*$') {
$result += $line
$inCodeBlock = $false
$codeBlockIndent = ""
}
else {
$result += $line
}
}
if ($inCodeBlock) {
$result += $codeBlockIndent + '```'
}
$newContent = $result -join "`n"
Set-Content -Path $file -Value $newContent -NoNewline
Write-Host "Fixed: $file"
}
}
Usage
Fix Files in Directory
# Python
python -c "
from pathlib import Path
exec(open('fix_fences.py').read())
fixed = fix_markdown_files(Path('docs'))
for f in fixed:
print(f'Fixed: {f}')
"
# PowerShell
pwsh fix-fences.ps1
Fix Single String (In-Memory)
content = """
```python
def example():
pass
```python
"""
fixed = fix_markdown_fences(content)
print(fixed)
Verification
# Check what changed
git diff --stat
# Review specific file
git diff docs/example.md
# Validate markdown renders correctly
# (use your preferred markdown preview tool)
Edge Cases Handled
- Nested indentation: Preserves indent level from opening fence
- Multiple consecutive blocks: Each block tracked independently
- File ending inside block: Automatically closes unclosed blocks
- Mixed line endings: Handles both
\nand\r\n
Prevention
When generating markdown with code blocks:
- Always use plain ``` for closing fences
- Never copy the opening fence line to close
- Track block state when programmatically generating markdown
Repository
