🔧 CORRECCIONES CRÍTICAS DE ORGANIZACIÓN
### Renumeración Bloque 2 → 4 (Soporte Vital Básico)
- Resuelto conflicto crítico: dos bloques compartían numeración 'Bloque 2'
- Renumerados 9 archivos de BLOQUE_02_* → BLOQUE_04_*
- Actualizados títulos internos: 2.0-2.8 → 4.0-4.8
- Actualizadas referencias cruzadas en múltiples capítulos
### Resolución de Duplicidades
- Glucómetro: Convertido 3.11 a redirect hacia 1.7 (ubicación definitiva)
- Actualizadas referencias a Glucómetro (3.11, 1.5 → 1.7)
### Documentación
- Generado DIAGNOSTICO_ORGANIZACIONAL_CRITICO.md
- Generado PLAN_RENUMERACION_COMPLETO.md
- Generado INFORME_RENUMERACION_BLOQUE_2_4.md
### Archivos Afectados
- 9 archivos renumerados (03_SOPORTE_VITAL_BASICO/)
- 10+ archivos con referencias actualizadas
- 1 archivo convertido a redirect (BLOQUE_03_11_GLUCOMETRO.md)
Fixes: Conflictos de numeración detectados en auditoría organizacional
328 lines
12 KiB
Python
328 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Script para convertir archivos MD de cada módulo en documentos Word (.docx)
|
||
Un documento Word por módulo, incluyendo todos sus bloques/temas
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from docx import Document
|
||
from docx.shared import Pt, Inches, RGBColor
|
||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||
from docx.oxml.ns import qn
|
||
|
||
# Mapeo de carpetas a nombres de módulos
|
||
MODULOS_MAP = {
|
||
'00_INDICE_Y_NAVEGACION': '00_INDICE_Y_NAVEGACION',
|
||
'01_FUNDAMENTOS_Y_CONCEPTOS': '01_FUNDAMENTOS_Y_CONCEPTOS',
|
||
'02_PROCEDIMIENTOS_BASICOS': '02_PROCEDIMIENTOS_BASICOS',
|
||
'03_SOPORTE_VITAL_BASICO': '03_SOPORTE_VITAL_BASICO',
|
||
'04_MATERIAL_E_INMOVILIZACION': '04_MATERIAL_E_INMOVILIZACION',
|
||
'05_OXIGENOTERAPIA_Y_MATERIAL_SANITARIO': '05_OXIGENOTERAPIA_Y_MATERIAL_SANITARIO',
|
||
'06_PROTOCOLOS_TRANSTELEFONICOS': '06_PROTOCOLOS_TRANSTELEFONICOS',
|
||
'07_FARMACOLOGIA_OPERATIVA': '07_FARMACOLOGIA_OPERATIVA',
|
||
'08_TRANSFERENCIA_Y_TRASLADO': '08_TRANSFERENCIA_Y_TRASLADO',
|
||
'09_TRIAGE_MULTIPLES_VICTIMAS': '09_TRIAGE_MULTIPLES_VICTIMAS',
|
||
'10_CONDUCCION_Y_SEGURIDAD_VIAL': '10_CONDUCCION_Y_SEGURIDAD_VIAL',
|
||
'11_SITUACIONES_ESPECIALES': '11_SITUACIONES_ESPECIALES',
|
||
'12_GESTION_OPERATIVA': '12_GESTION_OPERATIVA',
|
||
'HERRAMIENTAS_Y_CHECKLISTS': 'HERRAMIENTAS_Y_CHECKLISTS',
|
||
}
|
||
|
||
def limpiar_markdown(texto):
|
||
"""Limpia y prepara texto Markdown para Word"""
|
||
# Eliminar enlaces pero mantener texto
|
||
texto = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', texto)
|
||
# Eliminar código inline pero mantener texto
|
||
texto = re.sub(r'`([^`]+)`', r'\1', texto)
|
||
return texto
|
||
|
||
def procesar_markdown_a_docx(doc, contenido_md, archivo_origen):
|
||
"""Procesa contenido Markdown y lo agrega al documento Word"""
|
||
lineas = contenido_md.split('\n')
|
||
i = 0
|
||
|
||
while i < len(lineas):
|
||
linea = lineas[i].strip()
|
||
|
||
# Saltar líneas vacías
|
||
if not linea:
|
||
i += 1
|
||
continue
|
||
|
||
# Títulos (# ## ###)
|
||
if linea.startswith('# '):
|
||
doc.add_heading(linea[2:].strip(), level=1)
|
||
elif linea.startswith('## '):
|
||
doc.add_heading(linea[3:].strip(), level=2)
|
||
elif linea.startswith('### '):
|
||
doc.add_heading(linea[4:].strip(), level=3)
|
||
elif linea.startswith('#### '):
|
||
doc.add_heading(linea[5:].strip(), level=4)
|
||
|
||
# Listas no ordenadas
|
||
elif linea.startswith('- ') or linea.startswith('* '):
|
||
texto = limpiar_markdown(linea[2:].strip())
|
||
p = doc.add_paragraph(texto, style='List Bullet')
|
||
# Procesar líneas siguientes de la lista
|
||
i += 1
|
||
while i < len(lineas) and (lineas[i].strip().startswith('- ') or
|
||
lineas[i].strip().startswith('* ') or
|
||
(lineas[i].strip().startswith(' ') and lineas[i].strip())):
|
||
siguiente = lineas[i].strip()
|
||
if siguiente.startswith('- ') or siguiente.startswith('* '):
|
||
texto = limpiar_markdown(siguiente[2:].strip())
|
||
doc.add_paragraph(texto, style='List Bullet')
|
||
elif siguiente.startswith(' '):
|
||
texto = limpiar_markdown(siguiente.strip())
|
||
if texto:
|
||
doc.add_paragraph(texto, style='List Bullet 2')
|
||
i += 1
|
||
continue
|
||
|
||
# Listas ordenadas
|
||
elif re.match(r'^\d+\.\s', linea):
|
||
texto = limpiar_markdown(re.sub(r'^\d+\.\s', '', linea))
|
||
p = doc.add_paragraph(texto, style='List Number')
|
||
i += 1
|
||
while i < len(lineas) and (re.match(r'^\d+\.\s', lineas[i].strip()) or
|
||
(lineas[i].strip().startswith(' ') and lineas[i].strip())):
|
||
siguiente = lineas[i].strip()
|
||
if re.match(r'^\d+\.\s', siguiente):
|
||
texto = limpiar_markdown(re.sub(r'^\d+\.\s', '', siguiente))
|
||
doc.add_paragraph(texto, style='List Number')
|
||
elif siguiente.startswith(' '):
|
||
texto = limpiar_markdown(siguiente.strip())
|
||
if texto:
|
||
doc.add_paragraph(texto, style='List Number 2')
|
||
i += 1
|
||
continue
|
||
|
||
# Tablas (básico)
|
||
elif '|' in linea and linea.count('|') >= 2:
|
||
# Detectar inicio de tabla
|
||
filas_tabla = []
|
||
j = i
|
||
while j < len(lineas) and '|' in lineas[j]:
|
||
fila = [celda.strip() for celda in lineas[j].split('|') if celda.strip()]
|
||
if fila and not all(c in '-: ' for c in ''.join(fila)): # No es separador
|
||
filas_tabla.append(fila)
|
||
j += 1
|
||
|
||
if filas_tabla:
|
||
# Crear tabla en Word
|
||
tabla = doc.add_table(rows=len(filas_tabla), cols=len(filas_tabla[0]))
|
||
tabla.style = 'Light Grid Accent 1'
|
||
|
||
for row_idx, fila in enumerate(filas_tabla):
|
||
for col_idx, celda in enumerate(fila):
|
||
if col_idx < len(tabla.rows[row_idx].cells):
|
||
tabla.rows[row_idx].cells[col_idx].text = limpiar_markdown(celda)
|
||
|
||
i = j
|
||
continue
|
||
|
||
# Código bloque (ignorar por ahora o convertir a texto)
|
||
elif linea.startswith('```'):
|
||
# Saltar bloque de código
|
||
i += 1
|
||
while i < len(lineas) and not lineas[i].strip().startswith('```'):
|
||
i += 1
|
||
|
||
# Separadores
|
||
elif linea.startswith('---') or linea.startswith('***'):
|
||
doc.add_paragraph('─' * 60)
|
||
|
||
# Párrafo normal
|
||
else:
|
||
texto = limpiar_markdown(linea)
|
||
if texto:
|
||
doc.add_paragraph(texto)
|
||
|
||
i += 1
|
||
|
||
def crear_documento_modulo(nombre_modulo, archivos_md, ruta_base, output_dir):
|
||
"""Crea un documento Word para un módulo específico"""
|
||
print(f"\n📖 Creando documento: {nombre_modulo}.docx")
|
||
|
||
# Crear nuevo documento Word
|
||
doc = Document()
|
||
|
||
# Configurar estilos
|
||
style = doc.styles['Normal']
|
||
style.font.name = 'Calibri'
|
||
style.font.size = Pt(11)
|
||
|
||
# Portada
|
||
titulo_portada = doc.add_heading('MANUAL TES DIGITAL', 0)
|
||
titulo_portada.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
||
nombre_modulo_limpio = nombre_modulo.replace('_', ' ').title()
|
||
subtitulo = doc.add_heading(nombre_modulo_limpio, 1)
|
||
subtitulo.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
||
doc.add_paragraph()
|
||
doc.add_paragraph(f"Documento generado automáticamente desde archivos Markdown")
|
||
fecha = datetime.now().strftime("%Y-%m-%d")
|
||
doc.add_paragraph(f"Fecha: {fecha}")
|
||
|
||
doc.add_page_break()
|
||
|
||
# Índice
|
||
doc.add_heading('ÍNDICE DE CONTENIDOS', 1)
|
||
doc.add_paragraph()
|
||
|
||
for idx, archivo_md in enumerate(archivos_md, 1):
|
||
# Extraer nombre legible
|
||
nombre_tema = archivo_md.replace('BLOQUE_', '').replace('.md', '')
|
||
nombre_tema = re.sub(r'(\d+)_', r'\1. ', nombre_tema)
|
||
nombre_tema = nombre_tema.replace('_', ' ').title()
|
||
doc.add_paragraph(f"{idx}. {nombre_tema}", style='List Number')
|
||
|
||
doc.add_page_break()
|
||
|
||
# Procesar cada archivo MD del módulo
|
||
archivos_procesados = 0
|
||
archivos_no_encontrados = []
|
||
|
||
for archivo_md in archivos_md:
|
||
ruta_archivo = os.path.join(ruta_base, archivo_md)
|
||
|
||
if not os.path.exists(ruta_archivo):
|
||
print(f" ⚠️ Archivo no encontrado: {archivo_md}")
|
||
archivos_no_encontrados.append(archivo_md)
|
||
continue
|
||
|
||
print(f" ➕ Procesando: {archivo_md}")
|
||
|
||
# Leer contenido MD
|
||
try:
|
||
with open(ruta_archivo, 'r', encoding='utf-8') as f:
|
||
contenido_md = f.read()
|
||
except Exception as e:
|
||
print(f" ❌ Error leyendo {archivo_md}: {e}")
|
||
continue
|
||
|
||
# Extraer título principal (primera línea con #)
|
||
lineas = contenido_md.split('\n')
|
||
titulo_principal = None
|
||
|
||
for linea in lineas[:10]: # Buscar en primeras 10 líneas
|
||
if linea.startswith('# '):
|
||
titulo_principal = linea[2:].strip()
|
||
break
|
||
|
||
if not titulo_principal:
|
||
titulo_principal = archivo_md.replace('.md', '').replace('_', ' ').title()
|
||
|
||
# Agregar título al documento Word
|
||
doc.add_heading(titulo_principal, level=1)
|
||
|
||
# Procesar contenido
|
||
procesar_markdown_a_docx(doc, contenido_md, archivo_md)
|
||
|
||
# Separador entre bloques
|
||
doc.add_paragraph()
|
||
doc.add_paragraph('─' * 60)
|
||
doc.add_paragraph()
|
||
|
||
archivos_procesados += 1
|
||
|
||
# Guardar documento
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
output_path = os.path.join(output_dir, f"{nombre_modulo}.docx")
|
||
|
||
try:
|
||
doc.save(output_path)
|
||
tamaño_kb = os.path.getsize(output_path) / 1024
|
||
print(f" ✅ Documento guardado: {output_path} ({tamaño_kb:.1f} KB)")
|
||
print(f" 📄 Archivos procesados: {archivos_procesados}/{len(archivos_md)}")
|
||
|
||
if archivos_no_encontrados:
|
||
print(f" ⚠️ Archivos no encontrados: {len(archivos_no_encontrados)}")
|
||
|
||
return output_path, archivos_procesados, archivos_no_encontrados
|
||
except Exception as e:
|
||
print(f" ❌ Error guardando documento: {e}")
|
||
return None, 0, archivos_no_encontrados
|
||
|
||
def main():
|
||
"""Función principal"""
|
||
print("=" * 70)
|
||
print("GENERADOR DE DOCUMENTOS WORD - MANUAL TES DIGITAL")
|
||
print("=" * 70)
|
||
|
||
base_path = 'MANUAL_TES_DIGITAL'
|
||
output_dir = 'DOCUMENTOS_WORD'
|
||
|
||
if not os.path.exists(base_path):
|
||
print(f"❌ Error: La carpeta '{base_path}' no existe.")
|
||
return
|
||
|
||
# Escanear módulos
|
||
print(f"\n🔍 Explorando módulos en: {base_path}")
|
||
modulos_archivos = {}
|
||
|
||
for modulo_folder in MODULOS_MAP.keys():
|
||
modulo_path = os.path.join(base_path, modulo_folder)
|
||
if os.path.exists(modulo_path):
|
||
archivos = sorted([f for f in os.listdir(modulo_path) if f.endswith('.md')])
|
||
if archivos:
|
||
modulos_archivos[modulo_folder] = archivos
|
||
|
||
print(f"\n📚 Módulos encontrados con contenido: {len(modulos_archivos)}")
|
||
for modulo, archivos in sorted(modulos_archivos.items()):
|
||
print(f" • {modulo}: {len(archivos)} archivos")
|
||
|
||
if not modulos_archivos:
|
||
print("❌ No se encontraron módulos con archivos .md")
|
||
return
|
||
|
||
# Generar todos los documentos
|
||
print(f"\n🚀 Generando {len(modulos_archivos)} documentos Word...")
|
||
print("=" * 70)
|
||
|
||
documentos_generados = []
|
||
total_archivos_procesados = 0
|
||
total_archivos_no_encontrados = []
|
||
|
||
for modulo_folder in sorted(modulos_archivos.keys()):
|
||
archivos = modulos_archivos[modulo_folder]
|
||
nombre_modulo = MODULOS_MAP[modulo_folder]
|
||
modulo_path = os.path.join(base_path, modulo_folder)
|
||
|
||
try:
|
||
doc_path, procesados, no_encontrados = crear_documento_modulo(
|
||
nombre_modulo, archivos, modulo_path, output_dir
|
||
)
|
||
|
||
if doc_path:
|
||
documentos_generados.append(doc_path)
|
||
total_archivos_procesados += procesados
|
||
total_archivos_no_encontrados.extend(no_encontrados)
|
||
except Exception as e:
|
||
print(f"❌ Error procesando {modulo_folder}: {e}")
|
||
|
||
# Resumen final
|
||
print("\n" + "=" * 70)
|
||
print("📊 RESUMEN DE GENERACIÓN")
|
||
print("=" * 70)
|
||
print(f"✅ Documentos generados: {len(documentos_generados)}")
|
||
print(f"📄 Archivos MD procesados: {total_archivos_procesados}")
|
||
|
||
if total_archivos_no_encontrados:
|
||
print(f"⚠️ Archivos no encontrados: {len(total_archivos_no_encontrados)}")
|
||
|
||
print(f"\n📁 Documentos guardados en: {output_dir}/")
|
||
print("\n📋 Documentos generados:")
|
||
for doc in sorted(documentos_generados):
|
||
tamaño = os.path.getsize(doc) / 1024 # KB
|
||
print(f" • {os.path.basename(doc)} ({tamaño:.1f} KB)")
|
||
|
||
print("\n🎯 ¡Proceso completado!")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|