5.8 KiB
Limpieza de HTML embebido (Publicaciones y Entrelíneas)
Notas técnicas sobre el saneamiento de HTML que llega desde el CMS/índice de búsqueda
y se renderiza con v-html en los paneles de detalle. Documenta el problema, la causa
raíz y el fix aplicado en cada caso, para poder depurar o mejorar esto más adelante.
Contexto general
Varios documentos (publicaciones, entrelíneas) traen un campo html que en teoría es
un fragmento de texto formateado, pero en la práctica viene "contaminado" con marcado
que no debería estar ahí: estilos de Word, anchos fijos, o incluso HTML ya renderizado
de otro componente que fue copiado/pegado en Directus por error. Ese marcado extra
puede romper el layout porque se inyecta directo en el DOM de la app (clases de
Tailwind, data-*, tablas de Word con width en cm/pt, etc.).
Cada vista que hace v-html de estos campos tiene su propia función de limpieza
(basada en regex, no en un parser DOM) justo antes de renderizar.
Caso 1 — PublicationDetail.vue: HTML pegado desde Word
Archivo: app/components/PublicationDetail.vue
Síntoma: tablas/celdas con overflow horizontal o contenido comprimido en el panel de detalle de publicaciones.
Causa raíz: el HTML exportado/pegado desde Word trae:
- Bloques
<style>completos con reglasmso-*. - Anchos fijos en unidades absolutas (
width: 15.5cm,pt, etc.) en tablas y celdas, que no responden al layout del contenedor.
Fix: función cleanWordHtml() (cerca de la línea 85) aplicada al v-html del
párrafo:
- Elimina bloques
<style>. - Elimina declaraciones
width: <número><cm|mm|pt|px|em|rem|in|pc>. - Colapsa saltos de línea literales a un espacio.
Reforzado con CSS en .paragraph-html (:deep(p|span|li|td|th) → white-space: normal, tablas a width: 100% / table-layout: auto, celdas y divs a width: auto /
max-width: 100%) para cubrir estilos que la regex no puede tocar.
Caso 2 — EntrelineaDetail.vue: HTML contaminado con clases de otro componente
Archivo: app/components/entrelineas/EntrelineaDetail.vue
Síntoma: el texto de la entrelínea se mostraba con una palabra por línea, dejando la mayor parte del panel en blanco.
Causa raíz (confirmada consultando el documento directo en Typesense): el campo
html de ese registro no contenía un <p> por palabra (esa fue la hipótesis inicial,
descartada). Contenía el HTML ya renderizado de PublicationDetail.vue pegado por
error en Directus, incluyendo:
- Atributos de scoping de Vue (
data-v-d5ee3d80). data-paragraph-number="37".- Clases de Tailwind reales:
grid grid-cols-1fr items-start gap-2 mb-2 grid-cols-[20px_1fr].
Como esas clases son utilidades globales de Tailwind, se aplicaban igual dentro de
EntrelineaDetail. El contenedor quedaba como grid de 2 columnas (20px 1fr) y, al
tener un solo hijo, el auto-placement lo metía en la columna de 20px → todo el
párrafo se comprimía a un ancho mínimo y cada palabra terminaba en su propia línea al
hacer wrap.
Cómo se verificó: se consultó el documento crudo directo en Typesense:
curl -s "$NUXT_PUBLIC_TYPESENSE_URL/multi_search" \
-H "X-TYPESENSE-API-KEY: $NUXT_PUBLIC_TYPESENSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"searches":[{"collection":"entrelineas","q":"*","filter_by":"id:=<ID_DEL_DOC>","include_fields":"*"}]}'
Esto es útil para cualquier bug futuro de renderizado: primero confirmar qué HTML crudo hay realmente en el índice antes de asumir la causa.
Fix: función formatEntrelineaText() (cerca de la línea 56), agrega dos
reemplazos antes de los ya existentes:
- Elimina cualquier atributo
class="..."/class='...'. - Elimina cualquier atributo
data-*="..."/data-*='...'.
Esto neutraliza clases o atributos de scoping filtrados desde cualquier otra fuente,
sin tocar los style inline (que sí son necesarios: cursiva, colores, fuente del
documento original).
Limitaciones conocidas
- Ambas limpiezas son basadas en regex, no en un parser DOM real. Cubren los casos
vistos hasta ahora, pero no garantizan sanear cualquier HTML arbitrario (por ejemplo,
no tocan
style="display: grid; ..."puesto inline, solowidthen unidades absolutas). - La lógica está duplicada entre
cleanWordHtml(Publicaciones) yformatEntrelineaText(Entrelíneas). Si aparece un caso nuevo, hay que recordar aplicarlo en los dos lugares (o consolidarlos, ver abajo). - El origen del problema está en los datos (Directus / proceso de carga), no en el frontend. Estas funciones son un parche en el punto de renderizado, no una corrección en la fuente.
Ideas para mejorar esto a futuro
- Consolidar en un solo util compartido, por ejemplo en
app/utils/textUtilities.tso un nuevoapp/utils/htmlSanitizer.ts, con funciones nombradas por lo que hacen (stripStyleBlocks,stripAbsoluteWidths,stripClassAndDataAttrs,collapseNewlines) y componerlas según necesite cada vista, en vez de tener dos funciones casi idénticas. - Agregar tests de regresión con fixtures de HTML "sucio" ya vistos en producción
(el de Word con
mso-*, el de la entrelínea contaminada con clases de grid) para que un cambio futuro no reintroduzca estos bugs silenciosamente. - Sanear en el origen (al indexar en Typesense o al guardar en Directus) en lugar
de en cada punto de render, para que cualquier consumidor futuro del campo
html(snippets en listas, exportaciones, etc.) reciba datos ya limpios. - Si se detectan más casos de contaminación cruzada entre componentes, vale la pena revisar el flujo de carga de contenido en Directus para encontrar dónde se está pegando HTML renderizado en vez de HTML fuente.