6856a3736d
El post salía "doble y fatal" porque se ejecutaba wowhead DENTRO del iframe del editor: wowhead mutaba el DOM editable (icono, clase icontinyl y una tabla enorme oculta con el tooltip completo) y TinyMCE serializaba todo eso al publicar, así que la basura se guardaba en el mensaje. Se deja de inyectar wowhead en el editor. Ahora, igual que recruit: - El botón inserta un enlace PLANO con el nombre real como texto (resuelto en el servidor vía /api/forum/wowhead, sin el parpadeo de item=xxx). Nada de <img> ni clases/estilos horneados. - El icono (como fondo, icontinyl) y el color de calidad los añade wowhead en la PÁGINA publicada, donde muta el DOM en vivo sin que se guarde nada. Se revierte el <img> horneado y su CSS (.wh-icon) y el class en img del saneador. Se conservan los colores de calidad q0..q7 en forum.css (los necesita el qN que wowhead pone al vuelo, porque theme.css solo traía q3..q6). Verificado: crear tema funciona y el post se guarda como enlace plano + nombre, sin icontinyl/tabla/img. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.1 KiB
TypeScript
73 lines
3.1 KiB
TypeScript
import sanitizeHtml from 'sanitize-html'
|
|
|
|
// Tipos de entidad de wowhead que aceptan tooltip (para normalizar el enlace).
|
|
const WOWHEAD_TYPES = 'item|spell|quest|npc|achievement|object|faction|currency|itemset|title'
|
|
|
|
/**
|
|
* Normaliza un enlace de wowhead para que el tooltip salga en la rama WotLK.
|
|
* - Mantiene el subdominio (www=inglés, es=español…), que es lo que fija el idioma
|
|
* del tooltip cuando no hay data-wowhead.
|
|
* - Fuerza el segmento `/wotlk/` (si el usuario pega un enlace retail, el tooltip
|
|
* saldría de retail; así siempre es WotLK).
|
|
* Devuelve el href tal cual si no reconoce un `<tipo>=<id>`.
|
|
*/
|
|
function normalizeWowheadHref(href: string): string {
|
|
const m = /^https?:\/\/([a-z]+)\.wowhead\.com\/.*?\b(?:wotlk\/)?(?:(item|spell|quest|npc|achievement|object|faction|currency|itemset|title)=(\d+))/i.exec(
|
|
href,
|
|
)
|
|
if (!m) return href
|
|
const [, sub, type, id] = m
|
|
return `https://${sub}.wowhead.com/wotlk/${type.toLowerCase()}=${id}`
|
|
}
|
|
|
|
// Misma allowlist que forum/sanitize.py (nh3) para el HTML de los mensajes.
|
|
export function cleanPostHtml(html: string): string {
|
|
if (!html) return ''
|
|
return sanitizeHtml(html, {
|
|
allowedTags: [
|
|
'p', 'br', 'hr', 'span', 'div',
|
|
'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'sub', 'sup',
|
|
'ul', 'ol', 'li', 'blockquote', 'code', 'pre',
|
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'a', 'img',
|
|
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
],
|
|
allowedAttributes: {
|
|
// data-wowhead + class dejan que los enlaces de wowhead muestren tooltip,
|
|
// color por calidad e icono (los pinta el script global tooltips.js del layout).
|
|
// data-wh-rename-link deja que wowhead ponga el NOMBRE del ítem/misión/etc.
|
|
// automáticamente (sobrescribe el renameLinks:false global, que existe para no
|
|
// pisar los nombres en español de la tienda). data-wh-icon-size ajusta el icono.
|
|
a: ['href', 'title', 'target', 'data-wowhead', 'data-wh-rename-link', 'data-wh-icon-size', 'class'],
|
|
img: ['src', 'alt', 'title', 'width', 'height'],
|
|
span: ['style', 'data-wowhead'],
|
|
div: ['style'],
|
|
td: ['colspan', 'rowspan'],
|
|
th: ['colspan', 'rowspan'],
|
|
},
|
|
allowedSchemes: ['http', 'https', 'mailto'],
|
|
transformTags: {
|
|
a: (tagName, attribs) => {
|
|
const out: Record<string, string> = { ...attribs }
|
|
const isWowhead = Boolean(out.href && /\.wowhead\.com\//i.test(out.href))
|
|
// Los enlaces de wowhead SIN nofollow: el script de wowhead ignora los
|
|
// nofollow al colorear/iconizar, y así se comportan como en la tienda.
|
|
out.rel = isWowhead ? 'noopener noreferrer' : 'noopener noreferrer nofollow'
|
|
if (isWowhead) {
|
|
out.href = normalizeWowheadHref(out.href)
|
|
out.target = '_blank' // pestaña nueva, como el resto del sitio
|
|
}
|
|
return { tagName, attribs: out }
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
/** Longitud del texto plano (sin etiquetas) — para validar longitud mínima. */
|
|
export function plainLength(html: string): number {
|
|
return sanitizeHtml(html || '', { allowedTags: [], allowedAttributes: {} })
|
|
.replace(/ /g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim().length
|
|
}
|