174 lines
5.8 KiB
TypeScript
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;
|
|
}; |