codigo0/manual-tes/TES_Manual_Digital/convertir_a_word.py
planetazuzu af02a569a2 feat: Aplicación completa Manual TES Digital
- 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
2025-12-17 12:12:10 +01:00

303 lines
10 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Script para convertir archivos Markdown del Manual TES Digital a formato Word (.docx)
Uso:
python3 convertir_a_word.py [--directorio DIR] [--salida DIR_SALIDA] [--formato docx|xlsx]
Requisitos:
- pandoc (recomendado) O
- python-docx y markdown (alternativa)
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
from typing import Optional, List
import argparse
def check_pandoc() -> bool:
"""Verifica si pandoc está instalado"""
try:
result = subprocess.run(['pandoc', '--version'],
capture_output=True,
text=True,
timeout=5)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def convert_md_to_docx_pandoc(md_file: Path, docx_file: Path) -> tuple[bool, str]:
"""Convierte un archivo MD a DOCX usando pandoc"""
try:
# Comando pandoc para convertir MD a DOCX
cmd = [
'pandoc',
str(md_file),
'-o', str(docx_file),
'--from', 'markdown',
'--to', 'docx',
'--reference-doc', # Opcional: usar plantilla personalizada
]
# Ejecutar conversión
result = subprocess.run(cmd,
capture_output=True,
text=True,
timeout=30)
if result.returncode == 0:
return (True, "Convertido correctamente con pandoc")
else:
return (False, f"Error pandoc: {result.stderr}")
except subprocess.TimeoutExpired:
return (False, "Timeout en la conversión")
except Exception as e:
return (False, f"Error: {str(e)}")
def convert_md_to_docx_python(md_file: Path, docx_file: Path) -> tuple[bool, str]:
"""Convierte un archivo MD a DOCX usando python-docx (alternativa)"""
try:
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
import re
# Leer archivo Markdown
with open(md_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Crear documento Word
doc = Document()
# Configurar estilo por defecto
style = doc.styles['Normal']
font = style.font
font.name = 'Calibri'
font.size = Pt(11)
i = 0
in_list = False
list_level = 0
while i < len(lines):
line = lines[i].rstrip('\n\r')
# Línea vacía
if not line.strip():
if not in_list:
doc.add_paragraph()
in_list = False
i += 1
continue
# Separador horizontal
if line.strip() == '---':
doc.add_paragraph('_' * 50)
i += 1
continue
# Título nivel 1
if line.startswith('# '):
title = line[2:].strip()
doc.add_heading(title, level=1)
in_list = False
# Título nivel 2
elif line.startswith('## '):
title = line[3:].strip()
# Eliminar numeración de secciones (ej: "1.1.1 Objetivo" -> "Objetivo")
title = re.sub(r'^\d+\.\d+\.\d+\s+', '', title)
doc.add_heading(title, level=2)
in_list = False
# Título nivel 3
elif line.startswith('### '):
title = line[4:].strip()
doc.add_heading(title, level=3)
in_list = False
# Lista con viñetas
elif re.match(r'^[\s]*[-*]\s+', line):
content = re.sub(r'^[\s]*[-*]\s+', '', line)
# Manejar indentación (niveles de lista)
indent_match = re.match(r'^(\s+)', line)
level = len(indent_match.group(1)) // 2 if indent_match else 0
p = doc.add_paragraph(content, style='List Bullet')
in_list = True
# Lista numerada
elif re.match(r'^\s*\d+\.\s+', line):
content = re.sub(r'^\s*\d+\.\s+', '', line)
p = doc.add_paragraph(content, style='List Number')
in_list = True
# Texto con formato
else:
p = doc.add_paragraph()
# Procesar negritas, cursivas y código inline
content = line
# Negritas: **texto**
parts = re.split(r'(\*\*[^*]+\*\*)', content)
for part in parts:
if part.startswith('**') and part.endswith('**'):
run = p.add_run(part[2:-2])
run.bold = True
elif part.startswith('*') and part.endswith('*') and len(part) > 2:
# Podría ser cursiva
run = p.add_run(part[1:-1])
run.italic = True
elif part.startswith('`') and part.endswith('`'):
# Código inline
run = p.add_run(part[1:-1])
run.font.name = 'Courier New'
else:
p.add_run(part)
in_list = False
i += 1
# Guardar documento
doc.save(str(docx_file))
return (True, "Convertido correctamente con python-docx")
except ImportError as e:
return (False, f"Biblioteca faltante: {str(e)}. Instala con: pip install python-docx")
except Exception as e:
import traceback
return (False, f"Error en conversión: {str(e)}\n{traceback.format_exc()}")
def convert_directory(source_dir: Path, output_dir: Path, use_pandoc: bool = True) -> dict:
"""Convierte todos los archivos .md de un directorio a .docx"""
results = {
'total': 0,
'exitosos': 0,
'fallidos': 0,
'errores': []
}
# Crear directorio de salida si no existe
output_dir.mkdir(parents=True, exist_ok=True)
# Buscar todos los archivos .md
md_files = list(source_dir.rglob('*.md'))
# Excluir archivos en _DOCUMENTACION_INTERNA y archivos especiales
excluded_patterns = ['_DOCUMENTACION_INTERNA', 'MAPA_MAESTRO', 'INFORME', 'ANALISIS', 'ESTANDAR']
md_files = [f for f in md_files if not any(pattern in str(f) for pattern in excluded_patterns)]
results['total'] = len(md_files)
print(f"\nEncontrados {results['total']} archivos .md para convertir\n")
for md_file in sorted(md_files):
# Crear ruta relativa para mantener estructura
relative_path = md_file.relative_to(source_dir)
# Crear estructura de directorios en salida
docx_file = output_dir / relative_path.with_suffix('.docx')
docx_file.parent.mkdir(parents=True, exist_ok=True)
print(f"Convirtiendo: {relative_path}...", end=' ')
# Convertir según método disponible
if use_pandoc:
success, message = convert_md_to_docx_pandoc(md_file, docx_file)
else:
success, message = convert_md_to_docx_python(md_file, docx_file)
if success:
results['exitosos'] += 1
print(f"")
else:
results['fallidos'] += 1
results['errores'].append({
'archivo': str(relative_path),
'error': message
})
print(f"{message}")
return results
def main():
parser = argparse.ArgumentParser(
description='Convierte archivos Markdown del Manual TES Digital a formato Word (.docx)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Ejemplos:
# Convertir todo el directorio TES_Manual_Digital
python3 convertir_a_word.py
# Especificar directorio fuente y salida
python3 convertir_a_word.py --directorio ./TES_Manual_Digital --salida ./Manual_Word
# Forzar uso de python-docx (si pandoc no está disponible)
python3 convertir_a_word.py --no-pandoc
"""
)
parser.add_argument(
'--directorio',
type=str,
default='.',
help='Directorio fuente con archivos .md (default: directorio actual)'
)
parser.add_argument(
'--salida',
type=str,
default='Manual_Word',
help='Directorio de salida para archivos .docx (default: Manual_Word)'
)
parser.add_argument(
'--no-pandoc',
action='store_true',
help='Forzar uso de python-docx en lugar de pandoc'
)
args = parser.parse_args()
# Resolver rutas
source_dir = Path(args.directorio).resolve()
output_dir = Path(args.salida).resolve()
if not source_dir.exists():
print(f"❌ Error: El directorio fuente no existe: {source_dir}")
sys.exit(1)
# Verificar método de conversión
use_pandoc = False
if not args.no_pandoc:
if check_pandoc():
use_pandoc = True
print("✅ Pandoc detectado - usando pandoc para conversión (más fiel al formato original)")
else:
print("⚠️ Pandoc no detectado - intentando usar python-docx")
print(" Para mejor calidad, instala pandoc: sudo apt install pandoc")
# Convertir
results = convert_directory(source_dir, output_dir, use_pandoc=use_pandoc)
# Resumen
print(f"\n{'='*60}")
print(f"RESUMEN DE CONVERSIÓN")
print(f"{'='*60}")
print(f"Total de archivos: {results['total']}")
print(f"✅ Convertidos exitosamente: {results['exitosos']}")
print(f"❌ Fallidos: {results['fallidos']}")
if results['errores']:
print(f"\nErrores encontrados:")
for error in results['errores'][:10]: # Mostrar máximo 10 errores
print(f" - {error['archivo']}: {error['error']}")
if len(results['errores']) > 10:
print(f" ... y {len(results['errores']) - 10} errores más")
print(f"\nArchivos guardados en: {output_dir}")
print(f"{'='*60}\n")
if __name__ == '__main__':
main()