Files
turnosxpress/txclient/src/app/helpers/htmlSainitize.ts
T
2026-07-16 20:48:43 -03:00

174 lines
5.8 KiB
TypeScript

import DOMPurify from "dompurify";
/**
* Preprocesa HTML generado por Quill antes de enviarlo al backend
* o renderizarlo en React.
* - Sanitiza el HTML para prevenir XSS
* - Convierte listas bullets (<ol data-list="bullet">) a <ul>
* - Mantiene colores, background-color y clases de Quill
*
* @param rawHtml HTML crudo de Quill
* @returns HTML limpio y con listas correctamente formateadas
*/
export function preprocessQuillHtml(rawHtml: string): string {
// 1️⃣ Sanitización con DOMPurify
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
"p",
"ul",
"ol",
"li",
"strong",
"b",
"i",
"em",
"u",
"s",
"span",
"a",
"img",
"br",
"div",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"iframe", // Permitir iframes
],
// ✅ Incluimos data-list para que no sea eliminada
ALLOWED_ATTR: [
"style",
"class",
"contenteditable",
"data-list",
"href",
"target",
"rel",
"title",
"src",
"alt",
"width",
"height",
"allowfullscreen", // Permitir para videos
"frameborder", // Permitir para videos
],
ALLOWED_URI_REGEXP: /^(https?|mailto):/,
});
// 2️⃣ Parsear como DOM
const parser = new DOMParser();
const doc = parser.parseFromString(cleanHtml, "text/html");
// 📹 Filtrar iframes para permitir solo videos de YouTube
doc.querySelectorAll("iframe").forEach((iframe) => {
const src = iframe.getAttribute("src") || "";
const isYoutubeVideo =
src.startsWith("https://www.youtube.com/embed/") ||
src.startsWith("https://youtube.com/embed/");
if (!isYoutubeVideo) {
iframe.remove(); // Eliminar iframes que no sean de YouTube
}
});
// 3️⃣ Convertir <ol data-list="bullet"> a <ul> y manejar <li> con data-list="bullet"
doc.querySelectorAll("ol").forEach((ol) => {
const olEl = ol as HTMLElement;
// Si la OL tiene data-list="bullet" o si contiene LI con data-list="bullet"
const hasBulletLi = Array.from(olEl.children).some(
(child) => (child as HTMLElement).dataset.list === "bullet"
);
if (olEl.dataset.list === "bullet" || hasBulletLi) {
const ul = doc.createElement("ul");
// Mover los hijos de ol a ul
while (olEl.firstChild) {
const child = olEl.firstChild as HTMLElement;
// Eliminar spans ql-ui vacíos y el atributo data-list de los li
if (child.tagName === "LI") {
child.querySelectorAll("span.ql-ui").forEach((span) => span.remove());
delete child.dataset.list; // Eliminar el atributo data-list
}
ul.appendChild(child);
}
olEl.replaceWith(ul);
}
});
// Asegurarse de que cualquier <li> con data-list="bullet" que no esté dentro de una OL/UL sea manejado
doc.querySelectorAll("li[data-list='bullet']").forEach((li) => {
const liEl = li as HTMLElement;
// Eliminar spans ql-ui vacíos
liEl.querySelectorAll("span.ql-ui").forEach((span) => span.remove());
delete liEl.dataset.list; // Eliminar el atributo data-list
// Si el padre no es una UL, envolverlo en una UL (esto es un caso extremo, pero para robustez)
if (liEl.parentElement && liEl.parentElement.tagName !== "UL") {
const ul = doc.createElement("ul");
liEl.replaceWith(ul);
ul.appendChild(liEl);
}
});
return doc.body.innerHTML;
}
export const transformQuillContentForDisplay = (htmlString: string): string => {
if (!htmlString) return '';
// Usar DOMParser para manipulación segura del HTML
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
// Transformar imágenes
const images = doc.querySelectorAll('img');
images.forEach((img) => {
// Crear wrapper para imagen
const wrapper = doc.createElement('div');
wrapper.className = 'image-wrapper';
// Clonar la imagen con sus atributos
const clonedImg = img.cloneNode(true) as HTMLImageElement;
// Insertar en wrapper y reemplazar
wrapper.appendChild(clonedImg);
if (img.parentNode) {
img.parentNode.replaceChild(wrapper, img);
}
});
// Transformar videos (Quill usa iframes para videos)
const videos = doc.querySelectorAll('iframe.ql-video, iframe[class*="ql-video"]');
videos.forEach((iframe) => {
// Crear wrapper para video
const wrapper = doc.createElement('div');
wrapper.className = 'video-wrapper';
// Clonar el iframe y ajustar estilos
const clonedIframe = iframe.cloneNode(true) as HTMLIFrameElement;
// Insertar en wrapper y reemplazar
wrapper.appendChild(clonedIframe);
if (iframe.parentNode) {
iframe.parentNode.replaceChild(wrapper, iframe);
}
});
// También buscar divs que ya puedan ser videos de Quill
const videoDivs = doc.querySelectorAll('div.ql-video, div[class*="ql-video"]');
videoDivs.forEach((div) => {
const wrapper = doc.createElement('div');
wrapper.className = 'video-wrapper';
const clonedContent = div.cloneNode(true);
wrapper.appendChild(clonedContent);
if (div.parentNode) {
div.parentNode.replaceChild(wrapper, div);
}
});
return doc.body.innerHTML;
};