Files
NightSpire/web-next/lib/forum-sanitize.ts
T
Inna 799341a1a4 Foro: ítems de wowhead sin parpadeo y con color de calidad correcto
Dos problemas restantes con los enlaces de wowhead:

1) Texto blanco en vez del color de calidad. theme.css solo define q3..q6, así que
   un ítem de calidad 0/1/2 (p.ej. verde=q2) se quedaba sin color y salía blanco.
   Se completan .q0/.q1/.q2/.q7 en forum.css (colores estándar de WoW) con
   !important para ganar al color del enlace. Además el saneador deja los enlaces
   de wowhead SIN nofollow (el script de wowhead ignora los nofollow al colorear).

2) Parpadeo de «item=xxx» al recargar. Salía porque el texto del enlace era un
   marcador que wowhead renombraba de forma asíncrona. Ahora el botón resuelve el
   NOMBRE y la CALIDAD en el servidor (nuevo /api/forum/wowhead, que consulta el
   endpoint de tooltip de wowhead en la rama WotLK y el idioma de la web) e inserta
   el enlace ya con su nombre real y su clase qN: sin parpadeo y ya coloreado. Si la
   API falla, se cae al modo anterior (data-wh-rename-link).

El editor añade los colores de calidad a su content_style para que el preview salga
igual. El icono lo sigue poniendo wowhead (iconizeLinks) tanto en el editor como al
postear.

Verificado en producción: el endpoint devuelve nombre (es), calidad e icono; el CSS
sirve q2; un post con nombre real + q2 se guarda sin nofollow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:26:09 +00:00

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(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim().length
}