search/docs/html-content-cleanup.md

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 reglas mso-*.
  • 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, solo width en unidades absolutas).
  • La lógica está duplicada entre cleanWordHtml (Publicaciones) y formatEntrelineaText (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

  1. Consolidar en un solo util compartido, por ejemplo en app/utils/textUtilities.ts o un nuevo app/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.
  2. 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.
  3. 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.
  4. 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.