- Integración de 93 capítulos del manual completo - Componente MarkdownViewer para renderizar archivos .md - Navegación jerárquica completa (ManualIndex) - Sistema de búsqueda mejorado - Página ManualViewer con navegación anterior/siguiente - Scripts de verificación del manual - Puerto configurado en 8096 - Configuración de despliegue (Vercel, Netlify, GitHub Pages) - Todos los problemas detectados corregidos
283 lines
11 KiB
Python
283 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Script para normalizar el formato de todos los archivos .md del Manual TES Digital
|
||
según el estándar definido en ESTANDAR_FORMATO_MANUAL.md
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
from pathlib import Path
|
||
from typing import Tuple, Optional
|
||
|
||
def extract_block_chapter(filename: str) -> Optional[Tuple[int, Optional[int]]]:
|
||
"""Extrae bloque y capítulo del nombre de archivo"""
|
||
# Patrón: BLOQUE_XX_YY_...
|
||
match = re.match(r'BLOQUE_(\d+)_(\d+|[X])', filename)
|
||
if match:
|
||
block = int(match.group(1))
|
||
chapter_str = match.group(2)
|
||
if chapter_str == 'X':
|
||
return (block, None)
|
||
chapter = int(chapter_str)
|
||
return (block, chapter)
|
||
|
||
# Fallback: buscar en contenido del archivo
|
||
return None
|
||
|
||
def normalize_title(match_obj, block: int, chapter: Optional[int]) -> str:
|
||
"""Normaliza el título principal según el estándar"""
|
||
original = match_obj.group(0)
|
||
|
||
# Si ya tiene formato correcto X.Y –, mantenerlo pero capitalizar correctamente
|
||
pattern1 = r'^#\s*(\d+)\.(\d+)\s*[–-]\s*(.+)$'
|
||
match1 = re.match(pattern1, original)
|
||
if match1:
|
||
b, c, title = match1.groups()
|
||
# Capitalizar título (Title Case para palabras significativas)
|
||
title = title.title()
|
||
# Corregir casos especiales
|
||
title = re.sub(r'\b(De|Del|La|Los|Las|En|Por|Para|Con|Sin|Y|O|A|Al)\b', lambda m: m.group(1).lower(), title)
|
||
title = re.sub(r'\b(IV|IO|IM|SC|SVB|SVA|RCP|PCR|TES|ABCDE|PLS|OVACE|BVM|DESA|DEA)\b', lambda m: m.group(1).upper(), title)
|
||
# Primera letra siempre mayúscula
|
||
if title:
|
||
title = title[0].upper() + title[1:]
|
||
return f"# {b}.{c} – {title}"
|
||
|
||
# Patrón BLOQUE XX – ...
|
||
pattern2 = r'^#\s*BLOQUE\s+(\d+)\s*[–-]\s*(.+)$'
|
||
match2 = re.match(pattern2, original)
|
||
if match2:
|
||
b, title = match2.groups()
|
||
if chapter is not None:
|
||
title = title.title()
|
||
title = re.sub(r'\b(De|Del|La|Los|Las|En|Por|Para|Con|Sin|Y|O|A|Al)\b', lambda m: m.group(1).lower(), title)
|
||
title = re.sub(r'\b(IV|IO|IM|SC|SVB|SVA|RCP|PCR|TES|ABCDE|PLS|OVACE|BVM|DESA|DEA)\b', lambda m: m.group(1).upper(), title)
|
||
if title:
|
||
title = title[0].upper() + title[1:]
|
||
return f"# {b}.{chapter} – {title}"
|
||
|
||
# Patrón BLOQUE X.Y – ...
|
||
pattern3 = r'^#\s*BLOQUE\s+(\d+)\.(\d+)\s*[–-]\s*(.+)$'
|
||
match3 = re.match(pattern3, original)
|
||
if match3:
|
||
b, c, title = match3.groups()
|
||
title = title.title()
|
||
title = re.sub(r'\b(De|Del|La|Los|Las|En|Por|Para|Con|Sin|Y|O|A|Al)\b', lambda m: m.group(1).lower(), title)
|
||
title = re.sub(r'\b(IV|IO|IM|SC|SVB|SVA|RCP|PCR|TES|ABCDE|PLS|OVACE|BVM|DESA|DEA)\b', lambda m: m.group(1).upper(), title)
|
||
if title:
|
||
title = title[0].upper() + title[1:]
|
||
return f"# {b}.{c} – {title}"
|
||
|
||
# Si no coincide, devolver original
|
||
return original
|
||
|
||
def normalize_metadata(content: str) -> str:
|
||
"""Normaliza los metadatos del archivo"""
|
||
# Asegurar formato consistente de metadatos
|
||
content = re.sub(r'\*\*Versión:\*\*\s*(\d+\.?\d*)', r'**Versión:** \1', content)
|
||
content = re.sub(r'\*\*Fecha:\*\*\s*(\d{4}-\d{2}-\d{2})', r'**Fecha:** \1', content)
|
||
content = re.sub(r'\*\*Tipo:\*\*\s*(.+)', r'**Tipo:** \1', content)
|
||
|
||
# Asegurar que haya dos espacios al final de cada línea de metadatos (excepto la última)
|
||
lines = content.split('\n')
|
||
result = []
|
||
i = 0
|
||
while i < len(lines):
|
||
line = lines[i]
|
||
if re.match(r'\*\*(Versión|Fecha):\*\*', line) and i < len(lines) - 1:
|
||
# Añadir dos espacios al final si no los tiene
|
||
if not line.endswith(' '):
|
||
line = line.rstrip() + ' '
|
||
result.append(line)
|
||
i += 1
|
||
|
||
return '\n'.join(result)
|
||
|
||
def normalize_sections(content: str, block: int, chapter: Optional[int]) -> str:
|
||
"""Normaliza la numeración de secciones principales"""
|
||
if chapter is None:
|
||
return content # Capítulos especiales (X) no se normalizan
|
||
|
||
def fix_section_title(title: str) -> str:
|
||
"""Capitaliza correctamente el título de una sección (solo primera letra mayúscula)"""
|
||
if not title:
|
||
return title
|
||
|
||
title = title.strip()
|
||
|
||
# Convertir todo a minúsculas primero (excepto acrónimos que detectaremos después)
|
||
title_lower = title.lower()
|
||
|
||
# Detectar acrónimos y mantenerlos en mayúsculas
|
||
acronyms = ['TES', 'RCP', 'PCR', 'DESA', 'DEA', 'SVB', 'SVA', 'BVM', 'IV', 'IO', 'IM', 'SC', 'ABCDE', 'PLS', 'OVACE', 'SCA', 'AAS', 'ADR']
|
||
for acro in acronyms:
|
||
title_lower = re.sub(r'\b' + acro.lower() + r'\b', acro, title_lower, flags=re.IGNORECASE)
|
||
|
||
# Primera letra siempre mayúscula
|
||
if title_lower:
|
||
title_lower = title_lower[0].upper() + title_lower[1:]
|
||
|
||
return title_lower
|
||
|
||
# Buscar y reemplazar secciones principales con formato ## X.Y.Z título
|
||
def replace_section_with_decimal(match):
|
||
full_match = match.group(0)
|
||
num_part = match.group(1) if match.lastindex >= 1 else ''
|
||
title_part = match.group(2) if match.lastindex >= 2 else match.group(3) if match.lastindex >= 3 else ''
|
||
|
||
# Si ya tiene formato X.Y.Z correcto, solo capitalizar título
|
||
if re.match(r'^\d+\.\d+\.\d+', num_part):
|
||
fixed_title = fix_section_title(title_part)
|
||
return f"## {num_part} {fixed_title}"
|
||
|
||
# Si tiene formato X.Y.Z pero falta algo, mantener y capitalizar
|
||
if re.match(r'^\d+\.\d+\.\d+', num_part):
|
||
fixed_title = fix_section_title(title_part)
|
||
return f"## {num_part} {fixed_title}"
|
||
|
||
# Si tiene formato 1. o 2., convertir a X.Y.1, X.Y.2, etc.
|
||
if re.match(r'^\d+\.?$', num_part):
|
||
section_num = int(num_part.rstrip('.'))
|
||
fixed_title = fix_section_title(title_part)
|
||
return f"## {block}.{chapter}.{section_num} {fixed_title}"
|
||
|
||
return full_match
|
||
|
||
# Patrón más flexible: ## número título (con o sin punto)
|
||
pattern = r'^##\s+(\d+(?:\.\d+(?:\.\d+)?)?\.?)\s+(.+?)(?=\n|$)'
|
||
lines = content.split('\n')
|
||
result_lines = []
|
||
|
||
for line in lines:
|
||
match = re.match(pattern, line)
|
||
if match:
|
||
num_part = match.group(1)
|
||
title_part = match.group(2)
|
||
|
||
# Si ya tiene formato X.Y.Z, verificar y corregir título
|
||
if re.match(r'^\d+\.\d+\.\d+', num_part):
|
||
fixed_title = fix_section_title(title_part)
|
||
result_lines.append(f"## {num_part} {fixed_title}")
|
||
# Si tiene formato simple 1. o 1, convertir
|
||
elif re.match(r'^\d+\.?$', num_part):
|
||
section_num = int(num_part.rstrip('.'))
|
||
fixed_title = fix_section_title(title_part)
|
||
result_lines.append(f"## {block}.{chapter}.{section_num} {fixed_title}")
|
||
else:
|
||
# Ya tiene formato correcto, solo capitalizar
|
||
fixed_title = fix_section_title(title_part)
|
||
result_lines.append(f"## {num_part} {fixed_title}")
|
||
else:
|
||
result_lines.append(line)
|
||
|
||
return '\n'.join(result_lines)
|
||
|
||
def normalize_spacing(content: str) -> str:
|
||
"""Normaliza el espaciado del archivo"""
|
||
# Eliminar líneas en blanco múltiples (más de 2 seguidas)
|
||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||
|
||
# Eliminar espacios en blanco al final de líneas
|
||
lines = content.split('\n')
|
||
lines = [line.rstrip() for line in lines]
|
||
content = '\n'.join(lines)
|
||
|
||
# Eliminar líneas en blanco al final del archivo
|
||
content = content.rstrip() + '\n'
|
||
|
||
return content
|
||
|
||
def normalize_file(filepath: Path) -> Tuple[bool, str]:
|
||
"""Normaliza un archivo .md según el estándar"""
|
||
try:
|
||
with open(filepath, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
original_content = content
|
||
|
||
# Extraer bloque y capítulo del nombre del archivo
|
||
block_chapter = extract_block_chapter(filepath.name)
|
||
if block_chapter:
|
||
block, chapter = block_chapter
|
||
else:
|
||
# Intentar extraer del contenido
|
||
match = re.search(r'^#\s*(?:BLOQUE\s+)?(\d+)\.(\d+)', content, re.MULTILINE)
|
||
if match:
|
||
block, chapter = int(match.group(1)), int(match.group(2))
|
||
else:
|
||
# No se puede normalizar, archivo especial
|
||
return (False, "No se pudo identificar bloque/capítulo")
|
||
|
||
# Normalizar título principal
|
||
title_pattern = r'^#\s+.+$'
|
||
content = re.sub(title_pattern, lambda m: normalize_title(m, block, chapter), content, count=1, flags=re.MULTILINE)
|
||
|
||
# Normalizar metadatos
|
||
content = normalize_metadata(content)
|
||
|
||
# Normalizar secciones (solo si no es capítulo especial)
|
||
if chapter is not None:
|
||
content = normalize_sections(content, block, chapter)
|
||
|
||
# Normalizar espaciado
|
||
content = normalize_spacing(content)
|
||
|
||
# Solo escribir si hubo cambios
|
||
if content != original_content:
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
f.write(content)
|
||
return (True, "Normalizado correctamente")
|
||
else:
|
||
return (False, "Ya estaba normalizado")
|
||
|
||
except Exception as e:
|
||
return (False, f"Error: {str(e)}")
|
||
|
||
def main():
|
||
"""Función principal"""
|
||
base_dir = Path(__file__).parent
|
||
|
||
# Excluir documentación interna y archivos especiales
|
||
excluded_dirs = {'_DOCUMENTACION_INTERNA'}
|
||
excluded_files = {'MAPA_MAESTRO_MANUAL_TES_DIGITAL.md', 'normalizar_formato.py', 'INFORME_REORGANIZACION_FINAL.md'}
|
||
|
||
md_files = []
|
||
for root, dirs, files in os.walk(base_dir):
|
||
# Excluir directorios
|
||
dirs[:] = [d for d in dirs if d not in excluded_dirs]
|
||
|
||
for file in files:
|
||
if file.endswith('.md') and file not in excluded_files:
|
||
filepath = Path(root) / file
|
||
md_files.append(filepath)
|
||
|
||
print(f"Encontrados {len(md_files)} archivos .md para normalizar\n")
|
||
|
||
normalized = 0
|
||
skipped = 0
|
||
errors = 0
|
||
|
||
for filepath in sorted(md_files):
|
||
success, message = normalize_file(filepath)
|
||
relative_path = filepath.relative_to(base_dir)
|
||
|
||
if success:
|
||
normalized += 1
|
||
print(f"✅ {relative_path}: {message}")
|
||
elif "Error" in message:
|
||
errors += 1
|
||
print(f"❌ {relative_path}: {message}")
|
||
else:
|
||
skipped += 1
|
||
if "No se pudo" in message:
|
||
print(f"⚠️ {relative_path}: {message}")
|
||
|
||
print(f"\n=== RESUMEN ===")
|
||
print(f"Normalizados: {normalized}")
|
||
print(f"Omitidos: {skipped}")
|
||
print(f"Errores: {errors}")
|
||
print(f"Total: {len(md_files)}")
|
||
|
||
if __name__ == '__main__':
|
||
main()
|