'use client' import { Icons } from '@duck-docs/components/icons' import { useLiftMode } from '@duck-docs/hooks/use-lift-mode' import { cn } from '@gentleduck/libs/cn' import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@gentleduck/registry-ui/resizable' import { Tabs, TabsContent } from '@gentleduck/registry-ui/tabs' import React from 'react' import type { PanelImperativeHandle } from 'react-resizable-panels' type Block = { name: string container?: { height?: number } highlightedCode: string } // SECURITY: allowlist of tag names PrettyCode emits inside a `
` body. Any
// tag outside this list is dropped — its text children are still recursed
// over so unexpected wrappers don't blank the highlight tree.
const ALLOWED_TAGS = new Set(['span', 'code', 'pre', 'mark', 'br', 'div'])
// SECURITY: allowlist of attributes that may be spread onto a React element
// from parsed highlighter HTML. `on*` handlers, `href`, `src`, `srcset`,
// `xlink:href`, and `formaction` are explicitly NOT here — they're the
// classic vectors for attribute-based XSS when the HTML is attacker-shaped.
const ALLOWED_ATTRS = new Set([
  'class',
  'style',
  'tabindex',
  'data-line',
  'data-highlighted-line',
  'data-highlighted-line-id',
  'data-highlighted-chars',
  'data-chars-id',
  'data-language',
  'data-theme',
  'data-rehype-pretty-code-figure',
  'data-rehype-pretty-code-title',
  'data-line-numbers',
  'data-line-numbers-max-digits',
  'data-line-number-start',
  'data-token',
  'aria-hidden',
  'aria-label',
  'role',
  'id',
])

function toCamelCase(value: string) {
  return value.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
}

function parseInlineStyle(styleText: string): React.CSSProperties {
  const result: React.CSSProperties = {}
  for (const declaration of styleText.split(';')) {
    const trimmed = declaration.trim()
    if (!trimmed) continue
    const separatorIndex = trimmed.indexOf(':')
    if (separatorIndex === -1) continue
    const property = trimmed.slice(0, separatorIndex).trim()
    const value = trimmed.slice(separatorIndex + 1).trim()
    if (!property || !value) continue
    // Forbid `expression(...)` / `javascript:` payloads sneaking through CSS.
    if (/expression\s*\(|javascript\s*:/i.test(value)) continue
    // CSS custom properties (`--foo`) are valid CSSProperties keys; named
    // props get camelCased. Cast through `Record` because
    // `CSSProperties`'s indexed signature only accepts custom props.
    ;(result as Record)[property.startsWith('--') ? property : toCamelCase(property)] = value
  }
  return result
}

function getElementProps(element: HTMLElement): Record {
  const props: Record = {}

  for (const attribute of element.getAttributeNames()) {
    const lower = attribute.toLowerCase()

    // SECURITY: block every `on*` event-handler attribute. PrettyCode never
    // emits these; if one shows up the input is attacker-shaped.
    if (lower.startsWith('on')) continue
    // SECURITY: block URL-bearing attributes outright. The highlight tree
    // has no business with links — anything here is suspect.
    if (lower === 'href' || lower === 'src' || lower === 'srcset' || lower === 'xlink:href' || lower === 'formaction') {
      continue
    }
    if (!ALLOWED_ATTRS.has(lower)) continue

    const value = element.getAttribute(attribute)
    if (value === null) continue

    if (lower === 'class') {
      props.className = value
      continue
    }

    if (lower === 'style') {
      props.style = parseInlineStyle(value)
      continue
    }

    if (lower === 'tabindex') {
      const n = Number(value)
      if (Number.isFinite(n)) props.tabIndex = n
      continue
    }

    props[attribute] = value === '' ? true : value
  }

  return props
}

function renderHtmlNode(node: ChildNode, key: string): React.ReactNode {
  if (node.nodeType === Node.TEXT_NODE) {
    return node.textContent
  }

  if (!(node instanceof HTMLElement)) {
    return null
  }

  const tagName = node.tagName.toLowerCase()
  const children = Array.from(node.childNodes).map((child, index) => renderHtmlNode(child, `${key}-${index}`))

  // SECURITY: tag allowlist. Unknown tags collapse to a fragment so their
  // text descendants still render but the tag itself disappears.
  if (!ALLOWED_TAGS.has(tagName)) {
    return {children}
  }

  return React.createElement(tagName, { key, ...getElementProps(node) }, ...children)
}

function renderHighlightedCode(html: string) {
  const document = new DOMParser().parseFromString(html, 'text/html')
  return Array.from(document.body.childNodes).map((node, index) => renderHtmlNode(node, `highlighted-${index}`))
}

export function CodePreview({ block }: { block: Block & { hasLiftMode: boolean } }) {
  const { isLiftMode } = useLiftMode(block.name)
  const [isLoading, setIsLoading] = React.useState(true)
  const [isMounted, setIsMounted] = React.useState(false)
  const ref = React.useRef(null)
  const renderedCode = React.useMemo(
    () => (isMounted ? renderHighlightedCode(block.highlightedCode) : null),
    [block.highlightedCode, isMounted],
  )

  React.useEffect(() => {
    setIsMounted(true)
  }, [])

  return (
    
      
        
          
            {isLoading ? (
              
                
            ) : null}