import { recursivePayload, connectRecursion } from './recursion.js'; // Inert parsing only; inscription markup is never attached to the parent DOM. // The deliberately small allowlist also removes navigation (including meta refresh). const tags = new Set('p div span h1 h2 h3 h4 h5 h6 br hr pre code b strong i em u s ul ol li table thead tbody tr td th blockquote svg g path rect circle ellipse line polyline polygon text tspan defs lineargradient radialgradient stop'.split(' ')); const attrs = new Set('style title width height viewbox fill stroke stroke-width d x y x1 y1 x2 y2 cx cy r rx ry points transform opacity offset stop-color font-size text-anchor'.split(' ')); export function preview(item, api = null) { if (api && item.id) { const response = api.request(`/content/${item.id}`); if (response.status !== 200) { const error = document.createElement('pre'); error.textContent = `${response.status}: ${response.body.error}`; return error; } item = { ...item, bytes: response.body, contentType: response.contentType }; } const bytes = new Uint8Array(item.bytes); const type = item.contentType.split(';')[0]; const text = new TextDecoder().decode(bytes); if (type === 'text/plain' || type === 'application/json') { const node = document.createElement('pre'); node.textContent = text; return node; } let node, blob; if (type === 'text/html' || type === 'image/svg+xml') { const inert = document.implementation.createHTMLDocument(''); const template = inert.createElement('template'); template.innerHTML = text; const scripts = [...template.content.querySelectorAll('script')].filter(s => !s.src).map(s => s.textContent); const clean = parent => { for (const child of [...parent.children]) { if (!tags.has(child.localName.toLowerCase())) { child.remove(); continue; } for (const attr of [...child.attributes]) if (!attrs.has(attr.name.toLowerCase())) child.removeAttribute(attr.name); clean(child); } }; clean(template.content); if (api && item.id && scripts.length) { node = document.createElement('iframe'); node.setAttribute('sandbox', 'allow-scripts'); node.title = `${type} recursive preview`; node.referrerPolicy = 'no-referrer'; const payload = recursivePayload(template.innerHTML, scripts, item.id); node.onload = () => { node.cleanup?.(); node.cleanup = connectRecursion(node, api, payload); }; node.src = '/recursive-preview.html'; node.dataset.previewUrl = ''; node.className = 'inscription-preview'; return node; } const csp = "default-src 'none'; style-src 'unsafe-inline'; script-src 'none'; form-action 'none'; base-uri 'none'"; blob = new Blob([``, template.innerHTML], { type: 'text/html' }); node = document.createElement('iframe'); node.setAttribute('sandbox', ''); node.title = `${type} safe preview`; node.referrerPolicy = 'no-referrer'; } else { blob = new Blob([bytes], { type }); node = document.createElement('img'); node.alt = `${type} inscription`; } const url = URL.createObjectURL(blob); node.src = url; node.dataset.previewUrl = url; node.className = 'inscription-preview'; return node; } export function clearPreviews(root) { root.querySelectorAll('[data-preview-url]').forEach(node => { node.cleanup?.(); URL.revokeObjectURL(node.dataset.previewUrl); }); }