#!/usr/bin/env python3 import os import re import sys from pathlib import Path # Detect a pre-include directive PRE_INCLUDE_RE = re.compile(r'^(\s*)\.\.\s+pre-include::\s+(.+)$') # Detect options following the directive OPTION_RE = re.compile(r'^\s*:(\w[\w-]*):\s*(.*)$') def read_included_content(filepath, options): """Read included file and slice per start-after/end-before, literal, and tab-width.""" try: with open(filepath, "r", encoding="utf-8") as f: lines = f.readlines() except FileNotFoundError: return [f".. (error: file not found: {filepath})\n"] # Apply start-after if "start-after" in options: marker = options["start-after"] for i, line in enumerate(lines): if marker in line: lines = lines[i + 1 :] break # Apply end-before if "end-before" in options: marker = options["end-before"] for i, line in enumerate(lines): if marker in line: lines = lines[:i] break # Handle tab-width (default 8 if not provided) if "tab-width" in options: try: width = int(options["tab-width"]) if options["tab-width"] else 8 lines = [l.expandtabs(width) for l in lines] except ValueError: pass # ignore malformed value return lines def process_file(path): """Process one file and expand all pre-include directives.""" with open(path, "r", encoding="utf-8") as f: lines = f.readlines() output_lines = [] i = 0 changed = False while i < len(lines): line = lines[i] m = PRE_INCLUDE_RE.match(line) if not m: output_lines.append(line) i += 1 continue indent, filename = m.groups() filename = filename.strip() options = {} # Gather options (start-after, end-before, literal, tab-width, etc.) j = i + 1 while j < len(lines): opt_match = OPTION_RE.match(lines[j]) if not opt_match: break opt_name, opt_value = opt_match.groups() options[opt_name] = opt_value j += 1 include_path = (Path(path).parent / filename).resolve() included_lines = read_included_content(include_path, options) # Apply literal or standard indentation if "literal" in options: # For literal, ensure a blank line before and after if not present if output_lines and output_lines[-1].strip(): output_lines.append("\n") output_lines.extend([indent + l for l in included_lines]) if included_lines and included_lines[-1].strip(): output_lines.append("\n") else: # Normal include: indent non-empty lines, preserve blank ones indented = [indent + l if l.strip() else l for l in included_lines] output_lines.extend(indented) changed = True i = j # Skip over directive and its options if changed: # backup_path = path + ".bak" # os.rename(path, backup_path) with open(path, "w", encoding="utf-8") as f: f.writelines(output_lines) # print(f"Processed {path} (backup saved as {backup_path})") print(f"\u2705 Processed {path})") else: print(f"No pre-include directives in {path}") def main(): if len(sys.argv) != 2: print("Usage: normalize_includes.py ") sys.exit(1) root = Path(sys.argv[1]).resolve() if not root.is_dir(): print(f"⚠️ Error: {root} is not a directory") sys.exit(1) for ext in ("*.rst", "*.rest"): for path in root.rglob(ext): process_file(path) if __name__ == "__main__": main()