2026-10-05 03:57:01 +00:00
import { recursivePayload , connectRecursion } from './recursion.js' ;
2026-10-05 01:23:21 +00:00
// 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 ( ' ' ));
2026-10-05 03:57:01 +00:00
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 };
}
2026-10-05 01:23:21 +00:00
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 ;
2026-10-05 03:57:01 +00:00
const scripts = [... template . content . querySelectorAll ( 'script' )]. filter ( s => ! s . src ). map ( s => s . textContent );
2026-10-05 01:23:21 +00:00
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 );
2026-10-05 03:57:01 +00:00
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 ;
}
2026-10-05 01:23:21 +00:00
const csp = "default-src 'none'; style-src 'unsafe-inline'; script-src 'none'; form-action 'none'; base-uri 'none'" ;
blob = new Blob ([ `<!doctype html><meta http-equiv="Content-Security-Policy" content=" ${ csp } ">` , 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 ) {
2026-10-05 03:57:01 +00:00
root . querySelectorAll ( '[data-preview-url]' ). forEach ( node => { node . cleanup ? .(); URL . revokeObjectURL ( node . dataset . previewUrl ); });
2026-10-05 01:23:21 +00:00
}