- 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
303 lines
10 KiB
Python
Executable file
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()
|