<!-- Chrome MHTML fullscreen - render raw MHTML through the same viewer approach as the browser extension -->
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>MHTML Preview</title>
  <style>
    html, body, iframe { width: 100%; height: 100%; margin: 0; border: 0; background: #fff; }
    .mhtml-status { box-sizing: border-box; padding: 14px 16px; font: 13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; color: #333; }
    .mhtml-status.error { color: #8a1f11; background: #fff4f1; }
  </style>
</head>
<body data-output-path="{{ output_path }}">
  <div class="mhtml-status">Loading {{ output_path_raw|default:"snapshot.mhtml" }}...</div>
  <iframe hidden class="full-page-iframe" sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"></iframe>
  <script>
  (() => {
    const outputPath = document.body.dataset.outputPath || ''

    function splitHeaderAndBody(value) {
      const normalized = value.replace(/\r\n/g, "\n")
      const boundary = normalized.indexOf("\n\n")
      if (boundary === -1) return { headerText: "", body: normalized }
      return { headerText: normalized.slice(0, boundary), body: normalized.slice(boundary + 2) }
    }
    function parseMimeHeaders(headerText) {
      const unfolded = []
      for (const line of headerText.split("\n")) {
        if (/^\s/.test(line) && unfolded.length) unfolded[unfolded.length - 1] += ` ${line.trim()}`
        else unfolded.push(line)
      }
      return unfolded.reduce((headers, line) => {
        const separator = line.indexOf(":")
        if (separator <= 0) return headers
        headers[line.slice(0, separator).trim().toLowerCase()] = line.slice(separator + 1).trim()
        return headers
      }, {})
    }
    function mimeType(contentType = "") {
      const [type = ""] = contentType.split(";", 1)
      return type.trim().toLowerCase()
    }
    function mimeParameter(value, name) {
      if (!value) return ""
      const match = value.match(new RegExp(`${name}\\s*=\\s*(?:"([^"]+)"|([^;\\s]+))`, "i"))
      return (match && (match[1] || match[2])) || ""
    }
    function parseMhtmlParts(rawMhtml) {
      const top = splitHeaderAndBody(rawMhtml)
      const headers = parseMimeHeaders(top.headerText)
      const boundary = mimeParameter(headers["content-type"], "boundary")
      if (!boundary) throw new Error("MHTML file is missing a multipart boundary")
      const delimiter = `--${boundary}`
      const parts = top.body
        .split(delimiter)
        .slice(1)
        .map((part) => part.replace(/^\n/, "").replace(/\n$/, ""))
        .filter((part) => part.trim() && part.trim() !== "--")
        .map((part) => {
          const section = splitHeaderAndBody(part.replace(/\n--$/, ""))
          return { headers: parseMimeHeaders(section.headerText), body: section.body }
        })
      if (!parts.length) throw new Error("MHTML file does not contain any parts")
      return { headers, parts }
    }
    function base64ToBytes(value) {
      const binary = atob(value.replace(/\s/g, ""))
      const bytes = new Uint8Array(binary.length)
      for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
      return bytes
    }
    function bytesToBase64(bytes) {
      const chunkSize = 0x8000
      let binary = ""
      for (let index = 0; index < bytes.length; index += chunkSize) binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize))
      return btoa(binary)
    }
    function quotedPrintableToBytes(value) {
      const normalized = value.replace(/=\n/g, "")
      const bytes = []
      for (let index = 0; index < normalized.length; index += 1) {
        const char = normalized[index]
        const hex = normalized.slice(index + 1, index + 3)
        if (char === "=" && /^[0-9a-f]{2}$/i.test(hex)) {
          bytes.push(parseInt(hex, 16))
          index += 2
          continue
        }
        const codePoint = normalized.charCodeAt(index)
        if (codePoint <= 0xff) bytes.push(codePoint)
        else bytes.push(...new TextEncoder().encode(char))
      }
      return new Uint8Array(bytes)
    }
    function decodeMhtmlPart(part) {
      const transferEncoding = (part.headers["content-transfer-encoding"] || "").trim().toLowerCase()
      const bytes = transferEncoding === "base64"
        ? base64ToBytes(part.body)
        : transferEncoding === "quoted-printable"
          ? quotedPrintableToBytes(part.body)
          : new TextEncoder().encode(part.body)
      return {
        ...part,
        bytes,
        contentId: (part.headers["content-id"] || "").trim().replace(/^<|>$/g, ""),
        location: (part.headers["content-location"] || "").trim(),
        mimeType: mimeType(part.headers["content-type"]),
        text: new TextDecoder("utf-8").decode(bytes),
      }
    }
    function makeDataUrl(mime, bytes) {
      return `data:${mime || "application/octet-stream"};base64,${bytesToBase64(bytes)}`
    }
    function addResourceAlias(resources, alias, dataUrl, baseUrl) {
      if (!alias) return
      resources.set(alias, dataUrl)
      try { resources.set(new URL(alias, baseUrl).href, dataUrl) } catch {}
      try { resources.set(decodeURI(alias), dataUrl) } catch {}
    }
    function addResourceAliases(resources, part, dataUrl, baseUrl) {
      addResourceAlias(resources, part.location, dataUrl, baseUrl)
      addResourceAlias(resources, part.contentId, dataUrl, baseUrl)
      if (part.contentId) addResourceAlias(resources, `cid:${part.contentId}`, dataUrl, baseUrl)
    }
    function lookupResource(resources, value, baseUrl) {
      if (!value || /^(?:data|blob|javascript):/i.test(value)) return null
      const candidates = new Set([value])
      try { candidates.add(new URL(value, baseUrl).href) } catch {}
      try { candidates.add(decodeURI(value)) } catch {}
      for (const candidate of candidates) {
        const direct = resources.get(candidate)
        if (direct) return direct
        const hashless = resources.get(candidate.replace(/#.*$/, ""))
        if (hashless) return hashless
      }
      return null
    }
    function rewriteCssUrls(css, baseUrl, resources) {
      return css.replace(/url\(\s*(["']?)([^"')]+)\1\s*\)/g, (match, _quote, url) => {
        const replacement = lookupResource(resources, url.trim(), baseUrl)
        return replacement ? `url("${replacement}")` : match
      })
    }
    function rewriteSrcset(value, baseUrl, resources) {
      return value.split(",").map((candidate) => {
        const tokens = candidate.trim().split(/\s+/)
        const url = tokens.shift()
        if (!url) return candidate
        const replacement = lookupResource(resources, url, baseUrl)
        return [replacement || url, ...tokens].join(" ")
      }).join(", ")
    }
    function htmlDataUrl(html) {
      return makeDataUrl("text/html;charset=utf-8", new TextEncoder().encode(html))
    }
    function rewriteHtml(rawHtml, baseUrl, resources) {
      const doc = new DOMParser().parseFromString(rawHtml, "text/html")
      for (const element of [...doc.querySelectorAll("[src]")]) {
        const replacement = lookupResource(resources, element.getAttribute("src") || "", baseUrl)
        if (replacement) element.setAttribute("src", replacement)
      }
      for (const element of [...doc.querySelectorAll("[href]")]) {
        const value = element.getAttribute("href") || ""
        const replacement = lookupResource(resources, value, baseUrl)
        if (replacement) element.setAttribute("href", replacement)
        else if (element.tagName.toLowerCase() === "a") {
          element.setAttribute("target", "_blank")
          element.setAttribute("rel", "noreferrer")
        }
      }
      for (const element of [...doc.querySelectorAll("[poster]")]) {
        const replacement = lookupResource(resources, element.getAttribute("poster") || "", baseUrl)
        if (replacement) element.setAttribute("poster", replacement)
      }
      for (const element of [...doc.querySelectorAll("[srcset]")]) {
        element.setAttribute("srcset", rewriteSrcset(element.getAttribute("srcset") || "", baseUrl, resources))
      }
      for (const element of [...doc.querySelectorAll("[style]")]) {
        element.setAttribute("style", rewriteCssUrls(element.getAttribute("style") || "", baseUrl, resources))
      }
      for (const style of [...doc.querySelectorAll("style")]) {
        style.textContent = rewriteCssUrls(style.textContent || "", baseUrl, resources)
      }
      if (!doc.querySelector("base")) {
        const base = doc.createElement("base")
        base.href = baseUrl
        base.target = "_blank"
        doc.head.prepend(base)
      }
      return { html: `<!doctype html>\n${doc.documentElement.outerHTML}`, title: doc.title || "" }
    }
    function buildHtmlPartRenderer(htmlParts, resourceUrls, sourceUrl) {
      const renderedHtmlUrls = new Map()
      function renderHtmlPart(part, stack) {
        const cached = renderedHtmlUrls.get(part)
        if (cached) return cached
        if (stack.has(part)) return htmlDataUrl(part.text)
        stack.add(part)
        const baseUrl = part.location || sourceUrl
        const resources = new Map(resourceUrls)
        for (const otherPart of htmlParts) {
          if (otherPart !== part) addResourceAliases(resources, otherPart, renderHtmlPart(otherPart, stack), baseUrl)
        }
        const rendered = rewriteHtml(part.text, baseUrl, resources)
        const dataUrl = htmlDataUrl(rendered.html)
        renderedHtmlUrls.set(part, dataUrl)
        stack.delete(part)
        return dataUrl
      }
      return (part) => renderHtmlPart(part, new Set())
    }
    function escapeHtml(value) {
      return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;")
    }
    function findRootHtmlPart(parts, sourceUrl) {
      const htmlParts = parts.filter((part) => part.mimeType === "text/html")
      const fallback = htmlParts[0] || parts[0]
      if (!fallback) throw new Error("MHTML file does not contain any readable parts")
      let sourceHref = sourceUrl
      try { sourceHref = new URL(sourceUrl).href } catch {}
      return htmlParts.find((part) => {
        try { return new URL(part.location || sourceUrl, sourceUrl).href === sourceHref }
        catch { return part.location === sourceUrl }
      }) || fallback
    }
    function renderMhtmlToHtml(rawMhtml, sourceUrl) {
      const decodedParts = parseMhtmlParts(rawMhtml).parts.map(decodeMhtmlPart)
      const rootPart = findRootHtmlPart(decodedParts, sourceUrl)
      const baseUrl = rootPart.location || sourceUrl
      const resources = new Map()
      const htmlParts = decodedParts.filter((part) => part.mimeType === "text/html")
      for (const part of decodedParts) {
        if (part !== rootPart && part.mimeType !== "text/html") addResourceAliases(resources, part, makeDataUrl(part.mimeType, part.bytes), baseUrl)
      }
      for (const part of decodedParts) {
        if (part !== rootPart && part.mimeType === "text/css") {
          const rewrittenCss = rewriteCssUrls(part.text, baseUrl, resources)
          addResourceAliases(resources, part, makeDataUrl("text/css", new TextEncoder().encode(rewrittenCss)), baseUrl)
        }
      }
      const renderHtmlPart = buildHtmlPartRenderer(htmlParts, resources, sourceUrl)
      for (const part of htmlParts) {
        if (part !== rootPart) addResourceAliases(resources, part, renderHtmlPart(part), baseUrl)
      }
      if (rootPart.mimeType === "text/html") return rewriteHtml(rootPart.text, baseUrl, resources)
      return { html: `<!doctype html><html><head><title>MHTML Snapshot</title></head><body><pre>${escapeHtml(rootPart.text)}</pre></body></html>`, title: "MHTML Snapshot" }
    }
    function previewUrlFor(rawUrl) {
      const url = new URL(rawUrl.href)
      url.searchParams.set("preview", "1")
      return url.href
    }
    function rawUrlForCurrentPage() {
      const url = new URL(window.location.href)
      url.searchParams.delete("preview")
      return url
    }
    async function main() {
      const status = document.querySelector(".mhtml-status")
      const frame = document.querySelector("iframe")
      const rawUrl = outputPath ? new URL(outputPath, window.location.href) : rawUrlForCurrentPage()

      if (rawUrl.origin !== window.location.origin && !rawUrl.searchParams.get("preview")) {
        frame.src = previewUrlFor(rawUrl)
        frame.hidden = false
        status.remove()
        return
      }

      const rawMhtml = await fetch(rawUrl.href, { credentials: "same-origin" }).then((response) => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`)
        return response.text()
      })
      let rendered
      try {
        rendered = renderMhtmlToHtml(rawMhtml, rawUrl.href)
      } catch (error) {
        status.className = "mhtml-status error"
        status.textContent = `Unable to render MHTML preview: ${error.message}`
        rendered = { html: `<!doctype html><html><body><pre style="white-space:pre-wrap;word-break:break-word">${escapeHtml(rawMhtml)}</pre></body></html>`, title: "MHTML Snapshot" }
      }
      frame.srcdoc = rendered.html
      frame.hidden = false
      if (rendered.title) document.title = rendered.title
      if (!status.classList.contains("error")) status.remove()
    }
    main().catch((error) => {
      const status = document.querySelector(".mhtml-status")
      status.className = "mhtml-status error"
      status.textContent = `Unable to load MHTML preview: ${error.message}`
    })
  })()
  </script>
</body>
</html>
