<?xml version="1.0" encoding="UTF-8"?>
<feature name="svg-region-renderer" version="0.1">
  <description>
    Reader for &lt;svg-region&gt; regions. Renders the embedded SVG inline (or in a
    sandboxed iframe srcdoc for untrusted content), verifies &lt;captured-hash&gt; against
    the embedded &lt;svg-content&gt; CDATA via WebCrypto SHA-256, and sanitizes by default
    (strips &lt;script&gt;, &lt;foreignObject&gt;, and event handler attributes). Surfaces
    metadata above the rendered SVG: title, source, captured-at, captured-by, purpose,
    sandbox mode, sanitization mode.
  </description>
  <requires>
    <feature name="registries"/>
    <feature name="readers-base"/>
  </requires>
  <provides>
    <reader name="svg-region"/>
  </provides>
  <implementation lang="js"><![CDATA[
    const Readers = MR.registry.get('readers');

    function findChild(r, tagName) {
      for (const c of r.children) {
        if (c.tagName() === tagName) return c;
      }
      return null;
    }
    function findChildText(r, tagName) {
      const c = findChild(r, tagName);
      return c ? c.directText().trim() : '';
    }
    // Prefer CDATA content (semantic intent: exact bytes). Fall back to text.
    function directTextWithCdata(region) {
      let cdata = '';
      let txt = '';
      const children = region.element.childNodes;
      for (let i = 0; i < children.length; i++) {
        const n = children[i];
        if (n.nodeType === 4) cdata += n.nodeValue;
        else if (n.nodeType === 3) txt += n.nodeValue;
      }
      return cdata || txt;
    }
    async function sha256Hex(text) {
      const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
      return Array.from(new Uint8Array(buf)).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
    }
    function esc(s) {
      return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    }

    // Sanitize SVG via DOMParser walk:
    //   strict: strip <script>, <foreignObject>, all on* event handlers, external xlink:href
    //   loose: strip <script> only, keep everything else
    //   none: render as-is (use only for fully-trusted content)
    function sanitizeSvgString(svgStr, mode) {
      if (mode === 'none') return { svg: svgStr, removed: [] };
      const parser = new DOMParser();
      const doc = parser.parseFromString(svgStr, 'image/svg+xml');
      // parsererror check — fail safely: refuse to render rather than pass through unsafe content
      const parseError = doc.querySelector('parsererror');
      if (parseError) return { svg: '', removed: ['parse-error · refused to render unsafe content'] };
      const removed = [];

      function walk(node) {
        if (!node) return;
        const children = Array.from(node.childNodes);
        for (const child of children) {
          if (child.nodeType !== 1) continue; // element only
          const tag = child.tagName.toLowerCase();
          // Strip dangerous elements
          if (tag === 'script') { node.removeChild(child); removed.push('script'); continue; }
          if (mode === 'strict' && tag === 'foreignobject') { node.removeChild(child); removed.push('foreignObject'); continue; }
          // Strip event handler attributes
          for (const attr of Array.from(child.attributes)) {
            const name = attr.name.toLowerCase();
            if (name.startsWith('on')) { child.removeAttribute(attr.name); removed.push('on*:' + tag); }
            if (mode === 'strict' && (name === 'href' || name === 'xlink:href')) {
              const v = attr.value || '';
              if (v.startsWith('http:') || v.startsWith('https:') || v.startsWith('//')) {
                child.removeAttribute(attr.name); removed.push('href:' + tag);
              }
            }
          }
          walk(child);
        }
      }
      walk(doc.documentElement);
      const serialized = new XMLSerializer().serializeToString(doc.documentElement);
      return { svg: serialized, removed: removed };
    }

    Readers.register({
      name: 'svg-region',
      priority: 10,
      claims: function(r) { return r.tagName() === 'svg-region'; },
      read: async function(r) {
        return {
          type: 'mount',
          mount: async function(target) {
            const source = findChild(r, 'source');
            const svgContent = findChild(r, 'svg-content');
            const render = findChild(r, 'render');
            const provenance = findChild(r, 'provenance');

            const title = findChildText(r, 'title');
            const description = findChildText(r, 'description');
            const url = source ? findChildText(source, 'url') : '';
            const capturedAt = source ? findChildText(source, 'captured-at') : '';
            const capturedHashRaw = source ? findChildText(source, 'captured-hash') : '';
            const capturedHashHex = capturedHashRaw.replace(/^sha256:/i, '');
            const width = (render ? findChildText(render, 'width') : '') || '100%';
            const height = (render ? findChildText(render, 'height') : '') || 'auto';
            const sanitize = (render ? findChildText(render, 'sanitize') : '') || 'strict';
            const prefer = (render ? findChildText(render, 'prefer') : '') || 'inline';
            const capturedBy = provenance ? findChildText(provenance, 'captured-by') : '';
            const purpose = provenance ? findChildText(provenance, 'purpose') : '';

            const rawSvg = svgContent ? directTextWithCdata(svgContent) : '';

            // Verify hash BEFORE sanitization (hash is over the original bytes)
            let hashStatus = '';
            if (rawSvg && capturedHashHex) {
              const computed = await sha256Hex(rawSvg);
              if (computed === capturedHashHex.toLowerCase()) {
                hashStatus = '<span style="color:#7eb87e;">✓ matches embedded SVG content</span>';
              } else {
                hashStatus = '<span style="color:#c97a7a;">✗ MISMATCH (computed ' + esc(computed.slice(0, 16)) + '…)</span>';
              }
            } else if (rawSvg && !capturedHashHex) {
              hashStatus = '<span style="color:#c97a7a;">svg-content present but no captured-hash</span>';
            }

            // Sanitize for rendering (does not affect hash)
            const sanitizeResult = rawSvg ? sanitizeSvgString(rawSvg, sanitize) : { svg: '', removed: [] };
            const cleanSvg = sanitizeResult.svg;
            const stripped = sanitizeResult.removed;

            target.innerHTML = '';
            target.style.whiteSpace = 'normal';
            target.style.fontFamily = 'inherit';

            const wrap = document.createElement('div');
            wrap.style.cssText = 'font-family:var(--mono);font-size:12px;color:var(--ink);';

            if (title) {
              const t = document.createElement('div');
              t.style.cssText = 'font-family:Georgia,serif;font-size:15px;color:var(--accent,#d4a574);font-style:italic;margin-bottom:6px;';
              t.textContent = title;
              wrap.appendChild(t);
            }
            if (description) {
              const d = document.createElement('div');
              d.style.cssText = 'font-family:Georgia,serif;font-size:13px;color:var(--ink-soft,#a8b0bb);margin-bottom:10px;line-height:1.5;';
              d.textContent = description;
              wrap.appendChild(d);
            }

            const meta = document.createElement('div');
            meta.style.cssText = 'font-family:var(--mono);font-size:10px;line-height:1.6;color:var(--ink-soft,#a8b0bb);border-left:2px solid var(--rule,#232930);padding:6px 10px;margin-bottom:10px;';
            const rows = [];
            if (url) rows.push(['source', '<a href="' + esc(url) + '" target="_blank" style="color:#8fa9c4;">' + esc(url) + '</a>']);
            if (capturedAt) rows.push(['captured-at', esc(capturedAt)]);
            if (capturedHashRaw) rows.push(['captured-hash', '<span style="word-break:break-all;">sha256:' + esc(capturedHashHex.slice(0, 24)) + '…</span> ' + hashStatus]);
            if (capturedBy) rows.push(['captured-by', esc(capturedBy)]);
            if (purpose) rows.push(['purpose', esc(purpose)]);
            rows.push(['render', prefer === 'sandboxed' ? 'iframe srcdoc · sandboxed · no network' : 'inline DOM · scalable graphic']);
            rows.push(['sanitize', sanitize + (stripped.length ? ' · removed ' + stripped.length + ' (' + esc(stripped.slice(0, 4).join(', ')) + (stripped.length > 4 ? '…' : '') + ')' : ' · nothing stripped')]);
            rows.push(['dimensions', esc(width) + ' × ' + esc(height)]);

            meta.innerHTML = rows.map(function(row) {
              return '<div><span style="color:#5e6770;text-transform:uppercase;letter-spacing:.1em;">' + row[0] + ':</span> ' + row[1] + '</div>';
            }).join('');
            wrap.appendChild(meta);

            // Render the SVG
            const svgWrap = document.createElement('div');
            svgWrap.style.cssText = 'width:' + width + ';background:white;padding:12px;border:1px solid #232930;display:flex;align-items:center;justify-content:center;';
            if (height && height !== 'auto') svgWrap.style.minHeight = height;

            if (!cleanSvg) {
              svgWrap.innerHTML = '<div style="color:#5e6770;font-style:italic;padding:20px;text-align:center;">— no svg-content —</div>';
            } else if (prefer === 'sandboxed') {
              const iframe = document.createElement('iframe');
              iframe.setAttribute('sandbox', '');
              iframe.referrerPolicy = 'no-referrer';
              iframe.style.cssText = 'width:100%;border:0;display:block;background:white;';
              iframe.style.height = (height && height !== 'auto') ? height : '300px';
              iframe.srcdoc = '<!DOCTYPE html><html><body style="margin:0;padding:0;display:flex;align-items:center;justify-content:center;">' + cleanSvg + '</body></html>';
              svgWrap.style.padding = '0';
              svgWrap.appendChild(iframe);
            } else {
              // Inline: insert SVG directly into the DOM
              svgWrap.innerHTML = cleanSvg;
              const svgEl = svgWrap.querySelector('svg');
              if (svgEl) {
                svgEl.style.maxWidth = '100%';
                if (height && height !== 'auto') svgEl.style.maxHeight = height;
              }
            }
            wrap.appendChild(svgWrap);

            target.appendChild(wrap);
          }
        };
      }
    });

    MR.console.log('READER-REG svg-region (priority 10)', 'info');
  ]]></implementation>
</feature>
