search/docs/html-content-cleanup.md

120 lines
5.8 KiB
Markdown

# 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`](../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`](../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:
```bash
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`](../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.