import React, { useMemo } from 'react' import { sanitizeExerciseRichDisplayHtml } from '../utils/exerciseRichTextSanitize' import ExerciseMediaEmbed from './ExerciseMediaEmbed' function isTrashHidden(m) { return String(m?.asset_lifecycle_state || 'active').toLowerCase() === 'trash_hidden' } function buildVisibleMediaMap(mediaList) { const map = new Map() for (const m of mediaList || []) { if (!m || m.id == null || isTrashHidden(m)) continue map.set(Number(m.id), m) } return map } function domToReactNodes(node, exerciseId, mediaById, path) { if (node.nodeType === Node.TEXT_NODE) { const t = node.textContent return t ? t : null } if (node.nodeType !== Node.ELEMENT_NODE) return null const el = node const tag = el.tagName.toLowerCase() const key = path.join('.') if (tag === 'span' && el.getAttribute('data-shinkan-exercise-media')) { const raw = el.getAttribute('data-shinkan-exercise-media') const mid = parseInt(raw, 10) if (!Number.isFinite(mid) || mid < 1) { return ( [Ungültiger Medienverweis] ) } const media = mediaById.get(mid) if (!media) { return ( [Medium nicht verfügbar] ) } const rawSize = (el.getAttribute('data-shinkan-exercise-media-size') || 'medium').toLowerCase().trim() const layoutSize = rawSize === 'small' || rawSize === 'full' ? rawSize : 'medium' const wrapClass = layoutSize === 'small' ? 'shinkan-inline-media-wrap shinkan-inline-media-wrap--sm' : layoutSize === 'full' ? 'shinkan-inline-media-wrap shinkan-inline-media-wrap--full' : 'shinkan-inline-media-wrap shinkan-inline-media-wrap--md' const lc = String(media.asset_lifecycle_state || 'active').toLowerCase() return ( {lc === 'trash_soft' && ( Dieses Medium ist im Papierkorb. )} ) } const children = [] const childNodes = Array.from(el.childNodes) childNodes.forEach((ch, i) => { const sub = domToReactNodes(ch, exerciseId, mediaById, [...path, String(i)]) if (sub != null && sub !== false) children.push(sub) }) const props = { key } if (tag === 'a' && el.getAttribute('href')) { props.href = el.getAttribute('href') props.target = '_blank' props.rel = 'noreferrer' } return React.createElement(tag, props, children.length ? children : null) } /** * Zentraler Anzeige-Pfad für Übungs-Rich-Text inkl. §11 Inline-Medien. * @param {{ html?: string|null, exerciseId?: number|null, media?: object[]|null, className?: string }} props */ export default function ExerciseRichTextBlock({ html, exerciseId, media, className = '' }) { const safe = useMemo(() => sanitizeExerciseRichDisplayHtml(html), [html]) const mediaById = useMemo(() => buildVisibleMediaMap(media), [media]) const body = useMemo(() => { if (!safe.trim()) return null const tpl = document.createElement('template') tpl.innerHTML = safe const nodes = [] Array.from(tpl.content.childNodes).forEach((ch, i) => { const r = domToReactNodes(ch, exerciseId, mediaById, [String(i)]) if (r != null) nodes.push(r) }) return nodes }, [safe, exerciseId, mediaById]) if (!body || body.length === 0) return null return
{body}
}