scripts-fix-md060-table-formatting
#!/usr/bin/env python3 """
title: "Fix Md060 Table Formatting" component_type: script version: "1.0.0" audience: contributor status: stable summary: "MD060 Table Formatting Auto-Fixer (Non-Breaking)" keywords: ['fix', 'formatting', 'git', 'md060', 'review'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "fix-md060-table-formatting.py" language: python executable: true usage: "python3 scripts/fix-md060-table-formatting.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false
MD060 Table Formatting Auto-Fixer (Non-Breaking)
Automatically fixes table formatting issues by adding proper spacing around pipes.
Safety Features:
- Dry-run mode (preview changes before applying)
- Automatic backups before modifications
- Git working tree validation
- Rollback capability
- Before/after validation reports
Usage: # DRY RUN: Preview changes (recommended first step) python3 scripts/fix-md060-table-formatting.py --fix --dry-run
# Fix all tables with backup
python3 scripts/fix-md060-table-formatting.py --fix --backup
# Rollback changes
python3 scripts/fix-md060-table-formatting.py --rollback backup-TIMESTAMP
"""
import re import json import argparse import shutil import subprocess from pathlib import Path from datetime import datetime from typing import List, Tuple from dataclasses import dataclass, asdict
@dataclass class TableFix: """Represents a table formatting fix.""" file_path: str line_number: int original: str fixed: str
class SafetyManager: """Manages safety features: backups, git checks, rollbacks."""
@staticmethod
def check_git_clean(repo_root: Path) -> bool:
"""Check if git working tree is clean."""
try:
result = subprocess.run(
['git', 'status', '--porcelain'],
cwd=repo_root,
capture_output=True,
text=True
)
return result.returncode == 0 and len(result.stdout.strip()) == 0
except Exception:
return False
@staticmethod
def create_backup(files: List[Path], backup_dir: Path) -> bool:
"""Create backup of files before modification."""
try:
backup_dir.mkdir(parents=True, exist_ok=True)
for file_path in files:
rel_path = file_path.relative_to(file_path.parents[len(list(file_path.parents)) - 1])
backup_file = backup_dir / rel_path
backup_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(file_path, backup_file)
manifest = {
'timestamp': datetime.now().isoformat(),
'files': [str(f) for f in files]
}
(backup_dir / 'manifest.json').write_text(json.dumps(manifest, indent=2))
return True
except Exception as e:
print(f"Backup failed: {e}")
return False
@staticmethod
def rollback(backup_dir: Path) -> bool:
"""Restore files from backup."""
try:
manifest_file = backup_dir / 'manifest.json'
if not manifest_file.exists():
print(f"Error: No manifest found in {backup_dir}")
return False
manifest = json.loads(manifest_file.read_text())
for file_str in manifest['files']:
source = Path(file_str)
rel_path = source.relative_to(source.parents[len(list(source.parents)) - 1])
backup_file = backup_dir / rel_path
if backup_file.exists():
shutil.copy2(backup_file, source)
print(f" ✓ Restored {source}")
return True
except Exception as e:
print(f"Rollback failed: {e}")
return False
class MD060Fixer: """Fixes MD060 table formatting violations."""
def __init__(self, repo_root: Path, dry_run: bool = False):
self.repo_root = repo_root
self.dry_run = dry_run
# Match table rows (lines with pipes)
self.table_row_pattern = re.compile(r'^\|.+\|$')
def fix_table_row(self, line: str) -> str:
"""
Fix spacing around pipes in a table row.
Before: |Column1|Column2|Value|
After: | Column1 | Column2 | Value |
"""
if not line.strip().startswith('|') or not line.strip().endswith('|'):
return line
# Split by pipes
parts = line.split('|')
# Fix each cell (skip first and last which are empty)
fixed_parts = []
for i, part in enumerate(parts):
if i == 0 or i == len(parts) - 1:
# Keep empty parts at start/end
fixed_parts.append(part)
elif part.strip() == '':
# Empty cell
fixed_parts.append(' ')
elif re.match(r'^-+$', part.strip()):
# Separator row (keep dashes, just ensure spacing)
fixed_parts.append(f" {part.strip()} ")
else:
# Regular cell content - add single space padding
fixed_parts.append(f" {part.strip()} ")
return '|'.join(fixed_parts)
def process_file(self, file_path: Path) -> List[TableFix]:
"""Process a single file and fix table formatting."""
fixes = []
try:
content = file_path.read_text(encoding='utf-8')
lines = content.split('\n')
modified_lines = []
changed = False
for i, line in enumerate(lines):
if self.table_row_pattern.match(line):
fixed_line = self.fix_table_row(line)
if fixed_line != line:
fixes.append(TableFix(
file_path=str(file_path.relative_to(self.repo_root)),
line_number=i + 1,
original=line,
fixed=fixed_line
))
modified_lines.append(fixed_line)
changed = True
else:
modified_lines.append(line)
else:
modified_lines.append(line)
# Write back if changes were made and not dry-run
if changed and not self.dry_run:
file_path.write_text('\n'.join(modified_lines), encoding='utf-8')
except Exception as e:
print(f"Error processing {file_path}: {e}")
return fixes
def scan_repository(self, pattern: str = "**/*.md") -> Tuple[List[TableFix], List[Path]]:
"""Scan repository for MD060 violations."""
all_fixes = []
affected_files = []
for md_file in self.repo_root.glob(pattern):
if md_file.is_file() and not str(md_file).startswith(str(self.repo_root / 'node_modules')):
fixes = self.process_file(md_file)
if fixes:
all_fixes.extend(fixes)
affected_files.append(md_file)
return all_fixes, affected_files
def main(): parser = argparse.ArgumentParser( description='Fix MD060 table formatting violations (Non-Breaking)', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' Examples:
Dry run (safe preview)
python3 %(prog)s --fix --dry-run
Fix all tables with backup
python3 %(prog)s --fix --backup
Rollback changes
python3 %(prog)s --rollback backups/md060-backup-20251206-143022 ''' )
parser.add_argument('--fix', action='store_true',
help='Fix table formatting issues')
parser.add_argument('--rollback', type=str, metavar='BACKUP_DIR',
help='Rollback changes from backup directory')
parser.add_argument('--dry-run', action='store_true',
help='Preview changes without modifying files')
parser.add_argument('--backup', action='store_true',
help='Create backup before applying fixes')
parser.add_argument('--require-clean-git', action='store_true',
help='Require clean git working tree before applying')
parser.add_argument('--output', type=str, default='md060-fixes.json',
help='Output file for fix report (default: md060-fixes.json)')
parser.add_argument('--repo-root', type=str, default='.',
help='Repository root directory (default: current directory)')
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
# Handle rollback
if args.rollback:
backup_dir = Path(args.rollback)
print(f"Rolling back changes from {backup_dir}...")
if SafetyManager.rollback(backup_dir):
print("\n✅ Rollback completed successfully")
else:
print("\n❌ Rollback failed")
return
# Safety check: git working tree
if args.require_clean_git and not SafetyManager.check_git_clean(repo_root):
print("❌ Error: Git working tree is not clean")
print(" Commit or stash changes first, or remove --require-clean-git flag")
return
if args.fix:
fixer = MD060Fixer(repo_root, dry_run=args.dry_run)
print("Scanning repository for MD060 table formatting issues...")
fixes, affected_files = fixer.scan_repository()
print(f"\nFound {len(fixes)} table rows to fix in {len(affected_files)} files")
if not fixes:
print("✅ No table formatting issues found!")
return
# Save fix report
output_file = repo_root / args.output
with output_file.open('w', encoding='utf-8') as f:
json.dump([asdict(fix) for fix in fixes], f, indent=2)
print(f"Fix report saved to: {output_file}")
if args.dry_run:
print("\n[DRY RUN] Preview of changes:")
for i, fix in enumerate(fixes[:10]): # Show first 10
print(f"\n{fix.file_path}:{fix.line_number}")
print(f" Before: {fix.original}")
print(f" After: {fix.fixed}")
if len(fixes) > 10:
print(f"\n... and {len(fixes) - 10} more fixes")
print(f"\n[DRY RUN] Would fix {len(fixes)} table rows")
else:
# Create backup if requested
if args.backup:
timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
backup_dir = repo_root / 'backups' / f'md060-backup-{timestamp}'
print(f"\nCreating backup of {len(affected_files)} files...")
if not SafetyManager.create_backup(affected_files, backup_dir):
print("❌ Backup failed - aborting")
return
print(f"✅ Backup created: {backup_dir}")
print(f"\n✅ Fixed {len(fixes)} table rows in {len(affected_files)} files")
if args.backup:
print(f"\n💡 To rollback: python3 {__file__} --rollback {backup_dir}")
else:
parser.print_help()
if name == 'main': main()