/** * Sanitizer für Übungs-Rich-HTML inkl. §11 Platzhalter (span data-shinkan-exercise-media). * DOMPurify mit fester Allowlist. */ import DOMPurify from 'dompurify' import { sanitizeInlineMediaCaption } from './inlineMediaCaption' const ALLOWED_TAGS = ['p', 'div', 'br', 'ul', 'ol', 'li', 'h3', 'b', 'strong', 'i', 'em', 'u', 'span', 'a'] const ALLOWED_ATTR = [ 'href', 'class', 'data-shinkan-exercise-media', 'data-shinkan-exercise-media-size', 'data-shinkan-exercise-media-caption', ] const SIZE_OK = new Set(['small', 'medium', 'full']) function hookAttribute(_node, data) { const name = data.attrName if (name === 'href') { const href = String(data.attrValue || '').trim() if (!/^https?:\/\//i.test(href)) data.keepAttr = false else data.attrValue = href return } if (name === 'class') { const keep = String(data.attrValue || '') .split(/\s+/) .filter((c) => c === 'shinkan-inline-media') if (!keep.length) data.keepAttr = false else data.attrValue = keep.join(' ') return } if (name === 'data-shinkan-exercise-media') { if (!/^\d+$/.test(String(data.attrValue || '').trim())) data.keepAttr = false return } if (name === 'data-shinkan-exercise-media-size') { const size = String(data.attrValue || '').trim().toLowerCase() if (!SIZE_OK.has(size)) data.keepAttr = false else data.attrValue = size return } if (name === 'data-shinkan-exercise-media-caption') { const caption = sanitizeInlineMediaCaption(String(data.attrValue || '')) if (!caption) data.keepAttr = false else data.attrValue = caption } } const instances = new WeakMap() function purifyFor(win) { const existing = instances.get(win) if (existing) return existing const purify = DOMPurify(win) purify.addHook('uponSanitizeAttribute', hookAttribute) instances.set(win, purify) return purify } /** * @param {string|null|undefined} html * @param {Window=} win * @returns {string} */ export function sanitizeExerciseRichDisplayHtml(html, win) { if (html == null || typeof html !== 'string') return '' const trimmed = html.trim() if (!trimmed) return '' const target = win || (typeof window !== 'undefined' ? window : null) if (!target) return '' return purifyFor(target).sanitize(trimmed, { ALLOWED_TAGS, ALLOWED_ATTR, ALLOW_DATA_ATTR: false, }) }