skills-vault/skills/fix-title/scripts/fix_markdown_titles.py

117 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Shift Markdown ATX headings down by a fixed number of levels."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
HEADING_RE = re.compile(r"^( {0,3})(#{1,6})([ \t]+.*)$")
FENCE_RE = re.compile(r"^( {0,3})(`{3,}|~{3,})")
def shift_headings(text: str, levels: int) -> str:
if levels <= 0:
raise ValueError("levels must be a positive integer")
lines = text.splitlines(keepends=True)
in_fence = False
fence_marker = ""
output: list[str] = []
for line in lines:
body = line[:-1] if line.endswith("\n") else line
newline = "\n" if line.endswith("\n") else ""
if body.endswith("\r"):
body = body[:-1]
newline = "\r" + newline
fence_match = FENCE_RE.match(body)
if fence_match:
marker = fence_match.group(2)
marker_char = marker[0]
if not in_fence:
in_fence = True
fence_marker = marker_char * len(marker)
elif marker_char == fence_marker[0] and len(marker) >= len(fence_marker):
in_fence = False
fence_marker = ""
output.append(line)
continue
if in_fence:
output.append(line)
continue
heading_match = HEADING_RE.match(body)
if heading_match:
indent, hashes, rest = heading_match.groups()
new_level = min(6, len(hashes) + levels)
output.append(f"{indent}{'#' * new_level}{rest}{newline}")
else:
output.append(line)
return "".join(output)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Shift Markdown ATX headings down by adding # characters."
)
parser.add_argument("path", type=pathlib.Path, help="Markdown file to rewrite")
parser.add_argument(
"-l",
"--levels",
type=int,
default=2,
help="Number of heading levels to add. Default: 2",
)
parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
help="Write to this file instead of modifying the source file",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print converted content to stdout without writing files",
)
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
if args.levels <= 0:
print("error: --levels must be a positive integer", file=sys.stderr)
return 2
try:
source = args.path.read_text(encoding="utf-8")
except OSError as exc:
print(f"error: cannot read {args.path}: {exc}", file=sys.stderr)
return 1
fixed = shift_headings(source, args.levels)
if args.dry_run:
sys.stdout.write(fixed)
return 0
destination = args.output or args.path
try:
destination.write_text(fixed, encoding="utf-8")
except OSError as exc:
print(f"error: cannot write {destination}: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))