{"version":3,"file":"Iframe.mjs","names":[],"sources":["../../src/HtmlPreview/Iframe.tsx"],"sourcesContent":["'use client';\n\nimport { createStyles, cx } from 'antd-style';\nimport { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';\n\nimport { buildShellSrcDoc, SHELL_UPDATE_MESSAGE_TYPE } from './buildShellSrcDoc';\nimport { buildStaticSrcDoc } from './buildStaticSrcDoc';\nimport { DEFAULT_HEIGHT, DEFAULT_SANDBOX, SRCDOC_MAX_LENGTH } from './const';\nimport { AUTO_HEIGHT_MESSAGE_TYPE } from './injectAutoHeightScript';\nimport type { HtmlPreviewIframeProps } from './type';\n\nconst useStyles = createStyles(({ css, cssVar }) => ({\n  fallback: css`\n    padding: 16px;\n    font-size: 13px;\n    color: ${cssVar.colorTextDescription};\n  `,\n  iframe: css`\n    display: block;\n    width: 100%;\n    border: none;\n    background: transparent;\n  `,\n}));\n\ninterface Payload {\n  bodyHtml: string;\n  /**\n   * Non-inline-style children of `<head>` serialised in document order:\n   * `<script src=…>`, `<script>…</script>`, `<link>`, `<meta>`, `<base>`,\n   * `<title>` etc. The shell appends/dedupes these into its own head so\n   * head-loaded resources (Tailwind CDN, p5.js, fonts, …) work for full\n   * documents. Inline `<style>` is intentionally excluded — those flow\n   * through `styleContent` so streaming partial CSS grows in place rather\n   * than stacking duplicate `<style>` blocks.\n   *\n   * Empty until the user's `<head>` is *sealed* (a `</head>` close tag has\n   * arrived, or `<body>` has opened — browsers auto-close head at that\n   * point). Holding off prevents partial `src=\"https://cd\"` URLs from\n   * being mounted and 404-ing while the model is still streaming.\n   */\n  headExtrasHtml: string;\n  styleContent: string;\n}\n\n// Head is \"sealed\" as soon as we see a close tag or the body has begun;\n// after that point, additional chunks land in body and head extras won't\n// change.\nconst headSealedPattern = /<\\/head\\s*>|<body[\\s>]/i;\nconst isHeadSealed = (raw: string): boolean => headSealedPattern.test(raw);\n\nconst parseContent = (() => {\n  // Lazy-init: only need one parser instance, and only in the browser.\n  let parser: DOMParser | null = null;\n  return (content: string): Payload | null => {\n    if (typeof window === 'undefined') return null;\n    if (!content) return { bodyHtml: '', headExtrasHtml: '', styleContent: '' };\n    if (!parser) parser = new DOMParser();\n    const doc = parser.parseFromString(content, 'text/html');\n\n    const styleParts: string[] = [];\n    const headExtras: string[] = [];\n\n    if (doc.head) {\n      for (const child of Array.from(doc.head.children)) {\n        if (child.tagName === 'STYLE') {\n          styleParts.push(child.textContent || '');\n        } else {\n          headExtras.push(child.outerHTML);\n        }\n      }\n    }\n\n    return {\n      bodyHtml: doc.body ? doc.body.innerHTML : '',\n      headExtrasHtml: isHeadSealed(content) ? headExtras.join('') : '',\n      styleContent: styleParts.join('\\n'),\n    };\n  };\n})();\n\nexport const HtmlPreviewIframe = memo<HtmlPreviewIframeProps>(\n  ({\n    animated,\n    background,\n    content,\n    className,\n    defaultHeight = DEFAULT_HEIGHT,\n    ref,\n    sandbox = DEFAULT_SANDBOX,\n    style,\n    title = 'HTML preview',\n  }) => {\n    const { styles } = useStyles();\n    const innerRef = useRef<HTMLIFrameElement | null>(null);\n    const frameId = useId();\n    const [height, setHeight] = useState<number>(defaultHeight);\n    // Track caller-supplied `defaultHeight` in a ref so the (frameId-keyed)\n    // message handler can floor auto-height updates without re-subscribing\n    // every render.\n    const defaultHeightRef = useRef(defaultHeight);\n    useEffect(() => {\n      defaultHeightRef.current = defaultHeight;\n    }, [defaultHeight]);\n\n    const tooLarge = content.length > SRCDOC_MAX_LENGTH;\n\n    // ── Static mode ─────────────────────────────────────────────────────\n    // When the content isn't being streamed we can hand the iframe the\n    // user's HTML directly. The browser's normal HTML parser runs:\n    //   <script src=…> tags fetch and execute as if on a regular page,\n    //   inline <script> blocks run in DOM order, MutationObservers (like\n    //   Tailwind Play CDN's) get the document at its expected lifecycle\n    //   stage. Anything that a model can produce as a standalone web\n    //   page works without special handling on our side.\n    const staticSrcDoc = useMemo(() => {\n      if (animated || tooLarge) return null;\n      return buildStaticSrcDoc({ background, content, frameId });\n    }, [animated, background, content, frameId, tooLarge]);\n\n    // ── Shell mode ─────────────────────────────────────────────────────\n    // For streaming we keep one shell iframe loaded for the lifetime of\n    // the session and pump content updates through postMessage. The\n    // shell's morph script handles in-place DOM diffing + fade-in for\n    // new nodes (see buildShellSrcDoc.ts). Tradeoff: external <script\n    // src> tags appended this way don't always integrate cleanly with\n    // class-engine CDNs, so static content is preferred when possible.\n    const shellSrcDoc = useMemo(() => {\n      if (!animated || tooLarge) return null;\n      return buildShellSrcDoc({ background, frameId });\n    }, [animated, background, frameId, tooLarge]);\n\n    const [shellReady, setShellReady] = useState(false);\n    useEffect(() => {\n      // Each time we swap between shell and static modes (or rebuild the\n      // shell because the theme changed) we need to wait for a fresh\n      // ready ping before posting content.\n      setShellReady(false);\n    }, [shellSrcDoc]);\n\n    const payload = useMemo<Payload | null>(() => {\n      if (!animated || tooLarge) return null;\n      return parseContent(content);\n    }, [animated, content, tooLarge]);\n\n    // Push content into the shell iframe whenever it changes — but only\n    // after the shell has signalled ready, so its listener exists.\n    useEffect(() => {\n      if (!animated) return;\n      if (!shellReady || !payload) return;\n      const win = innerRef.current?.contentWindow;\n      if (!win) return;\n      win.postMessage(\n        {\n          frameId,\n          payload,\n          type: SHELL_UPDATE_MESSAGE_TYPE,\n        },\n        '*',\n      );\n    }, [animated, payload, shellReady, frameId]);\n\n    useEffect(() => {\n      const handler = (event: MessageEvent) => {\n        const data = event.data;\n        if (!data || typeof data !== 'object') return;\n        if (data.frameId !== frameId) return;\n        if (event.source !== innerRef.current?.contentWindow) return;\n\n        if (data.type === `${SHELL_UPDATE_MESSAGE_TYPE}:ready`) {\n          setShellReady(true);\n          return;\n        }\n\n        if (data.type === AUTO_HEIGHT_MESSAGE_TYPE) {\n          const next = Number(data.height);\n          if (!Number.isFinite(next) || next <= 0) return;\n          // Floor at `defaultHeight`. During streaming the shell body\n          // briefly reports a small height between morph commits (empty\n          // body just after head closes, then partial body, etc.) and on\n          // every iframe remount the auto-height starts at body padding\n          // before climbing back to content height. Letting the iframe\n          // shrink to that interim height causes a visible up/down jitter\n          // that reads as flicker — especially under a Markdown wrapper\n          // that re-renders every chunk. Anchoring to the caller's stated\n          // minimum height eliminates that without affecting the final\n          // size: real content taller than `defaultHeight` grows the\n          // iframe; content shorter than it stays at the floor.\n          const floored = Math.max(next, defaultHeightRef.current);\n          setHeight((prev) => (Math.abs(prev - floored) < 1 ? prev : floored));\n        }\n      };\n\n      window.addEventListener('message', handler);\n      return () => window.removeEventListener('message', handler);\n    }, [frameId]);\n\n    const setRef = useCallback(\n      (node: HTMLIFrameElement | null) => {\n        innerRef.current = node;\n        if (typeof ref === 'function') ref(node);\n        else if (ref) (ref as { current: HTMLIFrameElement | null }).current = node;\n      },\n      [ref],\n    );\n\n    if (tooLarge) {\n      return (\n        <div className={cx(styles.fallback, className)} style={style}>\n          Content too large to preview inline.\n        </div>\n      );\n    }\n\n    const srcDoc = staticSrcDoc ?? shellSrcDoc ?? '';\n\n    // Key the iframe by mode so React fully unmounts the previous DOM\n    // element when we switch from shell (streaming) to static (finalised).\n    // Setting iframe.srcdoc on an already-loaded element doesn't reliably\n    // re-navigate in Chromium when the previous document was also srcdoc-\n    // based — the new srcdoc attribute lands, but the document doesn't\n    // reload, so the user sees stale (often empty) shell content. A fresh\n    // element forces the browser to parse and load the new srcdoc.\n    const iframeKey = animated ? 'shell' : 'static';\n\n    return (\n      <iframe\n        className={cx(styles.iframe, className)}\n        key={iframeKey}\n        ref={setRef}\n        sandbox={sandbox}\n        srcDoc={srcDoc}\n        style={{ height, ...style }}\n        title={title}\n      />\n    );\n  },\n);\n\nHtmlPreviewIframe.displayName = 'HtmlPreviewIframe';\n\nexport default HtmlPreviewIframe;\n"],"mappings":"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"}