/**
 * Utilities for inlining background images as data URLs.
 * @module background
 */

import { getStyle, inlineSingleBackgroundEntry, splitBackgroundImage } from '../utils/helpers.js';
import { cache } from '../core/cache.js'

/**
 * Converts all background images in the cloned element tree to data URLs.
 *
 * @param {Element} source - Original element
 * @param {Element} clone - Cloned element
 * @param {Object} [options={}] - Options for image processing
 * @returns {Promise<void>} Promise that resolves when all background images are processed
 */
export

async function inlineBackgroundImages(source, clone, options = {}) {
  const queue = [[source, clone]];
  const imageProps = [
    "background-image",
    "mask",
    "mask-image", "-webkit-mask-image",
    "mask-source", "mask-box-image-source",
    "mask-border-source",
    "-webkit-mask-box-image-source"
  ];
  while (queue.length) {
    const [srcNode, cloneNode] = queue.shift();
    const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
    if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
    for (const prop of imageProps) {
      const val = style.getPropertyValue(prop);
      if (!val || val === "none") continue;
      const splits = splitBackgroundImage(val);
      const inlined = await Promise.all(
        splits.map(entry => inlineSingleBackgroundEntry(entry, options))
      );
      if (inlined.some(p => p && p !== "none" && !/^url\(undefined/.test(p))) {
        cloneNode.style.setProperty(prop, inlined.join(", "));
      }
    }
    const bgColor = style.getPropertyValue("background-color");
    if (
      bgColor &&
      bgColor !== "transparent" &&
      bgColor !== "rgba(0, 0, 0, 0)"
    ) {
      cloneNode.style.backgroundColor = bgColor;
    }
    const sChildren = Array.from(srcNode.children);
    const cChildren = Array.from(cloneNode.children);
    for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
      queue.push([sChildren[i], cChildren[i]]);
    }
  }
}

/**
 * Utilities for inlining ::before and ::after pseudo-elements.
 * @module pseudo
 */

import { getStyle, snapshotComputedStyle, parseContent, extractURL, safeEncodeURI, fetchImage, inlineSingleBackgroundEntry, splitBackgroundImage } from '../utils/helpers.js';
import { getStyleKey } from '../utils/cssTools.js';
import { iconToImage } from '../modules/fonts.js';
import { isIconFont } from '../modules/iconFonts.js';
import { cache } from '../core/cache.js'

/**
 * Creates elements to represent ::before, ::after, and ::first-letter pseudo-elements, inlining their styles and content.
 *
 * @param {Element} source - Original element
 * @param {Element} clone - Cloned element
 * @param {boolean} compress - Whether to compress style keys
 * @param {boolean} embedFonts - Whether to embed icon fonts as images
 * @returns {Promise} Promise that resolves when all pseudo-elements are processed
 */

export async function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
    try {
      const style = getStyle(source, pseudo);
      if (!style || typeof style[Symbol.iterator] !== "function") continue;
      if (pseudo === "::first-letter") {
        const normal = getComputedStyle(source);
        const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
        if (!isMeaningful) continue;
        const textNode = Array.from(clone.childNodes).find(
          (n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
        );
        if (!textNode) continue;
        const text = textNode.textContent;
        const match = text.match(/^([^\p{L}\p{N}\s]*[\p{L}\p{N}](?:['’])?)/u);
        const first = match?.[0];
        const rest = text.slice(first?.length || 0);
        if (!first || /[\uD800-\uDFFF]/.test(first)) continue;
        const span = document.createElement("span");
        span.textContent = first;
        span.dataset.snapdomPseudo = "::first-letter";
        const snapshot = snapshotComputedStyle(style);
        const key = getStyleKey(snapshot, "span", compress);
        cache.preStyleMap.set(span, key);
        const restNode = document.createTextNode(rest);
        clone.replaceChild(restNode, textNode);
        clone.insertBefore(span, restNode);
        continue;
      }
const content = style.getPropertyValue("content");
const bg = style.getPropertyValue("background-image");
const bgColor = style.getPropertyValue("background-color");

const fontFamily = style.getPropertyValue("font-family");
const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
const color = style.getPropertyValue("color") || "#000";
const display = style.getPropertyValue("display");
const width = parseFloat(style.getPropertyValue("width"));
const height = parseFloat(style.getPropertyValue("height"));
const borderStyle = style.getPropertyValue("border-style");
const transform = style.getPropertyValue("transform");

const isIconFont2 = isIconFont(fontFamily);

// Detect counter() || counters()
let cleanContent;
if (/counter\s*\(|counters\s*\(/.test(content)) {
  cleanContent = "- ";
} else {
  cleanContent = parseContent(content);
}

const hasContent = content !== "none";
const hasExplicitContent = hasContent && cleanContent !== "";
const hasBg = bg && bg !== "none";
const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
const hasBox = display !== "inline" && (width > 0 || height > 0);
const hasBorder = borderStyle && borderStyle !== "none";
const hasTransform = transform && transform !== "none";

const shouldRender =
  hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;

if (!shouldRender) continue;

const pseudoEl = document.createElement("span");
pseudoEl.dataset.snapdomPseudo = pseudo;
const snapshot = snapshotComputedStyle(style);
const key = getStyleKey(snapshot, "span", compress);
cache.preStyleMap.set(pseudoEl, key);

if (isIconFont2 && cleanContent.length === 1) {
  const imgEl = document.createElement("img");
  imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
  imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
  pseudoEl.appendChild(imgEl);
} else if (cleanContent.startsWith("url(")) {
  const rawUrl = extractURL(cleanContent);
  if (rawUrl && rawUrl.trim() !== "") {
    try {
      const imgEl = document.createElement("img");
      const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
      imgEl.src = dataUrl;
      imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
      pseudoEl.appendChild(imgEl);
    } catch (e) {
      console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
    }
  }
} else if (!isIconFont2 && hasExplicitContent) {
  pseudoEl.textContent = cleanContent;
}

if (hasBg) {
  try {
    const bgSplits = splitBackgroundImage(bg);
    const newBgParts = await Promise.all(
      bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
    );
    pseudoEl.style.backgroundImage = newBgParts.join(", ");
  } catch (e) {
    console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
  }
}

if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;

const hasContent2 =
  pseudoEl.childNodes.length > 0 ||
  (pseudoEl.textContent && pseudoEl.textContent.trim() !== "");
const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;

if (!hasVisibleBox) continue;

pseudo === "::before"
  ? clone.insertBefore(pseudoEl, clone.firstChild)
  : clone.appendChild(pseudoEl);

    } catch (e) {
      console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
    }
  }
  const sChildren = Array.from(source.children);
  const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
    await inlinePseudoElements(
      sChildren[i],
      cChildren[i],
      compress,
      embedFonts,
      useProxy
    );
  }
}

import { cache } from "../core/cache";

/**
 * Fetches and inlines a single background-image entry to a data URL (with caching).
 * - If entry is a gradient or "none", returns unchanged.
 * - If entry is a url(...), fetches the image as data URL and caches it.
 *
 * @param {string} entry - Single background-image entry (e.g., "url(...)").
 * @param {Object} [options={}] - Options like crossOrigin.
 * @param {boolean} [options.skipInline=false] - If true, only fetches & caches, doesn't return a replacement.
 * @returns {Promise<string|void>} - The processed entry (unless skipInline is true).
 */
export async function inlineSingleBackgroundEntry(entry, options = {}) {
  const rawUrl = extractURL(entry)

  const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);

  if (rawUrl) {
    const encodedUrl = safeEncodeURI(rawUrl);
    if (cache.background.has(encodedUrl)) {
      return options.skipInline ? void 0 : `url(${cache.background.get(encodedUrl)})`;
    } else {
      const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
      cache.background.set(encodedUrl, dataUrl);
      return options.skipInline ? void 0 : `url("${dataUrl}")`;
    }
  }

  if (isGradient || entry === "none") {
    return entry;
  }

  return entry;
}

/**
 * Creates a promise that resolves after the specified delay
 * @param {number} [ms=0] - Milliseconds to delay
 * @returns {Promise<void>} Promise that resolves after the delay
 */

export function idle(fn, { fast = false } = {}) {
  if (fast) return fn();
  if ('requestIdleCallback' in window) {
    requestIdleCallback(fn, { timeout: 50 });
  } else {
    setTimeout(fn, 1);
  }
}
/**
 * Gets the computed style for an element or pseudo-element, with caching.
 *
 * @param {Element} el - The element
 * @param {string|null} [pseudo=null] - The pseudo-element
 * @returns {CSSStyleDeclaration} The computed style
 */
export function getStyle(el, pseudo = null) {
  if (!(el instanceof Element)) {
    return window.getComputedStyle(el, pseudo);
  }

  let map = cache.computedStyle.get(el);
  if (!map) {
    map = new Map();
    cache.computedStyle.set(el, map);
  }

  if (!map.has(pseudo)) {
    const st = window.getComputedStyle(el, pseudo);
    map.set(pseudo, st);
  }

  return map.get(pseudo);
}
/**
 * Parses the CSS content property value, handling unicode escapes.
 *
 * @param {string} content - The CSS content value
 * @returns {string} The parsed content
 */
export function parseContent(content) {
  let clean = content.replace(/^['"]|['"]$/g, "");
  if (clean.startsWith("\\")) {
    try {
      return String.fromCharCode(parseInt(clean.replace("\\", ""), 16));
    } catch {
      return clean;
    }
  }
  return clean;
}
/**
 * Extracts a URL from a CSS value like background-image.
 *
 * @param {string} value - The CSS value
 * @returns {string|null} The extracted URL or null
 */

export function extractURL(value) {
  const match = value.match(/url\((['"]?)(.*?)(\1)\)/);
  if (!match) return null;

  const url = match[2].trim();
  if (url.startsWith('#')) return null;
  return url;
}

/**
 * Determines if a font family or URL is an icon font.
 *
 * @param {string} familyOrUrl - The font family or URL
 * @returns {boolean} True if it is an icon font
 */
export function isIconFont(familyOrUrl) {
  const iconFontPatterns = [
    /font\s*awesome/i,
    /material\s*icons/i,
    /ionicons/i,
    /glyphicons/i,
    /feather/i,
    /bootstrap\s*icons/i,
    /remix\s*icons/i,
    /heroicons/i,
    /layui/i,
    /lucide/i
  ];
  return iconFontPatterns.some(rx => rx.test(familyOrUrl));
}
/**
 *
 *
 * @export
 * @param {*} src
 * @param {number} [timeout=3000]
 * @return {*}
 */

export function fetchImage(src, { timeout = 3000, useProxy = '' } = {}) {
  function getCrossOriginMode(url) {
    try {
      const parsed = new URL(url, window.location.href);
      return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
    } catch {
      return "anonymous";
    }
  }

  // Función común para fallback vía fetch + proxy
  async function fetchWithFallback(url) {
    const fetchBlobAsDataURL = (fetchUrl) =>
      fetch(fetchUrl, {
        mode: "cors",
        credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit",
      })
        .then(r => r.blob())
        .then(blob => new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onloadend = () => {
            const base64 = reader.result;
            if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
              reject(new Error("Invalid image data URL"));
              return;
            }
            resolve(base64);
          };
          reader.onerror = () => reject(new Error("FileReader error"));
          reader.readAsDataURL(blob);
        }));

    try {
      return await fetchBlobAsDataURL(url);
    } catch (e) {
      if (useProxy && typeof useProxy === "string") {
        const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
        try {
          return await fetchBlobAsDataURL(proxied);
        } catch {

          throw new Error("[SnapDOM - fetchImage] CORS restrictions prevented image capture (even via proxy)");
        }
      } else {

        throw new Error("[SnapDOM - fetchImage] Fetch fallback failed and no proxy provided");
      }
    }
  }

  const crossOriginValue = getCrossOriginMode(src);

  if (cache.image.has(src)) {
    return Promise.resolve(cache.image.get(src));
  }

  // Detectamos si es un data URI, si sí, devolvemos directo sin fetch
  const isDataURI = src.startsWith("data:image/");
  if (isDataURI) {
    cache.image.set(src, src);
    return Promise.resolve(src);
  }

  // Mejor detección SVG, incluyendo query strings
  const isSVG = /\.svg(\?.*)?$/i.test(src);

  if (isSVG) {
    return (async () => {
      try {
        const response = await fetch(src, {
          mode: "cors",
          credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
        });
        const svgText = await response.text();
        const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
        cache.image.set(src, encoded);
        return encoded;
      } catch {
        return fetchWithFallback(src);
      }
    })();
  }

  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      reject(new Error("[SnapDOM - fetchImage] Image load timed out"));
    }, timeout);

    const image = new Image();
    image.crossOrigin = crossOriginValue;

    image.onload = async () => {
      clearTimeout(timeoutId);
      try {
        await image.decode();
        const canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        const dataURL = canvas.toDataURL("image/png");
        cache.image.set(src, dataURL);
        resolve(dataURL);
      } catch {
        try {
          const fallbackDataURL = await fetchWithFallback(src);
          cache.image.set(src, fallbackDataURL);
          resolve(fallbackDataURL);
        } catch (e) {
          reject(e);
        }
      }
    };

    image.onerror = async () => {
      clearTimeout(timeoutId);
      console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
      try {
        const fallbackDataURL = await fetchWithFallback(src);
        cache.image.set(src, fallbackDataURL);
        resolve(fallbackDataURL);
      } catch (e) {
        reject(e);
      }
    };

    image.src = src;
  });
}

/**
 *
 *
 * @export
 * @param {*} style
 * @return {*}
 */
export function snapshotComputedStyle(style) {
  const snap = {};
  for (let prop of style) {
    snap[prop] = style.getPropertyValue(prop);
  }
  return snap;
}

export function isSafari() {
  return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}

export function stripTranslate(transform) {
  if (!transform || transform === 'none') return '';

  let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, '');

  cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
    const parts = values.split(',').map(s => s.trim());
    if (parts.length !== 6) return `matrix(${values})`;
    parts[4] = '0';
    parts[5] = '0';
    return `matrix(${parts.join(', ')})`;
  });

  cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
    const parts = values.split(',').map(s => s.trim());
    if (parts.length !== 16) return `matrix3d(${values})`;
    parts[12] = '0';
    parts[13] = '0';
    return `matrix3d(${parts.join(', ')})`;
  });

  return cleaned.trim().replace(/\s{2,}/g, ' ');
}

export function safeEncodeURI(uri) {
  if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri; // prevent reencode
  try {
    return encodeURI(uri);
  } catch {
    return uri;
  }
}

export function splitBackgroundImage(bg) {
  const parts = [];
  let depth = 0;
  let lastIndex = 0;
  for (let i = 0; i < bg.length; i++) {
    const char = bg[i];
    if (char === '(') depth++;
    if (char === ')') depth--;
    if (char === ',' && depth === 0) {
      parts.push(bg.slice(lastIndex, i).trim());
      lastIndex = i + 1;
    }
  }
  parts.push(bg.slice(lastIndex).trim());
  return parts;
}

import { getStyle, inlineSingleBackgroundEntry, fetchImage, splitBackgroundImage } from '../utils/helpers.js';
import { embedCustomFonts } from '../modules/fonts.js';
import { precacheCommonTags } from '../utils/cssTools.js';
import { cache } from '../core/cache.js';

/**
 * Preloads images, background images, and optionally fonts into cache before DOM capture.
 *
 * @param {Document|Element} [root=document] - The root node to search for resources
 * @param {Object} [options={}] - Pre-caching options
 * @returns {Promise<void>} Resolves when all resources are pre-cached
 */

export async function preCache(root = document, options = {}) {
  const { embedFonts = true, reset = false} = options;
  if (reset) {
    cache.reset()
    return;
  }
  await document.fonts.ready;
  precacheCommonTags();
  let imgEls = [], allEls = [];
  if (root?.querySelectorAll) {
    imgEls = Array.from(root.querySelectorAll("img[src]"));
    allEls = Array.from(root.querySelectorAll("*"));
  }
  const promises = [];
  for (const img of imgEls) {
    const src = img.src;
    if (!cache.image.has(src)) {

      promises.push(
        fetchImage(src, { useProxy: options.useProxy}).then((dataURL) => cache.image.set(src, dataURL)).catch(() => {
        })
      );
    }
  }
  for (const el of allEls) {
    const bg = getStyle(el).backgroundImage;
    if (bg && bg !== "none") {
      const bgSplits = splitBackgroundImage(bg);
      for (const entry of bgSplits) {
        const isUrl = entry.startsWith("url(");
        if (isUrl) {
          promises.push(
            inlineSingleBackgroundEntry(entry, options).catch(() => {
            })
          );
        }
      }
    }
  }
  if (embedFonts) {
    await embedCustomFonts({ preCached: true });
  }
  await Promise.all(promises);
}

/**
 * Utilities for inlining <img> elements as data URLs or placeholders.
 * @module images
 */

import { fetchImage } from '../utils/helpers.js';

/**
 * Converts all <img> elements in the clone to data URLs or replaces them with placeholders if loading fails.
 *
 * @param {Element} clone - Clone of the original element
 * @param {Object} [options={}] - Options for image processing
 * @returns {Promise<void>} Promise that resolves when all images are processed
 */
export async function inlineImages(clone, options = {}) {
  const imgs = Array.from(clone.querySelectorAll("img"));
  const processImg = async (img) => {
    const src = img.src;
    try {
      const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
      img.src = dataUrl;
      if (!img.width) img.width = img.naturalWidth || 100;
      if (!img.height) img.height = img.naturalHeight || 100;
    } catch {
      const fallback = document.createElement("div");
      fallback.style = `width: ${img.width || 100}px; height: ${img.height || 100}px; background: #ccc; display: inline-block; text-align: center; line-height: ${img.height || 100}px; color: #666; font-size: 12px;`;
      fallback.innerText = "img";
      img.replaceWith(fallback);
    }
  };
  for (let i = 0; i < imgs.length; i += 4) {
    const group = imgs.slice(i, i + 4).map(processImg);
    await Promise.allSettled(group);
  }
}

/**
 * Prepares a deep clone of an element, inlining pseudo-elements and generating CSS classes.
 * @module prepare
 */

import { generateCSSClasses} from '../utils/cssTools.js';
import { stripTranslate} from '../utils/helpers.js';
import { deepClone } from './clone.js';
import { inlinePseudoElements } from '../modules/pseudo.js';
import { inlineExternalDef } from '../modules/svgDefs.js';
import { cache } from '../core/cache.js';

/**
 * Prepares a clone of an element for capture, inlining pseudo-elements and generating CSS classes.
 *
 * @param {Element} element - Element to clone
 * @param {boolean} [compress=false] - Whether to compress style keys
 * @param {boolean} [embedFonts=false] - Whether to embed custom fonts
 * @param {Object} [options={}] - Capture options
 * @param {string[]} [options.exclude] - CSS selectors for elements to exclude
 * @param {Function} [options.filter] - Custom filter function
 * @returns {Promise<Object>} Object containing the clone, generated CSS, and style cache
 */

export async function prepareClone(element, compress = false, embedFonts = false, options = {}) {

  let clone
  let classCSS = '';
  try {
    clone = deepClone(element, compress, options, element);
  } catch (e) {
    console.warn("deepClone failed:", e);
    throw e;
  }
  try {
    await inlinePseudoElements(element, clone, compress, embedFonts, options.useProxy);
  } catch (e) {
    console.warn("inlinePseudoElements failed:", e);
  }
  try {
    inlineExternalDef(clone);
  } catch (e) {
    console.warn("inlineExternalDef failed:", e);
  }
  if (compress) {
    const keyToClass = generateCSSClasses();
    classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
    for (const [node, key] of cache.preStyleMap.entries()) {
      if (node.tagName === "STYLE") continue;
      const className = keyToClass.get(key);
      if (className) node.classList.add(className);
      const bgImage = node.style?.backgroundImage;
      node.removeAttribute("style");
      if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
    }
  } else {
    for (const [node, key] of cache.preStyleMap.entries()) {
      if (node.tagName === "STYLE") continue;
      node.setAttribute("style", key.replace(/;/g, "; "));
    }
  }
  for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
    const scrollX = originalNode.scrollLeft;
    const scrollY = originalNode.scrollTop;
    const hasScroll = scrollX || scrollY;
    if (hasScroll && cloneNode instanceof HTMLElement) {
      cloneNode.style.overflow = "hidden";
      cloneNode.style.scrollbarWidth = "none";
      cloneNode.style.msOverflowStyle = "none";
      const inner = document.createElement("div");
      inner.style.transform = `translate(${-scrollX}px, ${-scrollY}px)`;
      inner.style.willChange = "transform";
      inner.style.display = "inline-block";
      inner.style.width = "100%";
      while (cloneNode.firstChild) {
        inner.appendChild(cloneNode.firstChild);
      }
      cloneNode.appendChild(inner);
    }
  }
  if (element === cache.preNodeMap.get(clone)) {
    const computed = cache.preStyle.get(element) || window.getComputedStyle(element);
    cache.preStyle.set(element, computed);
    const transform = stripTranslate(computed.transform);
    clone.style.margin = "0";
    clone.style.position = "static";
    clone.style.top = "auto";
    clone.style.left = "auto";
    clone.style.right = "auto";
    clone.style.bottom = "auto";
    clone.style.zIndex = "auto";
    clone.style.float = "none";
    clone.style.clear = "none";
    clone.style.transform = transform || "";
  }
  for (const [cloneNode, originalNode] of cache.preNodeMap.entries()) {
    if (originalNode.tagName === "PRE") {
      cloneNode.style.marginTop = "0";
      cloneNode.style.marginBlockStart = "0";
    }
  }
  return { clone, classCSS };
}

/**
 * Deep cloning utilities for DOM elements, including styles and shadow DOM.
 * @module clone
 */

import { inlineAllStyles } from '../modules/styles.js';
import { cache } from '../core/cache.js'

/**
 * Creates a deep clone of a DOM node, including styles, shadow DOM, and special handling for excluded/placeholder/canvas nodes.
 *
 * @param {Node} node - Node to clone
 * @param {boolean} compress - Whether to compress style keys
 * @param {Object} [options={}] - Capture options including exclude and filter
 * @param {Node} [originalRoot] - Original root element being captured
 * @returns {Node|null} Cloned node with styles and shadow DOM content, or null for empty text nodes or filtered elements
 */

export function deepClone(node, compress, options = {}, originalRoot) {
  if (!node) throw new Error('Invalid node');

  // Local set to avoid duplicates in slot processing
  const clonedAssignedNodes = new Set();
  let pendingSelectValue = null; // Track select value for later fix

  // 1. Text nodes
  if (node.nodeType === Node.TEXT_NODE) {
    return node.cloneNode(true);
  }

  // 2. Non-element nodes (comments, etc.)
  if (node.nodeType !== Node.ELEMENT_NODE) {
    return node.cloneNode(true);
  }

  // 3. Exclude by attribute
  if (node.getAttribute("data-capture") === "exclude") {
    const spacer = document.createElement("div");
    const rect = node.getBoundingClientRect();
    spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
    return spacer;
  }

  // 4. Exclude by selectors
  if (options.exclude && Array.isArray(options.exclude)) {
    for (const selector of options.exclude) {
      try {
        if (node.matches?.(selector)) {
          const spacer = document.createElement("div");
          const rect = node.getBoundingClientRect();
          spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
          return spacer;
        }
      } catch (err) {
        console.warn(`Invalid selector in exclude option: ${selector}`, err);
      }
    }
  }

  // 5. Custom filter function
  if (typeof options.filter === "function") {
    try {
      if (!options.filter(node, originalRoot || node)) {
        const spacer = document.createElement("div");
        const rect = node.getBoundingClientRect();
        spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
        return spacer;
      }
    } catch (err) {
      console.warn("Error in filter function:", err);
    }
  }

  // 6. Special case: iframe → fallback pattern
  if (node.tagName === "IFRAME") {
    const fallback = document.createElement("div");
    fallback.style.cssText = `width:${node.offsetWidth}px;height:${node.offsetHeight}px;background-image:repeating-linear-gradient(45deg,#ddd,#ddd 5px,#f9f9f9 5px,#f9f9f9 10px);display:flex;align-items:center;justify-content:center;font-size:12px;color:#555;border:1px solid #aaa;`;
    return fallback;
  }

  // 7. Placeholder nodes
  if (node.getAttribute("data-capture") === "placeholder") {
    const clone2 = node.cloneNode(false);
    cache.preNodeMap.set(clone2, node);
    inlineAllStyles(node, clone2, compress);
    const placeholder = document.createElement("div");
    placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
    placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
    clone2.appendChild(placeholder);
    return clone2;
  }

  // 8. Canvas → convert to image
  if (node.tagName === "CANVAS") {
    const dataURL = node.toDataURL();
    const img = document.createElement("img");
    img.src = dataURL;
    img.width = node.width;
    img.height = node.height;
    cache.preNodeMap.set(img, node);
    inlineAllStyles(node, img, compress);
    return img;
  }

  // 9. Base clone (without children)
  let clone;
  try {
    clone = node.cloneNode(false);
    cache.preNodeMap.set(clone, node);
  } catch (err) {
    console.error("[Snapdom] Failed to clone node:", node, err);
    throw err;
  }

  // Special handling: textarea (keep size and value)
  if (node instanceof HTMLTextAreaElement) {
    clone.textContent = node.value;
    clone.value = node.value;
    const rect = node.getBoundingClientRect();
    clone.style.width = `${rect.width}px`;
    clone.style.height = `${rect.height}px`;
    return clone;
  }

  // Special handling: input
  if (node instanceof HTMLInputElement) {
    clone.value = node.value;
    clone.setAttribute("value", node.value);
    if (node.checked !== void 0) {
      clone.checked = node.checked;
      if (node.checked) clone.setAttribute("checked", "");
      if (node.indeterminate) clone.indeterminate = node.indeterminate;
    }
    // return clone;
  }

  // Special handling: select → postpone value adjustment
  if (node instanceof HTMLSelectElement) {
    pendingSelectValue = node.value;
  }

  // 11. Inline styles
  inlineAllStyles(node, clone, compress);

  // 12. ShadowRoot logic
  if (node.shadowRoot) {
    const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;

    if (hasSlot) {
      // ShadowRoot with slots: only store styles
      for (const child of node.shadowRoot.childNodes) {
        if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
          const cssText = child.textContent || "";
          if (cssText.trim() && compress) {
            if (!cache.preStyle) cache.preStyle = new WeakMap();
            cache.preStyle.set(child, cssText);
          }
        }
      }
    } else {
      // ShadowRoot without slots: clone full content
      const shadowFrag = document.createDocumentFragment();
      for (const child of node.shadowRoot.childNodes) {
        if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
          const cssText = child.textContent || "";
          if (cssText.trim() && compress) {
            if (!cache.preStyle) cache.preStyle = new WeakMap();
            cache.preStyle.set(child, cssText);
          }
          continue;
        }
        const clonedChild = deepClone(child, compress, options, originalRoot || node);
        if (clonedChild) shadowFrag.appendChild(clonedChild);
      }
      clone.appendChild(shadowFrag);
    }
  }

  // 13. Slot outside ShadowRoot
  if (node.tagName === "SLOT") {
    const assigned = node.assignedNodes?.({ flatten: true }) || [];
    const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
    const fragment = document.createDocumentFragment();

    for (const child of nodesToClone) {
      const clonedChild = deepClone(child, compress, options, originalRoot || node);
      if (clonedChild) fragment.appendChild(clonedChild);
    }
    return fragment;
  }

  // 14. Clone children (light DOM), skipping duplicates
  for (const child of node.childNodes) {
    if (clonedAssignedNodes.has(child)) continue;

    const clonedChild = deepClone(child, compress, options, originalRoot || node);
    if (clonedChild) clone.appendChild(clonedChild);
  }

  // Adjust select value after children are cloned
  if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
    clone.value = pendingSelectValue;
    for (const opt of clone.options) {
      if (opt.value === pendingSelectValue) {
        opt.setAttribute("selected", "");
      } else {
        opt.removeAttribute("selected");
      }
    }
  }

  return clone;
}

/**
 * Utilities for handling and embedding web fonts and icon fonts.
 * @module fonts
 */

import { extractURL} from "../utils/helpers"
import { cache } from "../core/cache"
import { isIconFont } from '../modules/iconFonts.js';

/**
 * Converts a unicode character from an icon font into a data URL image.
 *
 * @export
 * @param {string} unicodeChar - The unicode character to render
 * @param {string} fontFamily - The font family name
 * @param {string|number} fontWeight - The font weight
 * @param {number} [fontSize=32] - The font size in pixels
 * @param {string} [color="#000"] - The color to use
 * @returns {Promise<string>} Data URL of the rendered icon
 */

export async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
  const dpr = window.devicePixelRatio || 1;

  // Create temporary context to measure
  const tempCanvas = document.createElement("canvas");
  const tempCtx = tempCanvas.getContext("2d");
  tempCtx.font = fontWeight
    ? `${fontWeight} ${fontSize}px "${fontFamily}"`
    : `${fontSize}px "${fontFamily}"`;

  const metrics = tempCtx.measureText(unicodeChar);
  const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
  const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2;
  const height = ascent + descent;
  const width = metrics.width;

  const canvas = document.createElement("canvas");
  canvas.width = Math.ceil(width * dpr);
  canvas.height = Math.ceil(height * dpr);

  const ctx = canvas.getContext("2d");
  ctx.scale(dpr, dpr);
  ctx.font = tempCtx.font;
  ctx.textAlign = "left";
  ctx.textBaseline = "alphabetic"; // aligns with baseline
  ctx.fillStyle = color;

  // Draw at (0, ascent) so the full glyph fits vertically
  ctx.fillText(unicodeChar, 0, ascent);

  return canvas.toDataURL();
}

function isStylesheetLoaded(href) {
  return Array.from(document.styleSheets).some(sheet => sheet.href === href);
}

function injectLinkIfMissing(href) {
  return new Promise((resolve) => {
    if (isStylesheetLoaded(href)) return resolve(null);
    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = href;
    link.setAttribute("data-snapdom", "injected-import");
    link.onload = () => resolve(link);
    link.onerror = () => resolve(null);
    document.head.appendChild(link);
  });
}

/**
 * Embeds custom fonts found in the document as data URLs in CSS.
 *
 * @export
 * @param {Object} options
 * @param {boolean} [options.preCached=false] - Whether to use pre-cached resources
 * @returns {Promise<string>} The inlined CSS for custom fonts
 */

export async function embedCustomFonts({preCached = false } = {}) {
  if (cache.resource.has("fonts-embed-css")) {
    if (preCached) {
      const style = document.createElement("style");
      style.setAttribute("data-snapdom", "embedFonts");
      style.textContent = cache.resource.get("fonts-embed-css");
      document.head.appendChild(style);
    }
    return cache.resource.get("fonts-embed-css");
  }

  const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
  const styleImports = [];

  for (const styleTag of document.querySelectorAll("style")) {
    const cssText = styleTag.textContent || "";
    const matches = Array.from(cssText.matchAll(importRegex));
    for (const match of matches) {
      const importUrl = match[1];
      if (isIconFont(importUrl)) continue;
      if (!isStylesheetLoaded(importUrl)) {
        styleImports.push(importUrl);
      }
    }
  }

  await Promise.all(styleImports.map(injectLinkIfMissing));

  const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
  let finalCSS = "";

  for (const link of links) {
    try {
      const res = await fetch(link.href);
      const cssText = await res.text();

      if ((isIconFont(link.href) || isIconFont(cssText))) continue;

      const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
      const inlinedCSS = await Promise.all(
        Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
          let rawUrl = extractURL(match[0]);
          if (!rawUrl) return null;
          let url = rawUrl;
          if (!url.startsWith("http") && !url.startsWith("data:")) {
            url = new URL(url, link.href).href;
          }
          if (isIconFont(url)) return null;
          if (cache.resource.has(url)) {
            cache.font.add(url);
            return { original: match[0], inlined: `url(${cache.resource.get(url)})` };
          }
          if (cache.font.has(url)) return null;
          try {
            const fontRes = await fetch(url);
            const blob = await fontRes.blob();
            const b64 = await new Promise((resolve) => {
              const reader = new FileReader();
              reader.onload = () => resolve(reader.result);
              reader.readAsDataURL(blob);
            });
            cache.resource.set(url, b64);
            cache.font.add(url);
            return { original: match[0], inlined: `url(${b64})` };
          } catch (e) {
            console.warn("[snapdom] Failed to fetch font resource:", url);
            return null;
          }
        })
      );

      let cssFinal = cssText;
      for (const r of inlinedCSS) {
        if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
      }
      finalCSS += cssFinal + "\n";
    } catch (e) {
      console.warn("[snapdom] Failed to fetch CSS:", link.href);
    }
  }

  for (const sheet of document.styleSheets) {
    try {
      if (!sheet.href || links.every((link) => link.href !== sheet.href)) {
        for (const rule of sheet.cssRules) {
          if (rule.type === CSSRule.FONT_FACE_RULE) {
            const src = rule.style.getPropertyValue("src");
            const family = rule.style.getPropertyValue("font-family");
            if (!src || isIconFont(family)) continue;

            const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
            const localRegex = /local\((["']?)[^)]+?\1\)/g;
            const hasURL = !!src.match(urlRegex);
            const hasLocal = !!src.match(localRegex);

            if (!hasURL && hasLocal) {
              // Solo local(), conservar en línea compacta
              finalCSS += `@font-face{font-family:${family};src:${src};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
              continue;
            }

            // Embebido para src con url()
            let inlinedSrc = src;
            const matches = Array.from(src.matchAll(urlRegex));
            for (const match of matches) {
              let rawUrl = match[2].trim();
              if (!rawUrl) continue;
              let url = rawUrl;
              if (!url.startsWith("http") && !url.startsWith("data:")) {
                url = new URL(url, sheet.href || location.href).href;
              }
              if (isIconFont(url)) continue;
              if (cache.resource.has(url)) {
                cache.font.add(url);
                inlinedSrc = inlinedSrc.replace(match[0], `url(${cache.resource.get(url)})`);
                continue;
              }
              if (cache.font.has(url)) continue;
              try {
                const res = await fetch(url);
                const blob = await res.blob();
                const b64 = await new Promise((resolve) => {
                  const reader = new FileReader();
                  reader.onload = () => resolve(reader.result);
                  reader.readAsDataURL(blob);
                });
                cache.resource.set(url, b64);
                cache.font.add(url);
                inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
              } catch (e) {
                console.warn("[snapdom] Failed to fetch font URL:", url);
              }
            }

            finalCSS += `@font-face{font-family:${family};src:${inlinedSrc};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
          }
        }
      }
    } catch (e) {
      console.warn("[snapdom] Cannot access stylesheet", sheet.href, e);
    }
  }

  for (const font of document.fonts) {
    if (font.family && font.status === "loaded" && font._snapdomSrc) {
      if (isIconFont(font.family)) continue;
      let b64 = font._snapdomSrc;
      if (!b64.startsWith("data:")) {
        if (cache.resource.has(font._snapdomSrc)) {
          b64 = cache.resource.get(font._snapdomSrc);
          cache.font.add(font._snapdomSrc);
        } else if (!cache.font.has(font._snapdomSrc)) {
          try {
            const res = await fetch(font._snapdomSrc);
            const blob = await res.blob();
            b64 = await new Promise((resolve) => {
              const reader = new FileReader();
              reader.onload = () => resolve(reader.result);
              reader.readAsDataURL(blob);
            });
            cache.resource.set(font._snapdomSrc, b64);
            cache.font.add(font._snapdomSrc);
          } catch (e) {
            console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
            continue;
          }
        }
      }

      finalCSS += `@font-face{font-family:'${font.family}';src:url(${b64});font-style:${font.style || "normal"};font-weight:${font.weight || "normal"};}`;
    }
  }

  if (finalCSS) {
    cache.resource.set("fonts-embed-css", finalCSS);
    if (preCached) {
      const style = document.createElement("style");
      style.setAttribute("data-snapdom", "embedFonts");
      style.textContent = finalCSS;
      document.head.appendChild(style);
    }
  }

  return finalCSS;
}

/**
 * Core logic for capturing DOM elements as SVG data URLs.
 * @module capture
 */

import { prepareClone } from './prepare.js';
import { inlineImages } from '../modules/images.js';
import { inlineBackgroundImages } from '../modules/background.js';
import { idle, isSafari } from '../utils/helpers.js';
import { collectUsedTagNames, generateDedupedBaseCSS } from '../utils/cssTools.js';
import { embedCustomFonts } from '../modules/fonts.js';
import { cache } from '../core/cache.js'

/**
 * Captures an HTML element as an SVG data URL, inlining styles, images, backgrounds, and optionally fonts.
 *
 * @param {Element} element - DOM element to capture
 * @param {Object} [options={}] - Capture options
 * @param {boolean} [options.compress=true] - Whether to compress style keys
 * @param {boolean} [options.embedFonts=false] - Whether to embed custom fonts
 * @param {boolean} [options.fast=true] - Whether to skip idle delay for faster results
 * @param {number} [options.scale=1] - Output scale multiplier
 * @param {string[]} [options.exclude] - CSS selectors for elements to exclude
 * @param {Function} [options.filter] - Custom filter function
 * @returns {Promise<string>} Promise that resolves to an SVG data URL
 */

export async function captureDOM(element, options = {}) {
  if (!element) throw new Error("Element cannot be null or undefined");
  cache.reset()
  const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = ''} = options;
  let clone, classCSS;
  let fontsCSS = "";
  let baseCSS = "";
  let dataURL;
  let svgString;

  ({ clone, classCSS } = await prepareClone(element, compress, embedFonts, options));

  await new Promise((resolve) => {
    idle(async () => {
      await inlineImages(clone, options);
      resolve();
    }, { fast });
  });
  await new Promise((resolve) => {
    idle(async () => {
      await inlineBackgroundImages(element, clone, options);
      resolve();
    }, { fast });
  });
  if (embedFonts) {
    await new Promise((resolve) => {
      idle(async () => {
        fontsCSS = await embedCustomFonts();
        resolve();
      }, { fast });
    });
  }
  if (compress) {
    const usedTags = collectUsedTagNames(clone).sort();
    const tagKey = usedTags.join(",");
    if (cache.baseStyle.has(tagKey)) {
      baseCSS = cache.baseStyle.get(tagKey);
    } else {
      await new Promise((resolve) => {
        idle(() => {
          baseCSS = generateDedupedBaseCSS(usedTags);
          cache.baseStyle.set(tagKey, baseCSS);
          resolve();
        }, { fast });
      });
    }
  }
  await new Promise((resolve) => {
    idle(() => {
      const rect = element.getBoundingClientRect();
      let w = rect.width;
      let h = rect.height;
      const hasW = Number.isFinite(options.width);
      const hasH = Number.isFinite(options.height);
      const hasScale = typeof scale === "number" && scale !== 1;
      if (!hasScale) {
        const aspect = rect.width / rect.height;
        if (hasW && hasH) {
          w = options.width;
          h = options.height;
        } else if (hasW) {
          w = options.width;
          h = w / aspect;
        } else if (hasH) {
          h = options.height;
          w = h * aspect;
        }
      }
      w = Math.ceil(w);
      h = Math.ceil(h);
      clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
      clone.style.transformOrigin = "top left";
      if (!hasScale && (hasW || hasH)) {
        const originalW = rect.width;
        const originalH = rect.height;
        const scaleX = w / originalW;
        const scaleY = h / originalH;
        const existingTransform = clone.style.transform || "";
        const scaleTransform = `scale(${scaleX}, ${scaleY})`;
        clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
      }
      const svgNS = "http://www.w3.org/2000/svg";
      const fo = document.createElementNS(svgNS, "foreignObject");
      fo.setAttribute("width", "100%");
      fo.setAttribute("height", "100%");
      const styleTag = document.createElement("style");
      styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
      fo.appendChild(styleTag);
      fo.appendChild(clone);
      const serializer = new XMLSerializer();
      const foString = serializer.serializeToString(fo);
      const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
      const svgFooter = "</svg>";
      svgString = svgHeader + foString + svgFooter;
      dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
      resolve();
    }, { fast });
  });
  const sandbox = document.getElementById("snapdom-sandbox");
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
  return dataURL;
}

import { getStyleKey } from '../utils/cssTools.js';
import { getStyle } from '../utils/helpers.js';
import {cache} from '../core/cache.js'

function snapshotComputedStyleFull(style) {
  const result = {};
  for (let i = 0; i < style.length; i++) {
    const prop = style[i];
    let val = style.getPropertyValue(prop);

    // Opcional: evitar URLs externas que puedan romper renderizado
    if (
      (prop === 'background-image' || prop === 'content') &&
      val.includes('url(') &&
      !val.includes('data:')
    ) {
      val = 'none';
    }

    result[prop] = val;
  }
  return result;
}

export function inlineAllStyles(source, clone, compress) {
  if (source.tagName === 'STYLE') return;

  if (!cache.preStyle.has(source)) {
    cache.preStyle.set(source, getStyle(source));
  }
  const style = cache.preStyle.get(source);

  if (!cache.snapshot.has(source)) {
    const snapshot = snapshotComputedStyleFull(style);
    cache.snapshot.set(source, snapshot);
  }

  const snapshot = cache.snapshot.get(source);

  const hash = Object.entries(snapshot)
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([prop, val]) => `${prop}:${val}`)
    .join(';');

  if (cache.snapshotKey.has(hash)) {
    cache.preStyleMap.set(clone, cache.snapshotKey.get(hash));
    return;
  }

  const tagName = source.tagName?.toLowerCase() || 'div';
  const key = getStyleKey(snapshot, tagName, compress);

  cache.snapshotKey.set(hash, key);
  cache.preStyleMap.set(clone, key);
}

/**
 * Utilities for working with CSS styles, defaults, and class generation.
 * @module cssTools
 */

import { cache } from "../core/cache"

const commonTags = [
  'div', 'span', 'p', 'a', 'img', 'ul', 'li', 'button', 'input',
  'select', 'textarea', 'label', 'section', 'article', 'header',
  'footer', 'nav', 'main', 'aside', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
  'svg', 'path', 'circle', 'rect', 'line', 'g', 'table', 'thead', 'tbody', 'tr', 'td', 'th'
];

export function precacheCommonTags() {
  for (let tag of commonTags) {
    getDefaultStyleForTag(tag);
  }
}
/**
 * Retrieves default CSS property values from a temporary element.
 *
 * @param {string} tagName - The tag name to get default styles for
 * @returns {Object} Object containing default values for all CSS properties
 */
export function getDefaultStyleForTag(tagName) {
  if (cache.defaultStyle.has(tagName)) {
    return cache.defaultStyle.get(tagName);
  }

  const skipTags = new Set(['script', 'style', 'meta', 'link', 'noscript', 'template']);
  if (skipTags.has(tagName)) {
    const empty = {};
    cache.defaultStyle.set(tagName, empty);
    return empty;
  }

  let sandbox = document.getElementById('snapdom-sandbox');
  if (!sandbox) {
    sandbox = document.createElement('div');
    sandbox.id = 'snapdom-sandbox';
    sandbox.style.position = 'absolute';
    sandbox.style.left = '-9999px';
    sandbox.style.top = '-9999px';
    sandbox.style.width = '0';
    sandbox.style.height = '0';
    sandbox.style.overflow = 'hidden';
    document.body.appendChild(sandbox);
  }

  const el = document.createElement(tagName);
  el.style.all = 'initial';
  sandbox.appendChild(el);

  const styles = getComputedStyle(el);
  const defaults = {};
  for (let prop of styles) {
    defaults[prop] = styles.getPropertyValue(prop);
  }

  sandbox.removeChild(el);
  cache.defaultStyle.set(tagName, defaults);
  return defaults;
}

/**
 * Creates a unique key from an element's computed style that differs from defaults.
 *
 * @param {Object} snapshot - Computed style snapshot
 * @param {string} tagName - The tag name of the element
 * @param {boolean} [compress=false] - Whether to compress style keys
 * @returns {string} Semi-colon separated list of non-default properties
 */

export function getStyleKey(snapshot, tagName, compress = false) {
  const entries = [];
  const defaultStyles = getDefaultStyleForTag(tagName);
  for (let [prop, value] of Object.entries(snapshot)) {
    if (!compress) {
      if (value) {
        entries.push(`${prop}:${value}`);
      }
    } else {
      const defaultValue = defaultStyles[prop];
      if (value && value !== defaultValue) {
        entries.push(`${prop}:${value}`);
      }
    }
  }

  return entries.sort().join(";");
}

/**
 * Collects all unique tag names used in the DOM tree rooted at the given node.
 *
 * @param {Node} root - The root node to search
 * @returns {string[]} Array of unique tag names
 */
export function collectUsedTagNames(root) {
  const tagSet = new Set();
  if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
    return [];
  }
  if (root.tagName) {
    tagSet.add(root.tagName.toLowerCase());
  }
  if (typeof root.querySelectorAll === 'function') {
    root.querySelectorAll("*").forEach(el => tagSet.add(el.tagName.toLowerCase()));
  }
  return Array.from(tagSet);
}

/**
 * Generates deduplicated base CSS for the given tag names.
 *
 * @param {string[]} usedTagNames - Array of tag names
 * @returns {string} CSS string
 */
export function generateDedupedBaseCSS(usedTagNames) {
  const groups = new Map();

  for (let tagName of usedTagNames) {
    const styles = cache.defaultStyle.get(tagName);
    if (!styles) continue;

    // Creamos la "firma" del bloque CSS para comparar
    const key = Object.entries(styles)
      .map(([k, v]) => `${k}:${v};`)
      .sort()
      .join('');

    // Agrupamos por firma
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key).push(tagName);
  }

  // Ahora generamos el CSS optimizado
  let css = '';
  for (let [styleBlock, tagList] of groups.entries()) {
    css += `${tagList.join(',')} { ${styleBlock} }\n`;
  }

  return css;
}
/**
 * Generates CSS classes from a style map.
 *
 * @returns {Map} Map of style keys to class names
 */
export function generateCSSClasses() {
  const keySet = new Set(cache.preStyleMap.values());
  const classMap = new Map();
  let counter = 1;
  for (const key of keySet) {
    classMap.set(key, `c${counter++}`);
  }
  return classMap;
}

/**
 * Main API for snapDOM: capture DOM elements as SVG and raster images.
 * Provides utilities for exporting, downloading, and converting DOM captures.
 * @module snapdom
 */

import { captureDOM } from '../core/capture';
import { isSafari } from '../utils/helpers.js';
import { extendIconFonts } from '../modules/iconFonts.js';

/**
 * Converts an SVG data URL to an HTMLImageElement.
 *
 * @param {string} url - SVG data URL
 * @param {Object} options
 * @param {number} [options.dpr=1] - Device pixel ratio
 * @param {number} [options.scale=1] - Scale multiplier
 * @returns {Promise<HTMLImageElement>} The resulting image
 */

 async function toImg(url, { dpr = 1, scale = 1 }) {
  const img = new Image();
  img.src = url;
  await img.decode();
  img.width = img.width * scale ;
  img.height = img.height * scale ;

  return img;
}

/**
 * Converts an SVG data URL to a Canvas element.
 *
 * @param {string} url - SVG data URL
 * @param {Object} options
 * @param {number} [options.dpr=1] - Device pixel ratio
 * @param {number} [options.scale=1] - Scale multiplier
 * @returns {Promise<HTMLCanvasElement>} The resulting canvas
 */

async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
  const img = new Image();
  img.src = url;
  img.crossOrigin = 'anonymous';
  img.loading = 'eager';
  img.decoding = 'sync';

  const isSafariBrowser = isSafari();
  let appended = false;

  if (isSafariBrowser) {
    document.body.appendChild(img);
    appended = true;
  }

  await img.decode();

  if (isSafariBrowser) {
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  if (img.width === 0 || img.height === 0) {
    if (appended) img.remove();
    throw new Error('Image failed to load or has no dimensions');
  }

  const width = img.width * scale;
  const height = img.height * scale;

  const canvas = document.createElement('canvas');
  canvas.width = Math.ceil(width * dpr);
  canvas.height = Math.ceil(height * dpr);
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;

  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  ctx.drawImage(img, 0, 0, width, height);

  if (appended) img.remove();

  return canvas;
}

/**
 * Converts a DOM snapshot (SVG data URL) into a Blob of the specified format.
 *
 * @param {string} url - SVG data URL
 * @param {Object} [options]
 * @param {string} [options.format="svg"] - Output format: "svg", "png", "jpeg", "webp"
 * @param {number} [options.dpr=1] - Device pixel ratio
 * @param {number} [options.scale=1] - Scale multiplier
 * @param {string} [options.backgroundColor="#fff"] - Background for raster formats
 * @param {number} [options.quality] - JPEG/WebP quality (0–1)
 * @returns {Promise<Blob>} The resulting Blob
 */
async function toBlob(url, {
  type = "svg",
  scale = 1,
  backgroundColor = "#fff",
  quality
} = {}) {
  const mime = {
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    png: "image/png",
    webp: "image/webp",
  }[type] || "image/png";

  if (type === "svg") {
    const svgText = decodeURIComponent(url.split(",")[1]);
    return new Blob([svgText], { type: "image/svg+xml" });
  }

  const canvas = await createBackground(url, { dpr: 1, scale }, backgroundColor);
  return new Promise((resolve) => {
    canvas.toBlob(blob => resolve(blob), `${mime}`, quality);
  });
}

/**
 * Creates a canvas with a background color from an SVG data URL.
 *
 * @param {string} url - SVG data URL
 * @param {Object} options
 * @param {number} [options.dpr=1] - Device pixel ratio
 * @param {number} [options.scale=1] - Scale multiplier
 * @param {string} [backgroundColor] - Background color to apply
 * @returns {Promise<HTMLCanvasElement>} The resulting canvas
 */

 async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
  const baseCanvas = await toCanvas(url, { dpr, scale });
  if (!backgroundColor) return baseCanvas;

  const temp = document.createElement("canvas");
  temp.width = baseCanvas.width;
  temp.height = baseCanvas.height;
  const ctx = temp.getContext("2d");

  ctx.fillStyle = backgroundColor;
  ctx.fillRect(0, 0, temp.width, temp.height);
  ctx.drawImage(baseCanvas, 0, 0);

  return temp;
}

/**
 * Converts an SVG data URL to a raster image (PNG, JPEG, WebP).
 *
 * @param {string} url - SVG data URL
 * @param {Object} options
 * @param {number} [options.dpr=1] - Device pixel ratio
 * @param {number} [options.scale=1] - Scale multiplier
 * @param {string} [options.backgroundColor="#fff"] - Background color for rasterization
 * @param {number} [options.quality] - Image quality (for JPEG/WebP)
 * @param {string} [format="png"] - Output format: "png", "jpeg", or "webp"
 * @returns {Promise<HTMLImageElement>} The resulting raster image
 */

 async function toRasterImg(url, { dpr = 1, scale = 1, backgroundColor, quality }, format = "png") {
  const defaultBg = ["jpg", "jpeg", "webp"].includes(format) ? "#fff" : undefined;
  const finalBg = backgroundColor ?? defaultBg;

  const canvas = await createBackground(url, { dpr, scale }, finalBg);

  const img = new Image();
  img.src = canvas.toDataURL(`image/${format}`, quality);
  await img.decode();

  img.style.width = `${canvas.width / dpr}px`;
  img.style.height = `${canvas.height / dpr}px`;

  return img;
}

/**
 * Downloads a captured image in the specified format.
 *
 * @param {string} url - SVG data URL
 * @param {Object} options
 * @param {number} [options.dpr=1] - Device pixel ratio
 * @param {number} [options.scale=1] - Scale multiplier
 * @param {string} [options.backgroundColor="#fff"] - Background color for rasterization
 * @param {string} [options.format="png"] - Output format
 * @param {string} [options.filename="capture"] - Download filename
 * @returns {Promise<void>} Resolves when download is triggered
 */

 async function download(url,{ dpr = 1, scale = 1, backgroundColor, format = "png", filename = "snapDOM"} = {}) {
  if (format === "svg") {
    const blob = await toBlob(url);
    const objectURL = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = objectURL;
    a.download = `${filename}.svg`;
    a.click();
    URL.revokeObjectURL(objectURL);
    return;
  }

  const defaultBg = ["jpg", "jpeg", "webp"].includes(format) ? "#fff" : undefined;
  const finalBg = backgroundColor ?? defaultBg;

  const canvas = await createBackground(url, { dpr, scale }, finalBg);
  const mime = {
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    png: "image/png",
    webp: "image/webp",
  }[format] || "image/png";

  const dataURL = canvas.toDataURL(mime);

  const a = document.createElement("a");
  a.href = dataURL;
  a.download = `${filename}.${format}`;
  a.click();
}

/**
 * Main entry point: captures a DOM element and returns an object with export methods.
 *
 * @param {Element} element - DOM element to capture
 * @param {Object} [options={}] - Capture options
 * @returns {Promise<Object>} Object with export methods (toRaw, toImg, toCanvas, etc.)
 */

export async function snapdom(element, options = {}) {
  options = { scale: 1, ...options };
  if (!element) throw new Error("Element cannot be null or undefined");
  if (options.iconFonts) {
    extendIconFonts(options.iconFonts);
  }
  return await snapdom.capture(element, options);
}

/**
 * Captures a DOM element and returns an object with export methods (internal use).
 *
 * @param {Element} el - DOM element to capture
 * @param {Object} [options={}] - Capture options
 * @returns {Promise<Object>} Object with export methods
 */

snapdom.capture = async (el, options = {}) => {
  const url = await captureDOM(el, options);
  const dpr = window.devicePixelRatio || 1;
  const scale = options.scale || 1;

  return {
    url,
    options,
    toRaw: () => url,
    toImg: () => toImg(url, { dpr, scale }),
    toCanvas: () => toCanvas(url, { dpr, scale }),
    toBlob: (options) => toBlob(url, { dpr, scale, ...options }),
    toPng: (options) => toRasterImg(url, { dpr, scale, ...options  }, "png"),
    toJpg: (options) => toRasterImg(url, { dpr, scale, ...options }, "jpeg"),
    toWebp: (options) => toRasterImg(url, { dpr, scale, ...options }, "webp"),
    download: ({ format = "png", filename = "capture", backgroundColor } = {}) => download(url, { dpr, scale, backgroundColor, format, filename})
  };
};

// Compatibilidad
snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
snapdom.download = async (el, options = {}) => {
  const {
    format = "png",
    filename = "capture",
    backgroundColor,
    ...rest
  } = options;

  const capture = await snapdom.capture(el, rest);
  return await capture.download({ format, filename, backgroundColor });
};

/**
 * Inlines external `<defs>` used by `<use xlink:href="#...">` or `<use href="#...">` inside SVGs,
 * copying only the necessary definitions into each individual cloned SVG.
 *
 * This is needed because cloned SVGs using `<use>` may reference elements like `<symbol>`, `<path>`, etc.,
 * defined elsewhere in the document (e.g., in a shared `<defs>` block that is not part of the cloned subtree).
 *
 * The function finds all `<use>` elements within `root`, extracts the referenced IDs,
 * and embeds the required definitions at the top of each SVG.
 *
 * @function inlineExternalDef
 * @param {ParentNode} root - The root node containing cloned SVGs (usually the result of a DOM snapshot).
 * @returns {void}
 *
 * @example
 * const { clone } = await prepareClone(element);
 * inlineExternalDef(clone);
 */

export function inlineExternalDef(root) {
  if (!root) return;
  const defsSources = document.querySelectorAll('svg > defs');
  if (!defsSources.length) return;

  root.querySelectorAll('svg').forEach(svg => {
    const uses = svg.querySelectorAll('use');
    if (!uses.length) return;

    const usedIds = new Set();
    uses.forEach(use => {
      const href = use.getAttribute('xlink:href') || use.getAttribute('href');
      if (href && href.startsWith('#')) usedIds.add(href.slice(1));
    });
    if (!usedIds.size) return;

    const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");

    for (const id of usedIds) {
      for (const source of defsSources) {
        const def = source.querySelector(`#${CSS.escape(id)}`);
        if (def) {
          defs.appendChild(def.cloneNode(true));
          break;
        }
      }
    }

    if (defs.childNodes.length) {
      svg.insertBefore(defs, svg.firstChild);
    }
  });
}

vi.mock('../src/utils/helpers.js', async () => {
  const actual = await vi.importActual('../src/utils/helpers.js');
  return {
    ...actual,
    fetchImage: vi.fn(),
  };
});
vi.mock('../src/modules/fonts.js', async () => {
  const actual = await vi.importActual('../src/modules/fonts.js');
  return {
    ...actual,
    iconToImage: vi.fn(),
  };
});

import { describe, it, expect, vi } from 'vitest';
import { inlinePseudoElements } from '../src/modules/pseudo.js';
import * as helpers from '../src/utils/helpers.js';
import * as fonts from '../src/modules/fonts.js';

describe('inlinePseudoElements', () => {
  it('does not fail with simple elements', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    await expect(inlinePseudoElements(el, clone, new Map(), new WeakMap(), false)).resolves.toBeUndefined();
  });

  it('handles ::before with text content', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? '"★"' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::before with icon font', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? '"★"' : prop === 'font-family' ? 'Font Awesome' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Font Awesome',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    fonts.iconToImage.mockResolvedValue('data:image/png;base64,icon');
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::before with url content', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? 'url("https://test.com/img.png")' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    helpers.fetchImage.mockResolvedValue('data:image/png;base64,img');
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::before with background-image (data url)', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? 'none' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'url("data:image/png;base64,abc")' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::before with background-image (fetch)', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? 'none' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'url("https://test.com/img.png")' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    helpers.fetchImage.mockResolvedValue('data:image/png;base64,img');
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::before with background-image fetch error', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? 'none' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'url("https://test.com/img.png")' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    helpers.fetchImage.mockRejectedValue(new Error('fail'));
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::before with no visible box', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: () => 'none', color: '', fontSize: '', fontWeight: '', fontFamily: ''
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles ::first-letter with no textNode', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
      getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: ''
    }));
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('handles error in pseudo processing', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation(() => { throw new Error('fail'); });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('ignores if source no es Element', async () => {
    const notElement = {};
    const clone = document.createElement('div');
    await expect(inlinePseudoElements(notElement, clone, new Map(), new WeakMap(), false)).resolves.toBeUndefined();
  });

  it('ignores if clone no es Element', async () => {
    const el = document.createElement('div');
    const notElement = {};
    await expect(inlinePseudoElements(el, notElement, new Map(), new WeakMap(), false)).resolves.toBeUndefined();
  });

  it('inserta pseudoEl como ::after', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::after') return {
        getPropertyValue: (prop) => prop === 'content' ? '"after"' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? '#f00' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('inserta pseudoEl como ::before', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? '"before"' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? '#f00' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('no inserta pseudoEl si no hay visible box', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => 'none', color: '', fontSize: '', fontWeight: '', fontFamily: ''
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('maneja ::first-letter meaningful', async () => {
    const el = document.createElement('div');
    el.textContent = 'Test';
    const clone = document.createElement('div');
    clone.textContent = 'Test';
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::first-letter') return {
        getPropertyValue: (prop) => prop === 'color' ? '#f00' : '', color: '#f00', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('inserta ambos pseudoEl ::before y ::after en una sola ejecución', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? '"before"' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? '#f00' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      if (pseudo === '::after') return {
        getPropertyValue: (prop) => prop === 'content' ? '"after"' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'none' : prop === 'background-color' ? '#0f0' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });

  it('should inline ::first-letter when style is meaningful', async () => {
  const el = document.createElement('p');
  el.textContent = '¡Hola mundo!';
  el.style.setProperty('color', 'black');

  document.body.appendChild(el);
  const clone = el.cloneNode(true);
  const styleMap = new Map();
  const styleCache = new WeakMap();

  const style = document.createElement('style');
  style.textContent = `
    p::first-letter {
      color: red;
      font-size: 200%;
    }
  `;
  document.head.appendChild(style);

  await inlinePseudoElements(el, clone, styleMap, styleCache, false);
  const firstLetterEl = clone.querySelector('[data-snapdom-pseudo="::first-letter"]');
  expect(firstLetterEl).toBeTruthy();
  expect(firstLetterEl.textContent.length).toBeGreaterThan(0);
});

it('should inline background-image entries for pseudo-element', async () => {
  const el = document.createElement('div');
  document.body.appendChild(el);

  const style = document.createElement('style');
  style.textContent = `
    div::after {
      content: " ";
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='10' height='10' fill='blue'/%3E%3C/svg%3E");
      display: inline-block;
      width: 10px;
      height: 10px;
    }
  `;
  document.head.appendChild(style);

  const clone = el.cloneNode(true);
  const styleMap = new Map();
  const styleCache = new WeakMap();

  await inlinePseudoElements(el, clone, styleMap, styleCache, false);
  const pseudoAfter = clone.querySelector('[data-snapdom-pseudo="::after"]');
  expect(pseudoAfter).toBeTruthy();
  expect(pseudoAfter.style.backgroundImage.startsWith("url(\"data:image/")).toBeTruthy();
});

});

vi.mock('../src/utils/helpers.js', async () => {
  const actual = await vi.importActual('../src/utils/helpers.js');
  return {
    ...actual,
    inlineSingleBackgroundEntry: vi.fn().mockResolvedValue('url("data:image/png;base64,abc")'),
  };
});

import { describe, it, vi } from 'vitest';
import { inlinePseudoElements } from '../src/modules/pseudo.js';

describe('inlinePseudoElements background-image inlining (success)', () => {
  it('cubre el inlining exitoso de background-image en pseudo', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? 'none' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'url("data:image/png;base64,abc")' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });
});

vi.mock('../src/utils/helpers.js', async () => {
  const actual = await vi.importActual('../src/utils/helpers.js');
  return {
    ...actual,
    inlineSingleBackgroundEntry: vi.fn().mockRejectedValue(new Error('fail')),
  };
});

import { describe, it, vi } from 'vitest';
import { inlinePseudoElements } from '../src/modules/pseudo.js';

describe('inlinePseudoElements background-image inlining (error)', () => {
  it('cubre el catch de error en inlining de background-image en pseudo', async () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockImplementation((_, pseudo) => {
      if (pseudo === '::before') return {
        getPropertyValue: (prop) => prop === 'content' ? 'none' : prop === 'font-family' ? 'Arial' : prop === 'font-size' ? '32' : prop === 'font-weight' ? '400' : prop === 'color' ? '#000' : prop === 'background-image' ? 'url("data:image/png;base64,abc")' : prop === 'background-color' ? 'transparent' : '',
        color: '#000', fontSize: '32px', fontWeight: '400', fontFamily: 'Arial',
      };
      return { getPropertyValue: () => '', color: '', fontSize: '', fontWeight: '', fontFamily: '' };
    });
    await inlinePseudoElements(el, clone, new Map(), new WeakMap(), false);
    window.getComputedStyle.mockRestore();
  });
});

var defaultIconFonts = [
// /uicons/i,
  /font\s*awesome/i,
  /material\s*icons/i,
  /ionicons/i,
  /glyphicons/i,
  /feather/i,
  /bootstrap\s*icons/i,
  /remix\s*icons/i,
  /heroicons/i,
  /layui/i,
  /lucide/i
];

var userIconFonts = [];

export function extendIconFonts(fonts) {
  const list = Array.isArray(fonts) ? fonts : [fonts];
  for (const f of list) {
    if (f instanceof RegExp) {
      userIconFonts.push(f);
    } else if (typeof f === "string") {
      userIconFonts.push(new RegExp(f, "i"));
    } else {
      console.warn("[snapdom] Ignored invalid iconFont value:", f);
    }
  }
}

export function isIconFont(input) {
  const text = typeof input === "string" ? input : "";

  const candidates = [...defaultIconFonts, ...userIconFonts];

  for (const rx of candidates) {
    if (rx instanceof RegExp && rx.test(text)) return true;
  }

  // Heurística básica
  if (
    /icon/i.test(text) ||
    /glyph/i.test(text) ||
    /symbols/i.test(text) ||
    /feather/i.test(text) ||
    /fontawesome/i.test(text)
  ) return true;

  return false;
}

import { bench, describe, beforeEach, afterEach } from 'vitest';
import { domToDataUrl } from 'https://unpkg.com/modern-screenshot';
import * as htmlToImage from 'https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/+esm';
//import { toPng, toJpeg, toBlob, toPixelData, toSvg } from 'https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js';
import { snapdom as sd } from 'https://cdn.jsdelivr.net/npm/@zumer/snapdom@1.8.0/dist/snapdom.mjs';
import { snapdom } from '../src/index';

let html2canvasLoaded = false;

async function loadHtml2Canvas() {
  if (html2canvasLoaded) return;
  await new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
    script.onload = () => resolve();
    script.onerror = reject;
    document.head.appendChild(script);
  });
  html2canvasLoaded = true;
}

await loadHtml2Canvas();

const sizes = [
  { width: 200, height: 100, label: 'Small element (200x100)' },
  { width: 400, height: 300, label: 'Modal size (400x300)' },
  { width: 1200, height: 800, label: 'Page view (1200x800)' },
  { width: 2000, height: 1500, label: 'Large scroll area (2000x1500)' },
  { width: 4000, height: 2000, label: 'Very large element (4000x2000)' },
];

for (const size of sizes) {
  describe(`Benchmark simple node at ${size.label}`, () => {
    let container;

    async function setupContainer() {
      if (container && document.body.contains(container)) {
        return;
      }
      container = document.createElement('div');
      container.style.width = `${size.width}px`;
      container.style.height = `${size.height}px`;
      container.style.background = 'linear-gradient(to right, red, blue)';
      container.style.fontFamily = 'Arial, sans-serif';
      container.style.display = 'flex';
      container.style.alignItems = 'center';
      container.style.justifyContent = 'center';
      container.style.fontSize = '24px';
      container.innerHTML = `<h1>${size.label}</h1>`;
      document.body.appendChild(container);
    }

    /*   async function setupContainer() {
        if (container && document.body.contains(container)) return;

        container = document.createElement('div');
        container.style.width = `${size.width}px`;
        container.style.height = `${size.height}px`;
        container.style.padding = '20px';
        container.style.overflow = 'auto';
        container.style.background = 'white';
        container.style.border = '2px solid black';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.color = '#333';
        container.style.position = 'relative';

        const grid = document.createElement('div');
        grid.style.display = 'grid';
        grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(120px, 1fr))';
        grid.style.gap = '10px';

        for (let i = 0; i < Math.floor((size.width * size.height) / 20000); i++) {
          const card = document.createElement('div');
          card.style.padding = '10px';
          card.style.borderRadius = '8px';
          card.style.background = i % 2 === 0 ? '#f0f0f0' : '#e0eaff';
          card.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
          card.style.display = 'flex';
          card.style.flexDirection = 'column';
          card.style.alignItems = 'center';

          const title = document.createElement('h3');
          title.textContent = `Card ${i + 1}`;
          title.style.margin = '0 0 10px 0';
          title.style.fontSize = '14px';

          const icon = document.createElement('div');
          icon.style.width = '30px';
          icon.style.height = '30px';
          icon.style.borderRadius = '50%';
          icon.style.background = i % 2 === 0 ? 'red' : 'blue';
          icon.style.marginBottom = '10px';

          const text = document.createElement('p');
          text.textContent = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
          text.style.fontSize = '12px';
          text.style.textAlign = 'center';

          card.appendChild(icon);
          card.appendChild(title);
          card.appendChild(text);
          grid.appendChild(card);
        }

        container.appendChild(grid);
        document.body.appendChild(container);
      }
       */
    afterEach(() => {
      if (container) {
        container.remove();
        container = null;
      }
    });

    bench('snapDOM current version', async () => {
      await setupContainer();
      await snapdom.toRaw(container);
    });

     bench('snapDOM V1.8.0', async () => {
      await setupContainer();
      await sd.toRaw(container);
    });

    bench('html2canvas capture', async () => {
      await setupContainer();
      const canvas = await window.html2canvas(container, { logging: false, scale: 1 });
      await canvas.toDataURL();
    });

    bench('modern-screenshot capture', async () => {
      await setupContainer();
      await domToDataUrl(container);
    });

    bench('html-to-image capture', async () => {
      await setupContainer();
      await htmlToImage.toSvg(container);
    });
  });
}

import { describe, it, expect, vi } from 'vitest';
import { getStyle, parseContent, extractURL, isIconFont, snapshotComputedStyle, isSafari, stripTranslate, safeEncodeURI, idle, fetchImage } from '../src/utils/helpers.js';

describe('getStyle', () => {
  it('returns a CSSStyleDeclaration', () => {
    const el = document.createElement('div');
    document.body.appendChild(el);
    const style = getStyle(el);
    expect(style).toBeInstanceOf(CSSStyleDeclaration);
    document.body.removeChild(el);
  });
});

describe('parseContent', () => {
  it('parses CSS content correctly', () => {
    expect(parseContent('"★"')).toBe('★');
    expect(parseContent('\\2605')).toBe('★');
  });
});

describe('parseContent edge cases', () => {
  it('returns \u0000 if parseInt fails (not hex)', () => {
    expect(parseContent('\\nothex')).toBe('\u0000');
  });
  it('returns clean if String.fromCharCode throws', () => {
    const orig = String.fromCharCode;
    String.fromCharCode = () => { throw new Error('fail'); };
    expect(parseContent('\\2605')).toBe('\\2605');
    String.fromCharCode = orig;
  });
});

describe('extractURL', () => {
  it('extracts the URL from background-image', () => {
    expect(extractURL('url("https://test.com/img.png")')).toBe('https://test.com/img.png');
    expect(extractURL('none')).toBeNull();
  });
});

describe('isIconFont', () => {
  it('detects icon fonts', () => {
    expect(isIconFont('Font Awesome')).toBe(true);
    expect(isIconFont('Arial')).toBe(false);
  });
});

describe('snapshotComputedStyle', () => {
  it('returns a style snapshot', () => {
    const el = document.createElement('div');
    document.body.appendChild(el);
    const style = getComputedStyle(el);
    const snap = snapshotComputedStyle(style);
    expect(typeof snap).toBe('object');
    document.body.removeChild(el);
  });
});

describe('isSafari', () => {
  it('returns a boolean', () => {
    expect(typeof isSafari()).toBe('boolean');
  });
});

describe('stripTranslate', () => {
  it('removes translate transforms', () => {
    expect(stripTranslate('translateX(10px) scale(2)')).toContain('scale(2)');
  });
  it('stripTranslate removes matrix and matrix3d', () => {
    expect(stripTranslate('matrix(1,0,0,1,10,20)')).not.toContain('10,20');
    expect(stripTranslate('matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,10,20,30,1)')).not.toContain('10,20,30');
  });
});

describe('safeEncodeURI', () => {
  it('returns an encoded string', () => {
    expect(typeof safeEncodeURI('https://test.com/á')).toBe('string');
  });
  it('safeEncodeURI handles invalid URIs gracefully', () => {
    expect(typeof safeEncodeURI('%E0%A4%A')).toBe('string');
  });
});

describe('idle', () => {
  it('calls fn immediately if fast is true', () => {
    let called = false;
    idle(() => { called = true; }, { fast: true });
    expect(called).toBe(true);
  });
  it('uses requestIdleCallback if available', () => {
    const orig = window.requestIdleCallback;
    let called = false;
    window.requestIdleCallback = (fn) => { called = true; fn(); };
    idle(() => { called = true; });
    expect(called).toBe(true);
    window.requestIdleCallback = orig;
  });
  it('falls back to setTimeout if requestIdleCallback not available', async () => {
    const orig = window.requestIdleCallback;
    delete window.requestIdleCallback;
    let called = false;
    idle(() => { called = true; });
    await new Promise(r => setTimeout(r, 10));
    expect(called).toBe(true);
    window.requestIdleCallback = orig;
  });
});

describe('fetchImage', () => {
  it('rejects on image error', async () => {
    await expect(fetchImage('invalid-url', { timeout: 100 }))
  .rejects.toThrow('Fetch fallback failed and no proxy provided');

  });
  it('rejects on timeout', async () => {
    // Simula una imagen que nunca carga
    const origImage = window.Image;
    window.Image = class { set src(_){} onload(){} onerror(){} };
    await expect(fetchImage('timeout-url', { timeout: 10 })).rejects.toThrow('Image load timed out');
    window.Image = origImage;
  });

});

describe('fetchImage cache', () => {
  it('returns cached image if present', async () => {
    const { cache } = await import('../src/core/cache.js');
    cache.image.set('cached-url', 'data:image/png;base64,abc');
    const result = await fetchImage('cached-url', { useProxy: false });
    expect(result).toBe('data:image/png;base64,abc');
    cache.image.delete('cached-url');
  });
});

describe('fetchImage error propagation', () => {
  it('rejects with original error if decode fails', async () => {
    const origImage = window.Image;
    window.Image = class {
      constructor() { setTimeout(() => this.onload(), 1); }
      set src(_){}
      decode() { return Promise.reject(new Error('decode fail')); }
      get width() { return 1; }
      get height() { return 1; }
    };
   await expect(fetchImage('decode-fail-url', { timeout: 100 }))
  .rejects.toThrow('Fetch fallback failed and no proxy provided');
    window.Image = origImage;
  });
});

describe('stripTranslate edge cases', () => {
  it('returns empty string for empty or none', () => {
    expect(stripTranslate('')).toBe('');
    expect(stripTranslate('none')).toBe('');
  });
  it('returns original for malformed matrix', () => {
    expect(stripTranslate('matrix(1,2,3)')).toBe('matrix(1,2,3)');
    expect(stripTranslate('matrix3d(1,2,3)')).toBe('matrix3d(1,2,3)');
  });
});

/**
 * Caches for images, backgrounds, resources, and computed styles used during DOM capture.
 * @module cache
 */

export const cache = {
  image: new Map(),
  background: new Map(),
  resource: new Map(),
  defaultStyle: new Map(),
  baseStyle: new Map(),
  computedStyle: new WeakMap(),
  font: new Set(),
  snapshot: new WeakMap(),
  snapshotKey: new Map(),
  preStyleMap: new Map(),
  preStyle: new WeakMap(),
  preNodeMap: new Map(),
  reset: resetCache
};

function resetCache() {
  cache.computedStyle = new WeakMap();
  cache.snapshot = new WeakMap();
  cache.snapshotKey.clear();
  cache.preStyleMap.clear();
  cache.preStyle = new WeakMap();
  cache.preNodeMap.clear();
}

import { bench, describe, beforeEach, afterEach } from 'vitest';
import { domToDataUrl } from 'https://unpkg.com/modern-screenshot';
import * as htmlToImage from 'https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/+esm';
import { snapdom as sd } from 'https://cdn.jsdelivr.net/npm/@zumer/snapdom@1.8.0/dist/snapdom.mjs';
import { snapdom } from '../src/index';

let html2canvasLoaded = false;

async function loadHtml2Canvas() {
  if (html2canvasLoaded) return;
  await new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
    script.onload = () => resolve();
    script.onerror = reject;
    document.head.appendChild(script);
  });
  html2canvasLoaded = true;
}

await loadHtml2Canvas();

const sizes = [
  { width: 200, height: 100, label: 'Small element (200x100)' },
  { width: 400, height: 300, label: 'Modal size (400x300)' },
  { width: 1200, height: 800, label: 'Page view (1200x800)' },
  { width: 2000, height: 1500, label: 'Large scroll area (2000x1500)' },
  { width: 4000, height: 2000, label: 'Very large element (4000x2000)' },
];

for (const size of sizes) {
  describe(`Benchmark complex node at ${size.label}`, () => {
    let container;

  async function setupContainer() {
        if (container && document.body.contains(container)) return;

        container = document.createElement('div');
        container.style.width = `${size.width}px`;
        container.style.height = `${size.height}px`;
        container.style.padding = '20px';
        container.style.overflow = 'auto';
        container.style.background = 'white';
        container.style.border = '2px solid black';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.color = '#333';
        container.style.position = 'relative';

        const grid = document.createElement('div');
        grid.style.display = 'grid';
        grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(120px, 1fr))';
        grid.style.gap = '10px';

        for (let i = 0; i < Math.floor((size.width * size.height) / 20000); i++) {
          const card = document.createElement('div');
          card.style.padding = '10px';
          card.style.borderRadius = '8px';
          card.style.background = i % 2 === 0 ? '#f0f0f0' : '#e0eaff';
          card.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
          card.style.display = 'flex';
          card.style.flexDirection = 'column';
          card.style.alignItems = 'center';

          const title = document.createElement('h3');
          title.textContent = `Card ${i + 1}`;
          title.style.margin = '0 0 10px 0';
          title.style.fontSize = '14px';

          const icon = document.createElement('div');
          icon.style.width = '30px';
          icon.style.height = '30px';
          icon.style.borderRadius = '50%';
          icon.style.background = i % 2 === 0 ? 'red' : 'blue';
          icon.style.marginBottom = '10px';

          const text = document.createElement('p');
          text.textContent = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
          text.style.fontSize = '12px';
          text.style.textAlign = 'center';

          card.appendChild(icon);
          card.appendChild(title);
          card.appendChild(text);
          grid.appendChild(card);
        }

        container.appendChild(grid);
        document.body.appendChild(container);

      }

    afterEach (async () => {
      if (container) {
        container.remove();
        container = null;
      }

       document.body.innerHTML = ''
    });

    bench('snapDOM current version', async () => {
      await setupContainer();
      await snapdom.toRaw(container, {compress: true, fast: true});
    });

     bench('snapDOM V1.8.0', async () => {
      await setupContainer();
      await sd.toRaw(container);
    });

    bench('html2canvas capture', async () => {
      await setupContainer();
      const canvas = await window.html2canvas(container, { logging: false, scale: 1 });
      await canvas.toDataURL();
    });

    bench('modern-screenshot capture', async () => {
      await setupContainer();
      await domToDataUrl(container);
    });
    bench('html-to-image capture', async () => {
      await setupContainer();
      await htmlToImage.toSvg(container);
    });
  });
}

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { inlineBackgroundImages } from '../src/modules/background.js';

describe('inlineBackgroundImages', () => {
  let source, clone;
  beforeEach(() => {
    source = document.createElement('div');
    clone = document.createElement('div');
    document.body.appendChild(source);
    document.body.appendChild(clone);
  });
  afterEach(() => {
    document.body.removeChild(source);
    document.body.removeChild(clone);
  });

  it('does not fail if there is no background-image', async () => {
    source.style.background = 'none';
    await expect(inlineBackgroundImages(source, clone, new WeakMap())).resolves.toBeUndefined();
  });

  it('processes a valid background-image', async () => {
    source.style.backgroundImage = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAn8B9p6Q2wAAAABJRU5ErkJggg==")';
    await expect(inlineBackgroundImages(source, clone, new WeakMap())).resolves.toBeUndefined();
  });
});

import { describe, test, expect, afterEach, afterAll, beforeEach } from 'vitest';
import { snapdom, preCache } from '../src/index';
import { cache } from '../src/core/cache';

const sizes = [
  { width: 200, height: 100, label: 'Small element (200x100)' },
  { width: 400, height: 300, label: 'Modal size (400x300)' },
  { width: 1200, height: 800, label: 'Page view (1200x800)' },
];
let results = [];
function createContainer(size) {
  const container = document.createElement('div');
  container.style.width = `${size.width}px`;
  container.style.height = `${size.height}px`;
  container.style.padding = '20px';
  container.style.overflow = 'auto';
  container.style.background = 'white';
  container.style.border = '2px solid black';
  container.style.fontFamily = 'Arial, sans-serif';
  container.style.color = '#333';
  container.style.position = 'relative';

  const grid = document.createElement('div');
  grid.style.display = 'grid';
  grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(120px, 1fr))';
  grid.style.gap = '10px';

  const cardCount = Math.floor((size.width * size.height) / 20000);
  for (let i = 0; i < cardCount; i++) {
    const card = document.createElement('div');
    card.style.padding = '10px';
    card.style.borderRadius = '8px';
    card.style.background = i % 2 === 0 ? '#f0f0f0' : '#e0eaff';
    card.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
    card.style.display = 'flex';
    card.style.flexDirection = 'column';
    card.style.alignItems = 'center';

    const title = document.createElement('h3');
    title.textContent = `Card ${i + 1}`;
    title.style.margin = '0 0 10px 0';
    title.style.fontSize = '14px';

    const icon = document.createElement('div');
    icon.style.width = '30px';
    icon.style.height = '30px';
    icon.style.borderRadius = '50%';
    icon.style.background = i % 2 === 0 ? 'red' : 'blue';
    icon.style.marginBottom = '10px';

    const text = document.createElement('p');
    text.textContent = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
    text.style.fontSize = '12px';
    text.style.textAlign = 'center';

    card.appendChild(icon);
    card.appendChild(title);
    card.appendChild(text);
    grid.appendChild(card);
  }

  container.appendChild(grid);
  return container;
}

function waitForNextFrame() {
  return new Promise((resolve) => {
    requestAnimationFrame(() => setTimeout(resolve, 0));
  });
}
beforeEach(() => {
  cache.image.clear();
  cache.background.clear();
  cache.resource.clear();
});
afterAll(() => {
  for (const r of results) {
    console.log(r.log);
  }
  results = [];

      document.body.innerHTML = '';
});
for (const size of sizes) {
  describe(`snapDOM performance test (may not be accurate) - ${size.label}`, () => {
    let container;

    afterEach( () => {
      container?.remove();
      container = null;
      document.body.innerHTML = '';

    });

    test('without preCache', async () => {
      container = createContainer(size);
      document.body.appendChild(container);
      await waitForNextFrame();

      const start = performance.now();
      await snapdom.toRaw(container, { compress: true });
      const end = performance.now();

       let log = `[${size.label}] WITHOUT preCache: capture ${(end - start).toFixed(2)}ms`;
       results.push({ log });
      expect(true).toBe(true);

    });

    test('with preCache', async () => {
      container = createContainer(size);
      document.body.appendChild(container);
      await waitForNextFrame();

      const startPre = performance.now();
      await preCache();
      const endPre = performance.now();

      const startCap = performance.now();
      await snapdom.toRaw(container, { compress: true });
      const endCap = performance.now();

      const precacheTime = (endPre - startPre).toFixed(2);
      const captureTime = (endCap - startCap).toFixed(2);

     let log = `[${size.label}] WITH preCache:  capture ${captureTime}ms  (preCache ${precacheTime}ms)  `;

      results.push({ log });

      expect(true).toBe(true);
    });

  });
}

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { iconToImage, embedCustomFonts } from '../src/modules/fonts.js';
import { cache } from '../src/core/cache.js';

// Utilidad para limpiar estilos y links antes de cada test
function cleanFontEnvironment() {
  document.querySelectorAll('style,link[rel="stylesheet"]').forEach(s => s.remove());
}

function addStyleTag(css) {
  const style = document.createElement('style');
  style.setAttribute('data-test-style', 'true');
  style.textContent = css;
  document.head.appendChild(style);
  return style;
}

describe('iconToImage', () => {
  it('genera un dataURL para un carácter unicode', async () => {
    const url = await iconToImage('★', 'Arial', 'bold', 32, '#000');
    expect(url.startsWith('data:image/')).toBe(true);
  });

  it('maneja diferentes pesos y colores de fuente', async () => {
    const url = await iconToImage('★', 'Arial', 700, 40, '#ff0000');
    expect(url.startsWith('data:image/')).toBe(true);
  });

  it('usa valores por defecto si no hay métricas', async () => {
    const orig = HTMLCanvasElement.prototype.getContext;
    HTMLCanvasElement.prototype.getContext = function() {
      return {
        font: '',
        scale: vi.fn(),
        textAlign: '',
        textBaseline: '',
        fillStyle: '',
        fillText: vi.fn(),
        measureText: () => ({ width: 10 })
      };
    };
    const url = await iconToImage('★', 'Arial', 'bold', 32, '#000');
    expect(url.startsWith('data:image/')).toBe(true);
    HTMLCanvasElement.prototype.getContext = orig;
  });
});

describe('embedCustomFonts', () => {
  beforeEach(() => {
    cache.reset();
    cache.font.clear();
    cache.resource.clear();
    cleanFontEnvironment();
    vi.restoreAllMocks();
  });

  it('conserva @font-face con solo local() en src', async () => {
    const style = addStyleTag(`
      @font-face {
        font-family: OnlyLocal;
        src: local("Arial");
        font-style: normal;
        font-weight: normal;
      }
    `);

    const css = await embedCustomFonts();
    expect(css).toMatch(/font-family:\s*OnlyLocal/);
    expect(css).toMatch(/src:\s*local\(["']Arial["']\)/);
    document.head.removeChild(style);
  });

  it('conserva @font-face con local() y sin url()', async () => {
    const style = addStyleTag(`
      @font-face {
        font-family: LocalFont;
        src: local('MyFont'), local('FallbackFont');
        font-style: italic;
        font-weight: bold;
      }
    `);

    const css = await embedCustomFonts();
    expect(css).toMatch(/font-family:\s*LocalFont/);
    expect(css).toMatch(/src:\s*local\(['"]MyFont['"]\),\s*local\(['"]FallbackFont['"]\)/);
   expect(css).toMatch(/font-style:\s*italic/);
    document.head.removeChild(style);
  });
});

import { describe, it, expect, beforeEach } from 'vitest';
import { preCache } from '../src/api/preCache.js';
import { cache } from '../src/core/cache.js';

describe('preCache', () => {
  beforeEach(() => {
    cache.reset()
  });

  it('pre-caches images and backgrounds', async () => {
    const el = document.createElement('div');
    const img = document.createElement('img');
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAn8B9p6Q2wAAAABJRU5ErkJggg==';
    el.appendChild(img);
    document.body.appendChild(el);
    await preCache(el);
    expect(cache.image.has(img.src)).toBe(true);
    document.body.removeChild(el);
  });

  it('should handle preCache with embedFonts false', async () => {
    const el = document.createElement('div');
    await expect(preCache(el, { embedFonts: false })).resolves.toBeUndefined();
  });

  it('should handle preCache with images that fail to load', async () => {
    const el = document.createElement('div');
    const img = document.createElement('img');
    img.src = 'invalid-url.png';
    el.appendChild(img);
    await expect(preCache(el)).resolves.toBeUndefined();
  });

  it('should handle preCache with backgrounds that fail to load', async () => {
    const el = document.createElement('div');
    el.style.backgroundImage = 'url(invalid-url.png)';
    document.body.appendChild(el);
    await expect(preCache(el)).resolves.toBeUndefined();
    document.body.removeChild(el);
  });

  it('limpia los caches y retorna si reset=true', async () => {
    // Prellenar los caches
    cache.snapshotKey.set('foo', 'bar');
    cache.preStyleMap.set('foo', 'bar');
    cache.preNodeMap.set('foo', 'bar');
    await preCache(document, { reset: true });
    expect(cache.snapshotKey.size).toBe(0);
    expect(cache.preStyleMap.size).toBe(0);
    expect(cache.preNodeMap.size).toBe(0);
  });

  it('procesa múltiples backgrounds en un solo elemento', async () => {
    const el = document.createElement('div');
    el.style.backgroundImage = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAn8B9p6Q2wAAAABJRU5ErkJggg==), url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAn8B9p6Q2wAAAABJRU5ErkJggg==)';
    document.body.appendChild(el);
    await preCache(el);
    // No assertion estricta porque depende de helpers, pero no debe lanzar error
    document.body.removeChild(el);
  });

});

import { describe, it, beforeEach, afterEach, afterAll } from 'vitest';
import { snapdom }  from '../src/index';
//ok

describe('Output file size snapdom vs html2canvas (cdn with averaging)', () => {
  let container;
  let html2canvas;
  let report;

  beforeEach(async () => {
    container = document.createElement('div');
    container.style.width = '400px';
    container.style.height = '300px';
    container.style.background = 'linear-gradient(to right, red, blue)';
    container.innerHTML = '
Hello Benchmark

Testing multiple runs...
';
    document.body.appendChild(container);

    if (!html2canvas) {
      // Cargar html2canvas desde CDN
      await new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
        script.onload = () => {
          html2canvas = window.html2canvas;
          resolve();
        };
        script.onerror = reject;
        document.head.appendChild(script);
      });
    }
  });

  afterEach(() => {
    document.body.removeChild(container);
  });

  afterAll(() => {
    console.log(report)
  })

  it('snapdom output file size should be smaller than html2canvas', async () => {

    // SnapDOM capture
    const snapdomDataURL = await snapdom.toRaw(container, {compress: true});
    const snapdomSizeKB = (snapdomDataURL.length * 3 / 4) / 1024; // Base64 to bytes approx

    // html2canvas capture
    const canvas = await html2canvas(container, { logging: false });
    const html2canvasDataURL = await canvas.toDataURL();
    const html2canvasSizeKB = (html2canvasDataURL.length * 3 / 4) / 1024; // Base64 to bytes approx

    const differencePercent = ((html2canvasSizeKB - snapdomSizeKB) / html2canvasSizeKB) * 100;

    report =`snapDom Captured file size is ${differencePercent.toFixed(2)}% smaller compared to html2canvas (${snapdomSizeKB.toFixed(2)} KB vs. ${html2canvasSizeKB.toFixed(2)} KB)`

})

})

import { describe, it, expect, vi } from 'vitest';
import { snapdom } from '../src/api/snapdom.js';

describe('snapdom API (direct)', () => {
  it('throws on null element', async () => {
    await expect(snapdom(null)).rejects.toThrow();
  });

  it('snapdom.capture returns export methods', async () => {
    const el = document.createElement('div');
    el.style.width = '100px';
    el.style.height = '50px';
    document.body.appendChild(el);
    const result = await snapdom.capture(el);
    expect(result).toHaveProperty('toRaw');
    expect(result).toHaveProperty('toImg');
    expect(result).toHaveProperty('download');
    document.body.removeChild(el);
  });

  it('snapdom.toRaw, toImg, toCanvas, toBlob, toPng, toJpg, toWebp, download', async () => {
    const el = document.createElement('div');
    el.style.width = '100px';
    el.style.height = '50px';
    document.body.appendChild(el);
    await snapdom.toRaw(el);
    await snapdom.toImg(el);
    await snapdom.toCanvas(el);
    await snapdom.toBlob(el);
    await snapdom.toPng(el);
    await snapdom.toJpg(el);
    await snapdom.toWebp(el);
    await snapdom.download(el, { format: 'png', filename: 'test' });
    document.body.removeChild(el);
  });

  it('cubre rama Safari en toImg', async () => {
    vi.resetModules();
    vi.mock('../utils/helpers.js', async () => {
      const actual = await vi.importActual('../utils/helpers.js');
      return { ...actual, isSafari: true };
    });
    const { snapdom } = await import('../src/api/snapdom.js');
    const el = document.createElement('div');
    el.style.width = '10px';
    el.style.height = '10px';
    document.body.appendChild(el);
    // Forzar un SVG dataURL simple
    const img = new Image();
    img.width = 10;
    img.height = 10;
    img.decode = () => Promise.resolve();
    globalThis.Image = function() { return img; };
    const res = await snapdom.capture(el);
    await res.toImg();
    document.body.removeChild(el);
    vi.resetModules();
  });

  it('cubre rama de download SVG', async () => {
    const el = document.createElement('div');
    el.style.width = '10px';
    el.style.height = '10px';
    document.body.appendChild(el);
    // Mock a.click y URL.createObjectURL
    const a = document.createElement('a');
    document.body.appendChild(a);
    const origCreate = URL.createObjectURL;
    URL.createObjectURL = () => 'blob:url';
    const origClick = a.click;
    a.click = () => {};
    HTMLAnchorElement.prototype.click = () => {};
    const { snapdom } = await import('../src/api/snapdom.js');
    await snapdom.download(el, { format: 'svg', filename: 'testsvg' });
    URL.createObjectURL = origCreate;
    a.click = origClick;
    document.body.removeChild(a);
    document.body.removeChild(el);
  });

  it('snapdom.toBlob supports type options ', async () => {
  const el = document.createElement('div');
  el.style.width = '50px';
  el.style.height = '30px';
  document.body.appendChild(el);

  const result = await snapdom.capture(el);

  const pngBlob = await result.toBlob({ type: 'png' });
  expect(pngBlob).toBeInstanceOf(Blob);
  expect(pngBlob.type).toBe('image/png');

  const jpgBlob = await result.toBlob({ type: 'jpeg', quality: 0.8 });
  expect(jpgBlob).toBeInstanceOf(Blob);
  expect(jpgBlob.type).toBe('image/jpeg');

  const webpBlob = await result.toBlob({ type: 'webp', quality: 0.9 });
  expect(webpBlob).toBeInstanceOf(Blob);
  expect(webpBlob.type).toBe('image/webp');

  // default fallback
  const svgBlob = await result.toBlob();
  expect(svgBlob).toBeInstanceOf(Blob);
  expect(svgBlob.type).toBe('image/svg+xml');

  document.body.removeChild(el);
});

it('toPng, toJpg, toWebp return HTMLImageElement with  URLs', async () => {
  const el = document.createElement('div');
  el.style.width = '60px';
  el.style.height = '40px';
  document.body.appendChild(el);
  const snap = await snapdom.capture(el);

  const pngImg = await snap.toPng();
  expect(pngImg).toBeInstanceOf(HTMLImageElement);
  expect(typeof pngImg.src).toBe('string');
expect(pngImg.src.startsWith('data:image/png')).toBe(true);

  const jpgImg = await snap.toJpg();
  expect(jpgImg).toBeInstanceOf(HTMLImageElement);
  expect(typeof jpgImg.src).toBe('string');
expect(jpgImg.src.startsWith('data:image/jpeg')).toBe(true);

  const webpImg = await snap.toWebp();
  expect(webpImg).toBeInstanceOf(HTMLImageElement);
  expect(typeof webpImg.src).toBe('string');
expect(webpImg.src.startsWith('data:image/webp')).toBe(true);
  document.body.removeChild(el);
});

it('snapdom should support exclude option to filter out elements by CSS selectors', async () => {
  const el = document.createElement('div');
  el.innerHTML = `

Title

Should be excluded

Private data

This should remain

  `;
  document.body.appendChild(el);

  const result = await snapdom(el, { exclude: ['.exclude-me', '[data-private]'] });

  const svg = result.toRaw();
  const decoded = decodeURIComponent(svg.split(",")[1]);
  expect(decoded).not.toContain('Should be excluded');
  expect(decoded).not.toContain('Private data');
  expect(decoded).toContain('Title');
  expect(decoded).toContain('This should remain');
});

it('snapdom should support filter option to exclude elements with custom logic', async () => {
  const el = document.createElement('div');
  el.innerHTML = `

Level 1

Level 2

Level 3

  `;
  document.body.appendChild(el);
  const result = await snapdom(el, {
    filter: (element) => !element.classList.contains('level-3')
  });

    const svg = result.toRaw();
  const decoded = decodeURIComponent(svg.split(",")[1]);
  expect(decoded).toContain('Level 1');
  expect(decoded).toContain('Level 2');
  expect(decoded).not.toContain('Level 3');
});

it('snapdom should support combining exclude and filter options', async () => {
  const el = document.createElement('div');
  el.innerHTML = `

Exclude by selector

Exclude by filter

Keep this content

  `;
  document.body.appendChild(el);

  const result = await snapdom(el, {
    exclude: ['.exclude-by-selector'],
    filter: (element) => !element.classList.contains('exclude-by-filter')
  });

   const svg = result.toRaw();
  const decoded = decodeURIComponent(svg.split(",")[1]);
  expect(decoded).not.toContain('Exclude by selector');
  expect(decoded).not.toContain('Exclude by filter');
  expect(decoded).toContain('Keep this content');
});

});

import { describe, it, expect, vi } from 'vitest';
import { captureDOM } from '../src/core/capture.js';

describe('captureDOM edge cases', () => {
  it('throws for unsupported element (unknown nodeType)', async () => {
    // Simulate a node with an invalid nodeType
    const fakeNode = { nodeType: 999 };
    await expect(captureDOM(fakeNode)).rejects.toThrow();
  });

  it('throws if element is null', async () => {
    await expect(captureDOM(null)).rejects.toThrow();
  });

  it('throws error if getBoundingClientRect fails', async () => {
    const el = document.createElement('div');
    vi.spyOn(el, 'getBoundingClientRect').mockImplementation(() => { throw new Error('fail'); });
    await expect(captureDOM(el)).rejects.toThrow('fail');
  });
});

describe('captureDOM functional', () => {
  it('captures a simple div and returns an SVG dataURL', async () => {
    const el = document.createElement('div');
    el.textContent = 'test';
    const url = await captureDOM(el);
    expect(url.startsWith('data:image/svg+xml')).toBe(true);
  });

  it('supports scale and width/height options', async () => {
    const el = document.createElement('div');
    el.style.width = '100px';
    el.style.height = '50px';
    await captureDOM(el, { scale: 2 });
    await captureDOM(el, { width: 200 });
    await captureDOM(el, { height: 100 });
  });

  it('supports fast=false', async () => {
    const el = document.createElement('div');
    await captureDOM(el, { fast: false });
  });

  it('supports embedFonts', async () => {
    const el = document.createElement('div');
    await captureDOM(el, { embedFonts: true });
  });
});

import { describe, it, expect, vi } from 'vitest';
import { deepClone } from '../src/core/clone.js';

describe('deepClone', () => {
  it('clones a simple div', () => {
    const el = document.createElement('div');
    el.textContent = 'hello';
    const styleMap = new Map();
    const styleCache = new WeakMap();
    const nodeMap = new Map();
    const clone = deepClone(el, styleMap, styleCache, nodeMap, false);
    expect(clone).not.toBe(el);
    expect(clone.textContent).toBe('hello');
  });

  it('clones canvas as an image', () => {
    const canvas = document.createElement('canvas');
    canvas.width = 10;
    canvas.height = 10;
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'red';
    ctx.fillRect(0,0,10,10);
    const styleMap = new Map();
    const styleCache = new WeakMap();
    const nodeMap = new Map();
    const clone = deepClone(canvas, styleMap, styleCache, nodeMap, false);
    expect(clone.tagName).toBe('IMG');
    expect(clone.src.startsWith('data:image/')).toBe(true);
  });

  it('deepClone handles data-capture="exclude"', () => {
    const el = document.createElement('div');
    el.setAttribute('data-capture', 'exclude');
    const clone = deepClone(el, true, {});
    expect(clone).not.toBeNull();
  });

  it('deepClone handles data-capture="placeholder"', () => {
    const el = document.createElement('div');
    el.setAttribute('data-capture', 'placeholder');
    el.setAttribute('data-placeholder-text', 'Placeholder!');
    const clone = deepClone(el, true, {});
    expect(clone.textContent).toContain('Placeholder!');
  });

  it('deepClone handles iframe', () => {
    const iframe = document.createElement('iframe');
    iframe.width = 100;
    iframe.height = 50;
    const clone = deepClone(iframe, true, {});
    expect(clone.tagName).toBe('DIV');
  });

  it('deepClone handles input, textarea, select', () => {
    const input = document.createElement('input');
    input.value = 'foo';
    input.checked = true;
    const textarea = document.createElement('textarea');
    textarea.value = 'bar';
    const select = document.createElement('select');
    const opt = document.createElement('option');
    opt.value = 'baz';
    select.appendChild(opt);
    select.value = 'baz';
    [input, textarea, select].forEach(el => {
      const clone = deepClone(el, true, {});
      expect(clone.value).toBe(el.value);
    });
  });

  it('deepClone handles shadow DOM', () => {
    const el = document.createElement('div');
    const shadow = el.attachShadow({mode:'open'});
    const span = document.createElement('span');
    span.textContent = 'shadow';
    shadow.appendChild(span);
    const clone = deepClone(el, true, {});
    expect(clone).not.toBeNull();
  });
});

describe('deepClone edge cases', () => {
  it('clones unsupported node (Comment) as a new Comment', () => {
    const fake = document.createComment('not supported');
    const result = deepClone(fake, true, {});
    expect(result.nodeType).toBe(Node.COMMENT_NODE);
    expect(result.textContent).toBe('not supported');
    expect(result).not.toBe(fake); // Es un clon, no el mismo objeto
  });
  it('clones attributes and children', () => {
    const el = document.createElement('div');
    el.setAttribute('data-test', '1');
    const result = deepClone(el, true, {});
    expect(result.getAttribute('data-test')).toBe('1');
  });
});

import { describe, it, expect } from 'vitest';
import { inlineAllStyles } from '../src/modules/styles.js';
import { cache } from '../src/core/cache.js';

describe('inlineAllStyles', () => {
  it('adds a style key to the styleMap', () => {
    const el = document.createElement('div');
    const clone = document.createElement('div');
    inlineAllStyles(el, clone, false);
    expect(cache.preStyleMap.has(clone)).toBe(true);
  });
});

import { describe, it, expect, vi } from 'vitest';
import { prepareClone } from '../src/core/prepare.js';

describe('prepareClone', () => {
  it('prepares a clone of a div', async () => {
    const el = document.createElement('div');
    el.textContent = 'test';
    const { clone, classCSS } = await prepareClone(el);
    expect(clone).not.toBeNull();
    expect(typeof classCSS).toBe('string');
  });
});

describe('prepareClone edge cases', () => {
  it('throws for null node', async () => {
    await expect(prepareClone(null)).rejects.toThrow();
  });
  it('returns a comment clone for unsupported node', async () => {
    const fake = document.createComment('not supported');
    const result = await prepareClone(fake);
    expect(result.clone.nodeType).toBe(Node.COMMENT_NODE);
  });
  it('handles error in internal logic', async () => {
    const el = document.createElement('div');
    vi.spyOn(el, 'cloneNode').mockImplementation(() => { throw new Error('fail'); });
    await expect(prepareClone(el)).rejects.toThrow('fail');
  });
  it('clones attributes and children', async () => {
    const el = document.createElement('div');
    el.setAttribute('data-test', '1');
    const result = await prepareClone(el);
    expect(result.clone.getAttribute('data-test')).toBe('1');
  });
});

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { snapdom } from  '../src/index';

describe('snapdom advanced tests', () => {
  let testElement;

  beforeEach(() => {
    testElement = document.createElement('div');
    testElement.style.width = '100px';
    testElement.style.height = '50px';
    testElement.innerHTML = '
Hello World
';
    document.body.appendChild(testElement);
  });

  afterEach(() => {
    document.body.removeChild(testElement);
  });

  it('should generate different SVGs for different scales', async () => {
    const svg1 = await snapdom.toImg(testElement, { scale: 1 });
    const svg2 = await snapdom.toImg(testElement, { scale: 2 });
    expect(svg1).not.toBe(svg2);
  });

  it('captured SVG should contain inner text content', async () => {
    const svgDataUrl = await snapdom.toRaw(testElement);
    const svgText = decodeURIComponent(svgDataUrl.split(',')[1]);
    expect(svgText).toContain('Hello World');
  });

  it('should throw an error if element is null', async () => {
    await expect(() => snapdom.toRaw(null)).rejects.toThrow();
  });

  it('should generate SVG with correct attributes', async () => {
    const svgDataUrl = await snapdom.toRaw(testElement);
    const svgText = decodeURIComponent(svgDataUrl.split(',')[1]);
    const parser = new DOMParser();
    const doc = parser.parseFromString(svgText, 'image/svg+xml');
    const svg = doc.querySelector('svg');

    expect(svg).not.toBeNull();
    expect(svg.getAttribute('width')).toBe('100');
    expect(svg.getAttribute('height')).toBe('50');
    expect(svg.getAttribute('viewBox')).toBe('0 0 100 50');
  });

  it('snapdom.toBlob should contain valid SVG content', async () => {
    const blob = await snapdom.toBlob(testElement);
    const text = await blob.text();
    expect(text).toContain('<svg');
    expect(text).toContain('</svg>');
  });

  it('should replace iframes with fallback visuals', async () => {
  const iframe = document.createElement('iframe');
  iframe.style.width = '200px';
  iframe.style.height = '100px';
  testElement.appendChild(iframe);

  const svgDataUrl = await snapdom.toRaw(testElement);
  const svgText = decodeURIComponent(svgDataUrl.split(',')[1]);

  const parser = new DOMParser();
  const doc = parser.parseFromString(svgText, 'image/svg+xml');

  // No debe haber iframes en el SVG
  expect(doc.querySelectorAll('iframe').length).toBe(0);

  // Buscamos divs con estilo inline que contengan repeating-linear-gradient
  const fallbackDivs = Array.from(doc.querySelectorAll('div')).filter(div => {
    const styleAttr = div.getAttribute('style') || '';
    return styleAttr.includes('repeating-linear-gradient');
  });

  expect(fallbackDivs.length).toBeGreaterThan(0);
});

});

/**
 * Entry point for snapDOM library exports.
 *
 * @file index.browser.js
 */

import { snapdom } from './api/snapdom.js';
import { preCache } from './api/preCache.js';

if (typeof window !== 'undefined') {
  window.snapdom = snapdom;
  window.preCache = preCache;
}

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { snapdom } from '../src/index';

describe('snapdom API', () => {
  let testElement;

  beforeEach(() => {
    testElement = document.createElement('div');
    testElement.style.width = '100px';
    testElement.style.height = '50px';
    document.body.appendChild(testElement );
  });

  afterEach(() => {
    document.body.removeChild(testElement );
  });

  describe('snapdom.toRaw', () => {
    it('should return a SVG data URL', async () => {
      const result = await snapdom.toRaw(testElement );
      expect(result).toMatch(/^data:image\/svg\+xml/);
    });
  });

  describe('snapdom', () => {
    it('toImg should return an HTMLImageElement', async () => {

      const img = await snapdom.toImg(testElement );
      expect(img).toBeInstanceOf(HTMLImageElement);
      expect(img.src).toMatch(/^data:image\/svg\+xml/);

    });

    it('toCanvas should return a HTMLCanvasElement', async () => {
     // const decodeMock = vi.spyOn(window.Image.prototype, 'decode').mockResolvedValue();
      const canvas = await snapdom.toCanvas(testElement );
      expect(canvas).toBeInstanceOf(HTMLCanvasElement);
    //  decodeMock.mockRestore();
    });

    it('toPng should return an HTMLImageElement with PNG data URL', async () => {
    //  const decodeMock = vi.spyOn(window.Image.prototype, 'decode').mockResolvedValue();
      const img = await snapdom.toPng(testElement );
      expect(img).toBeInstanceOf(HTMLImageElement);
      expect(img.src).toMatch(/^data:image\/png/);
    //  decodeMock.mockRestore();
    });

    it('toJpg should return an HTMLImageElement with JPEG data URL', async () => {
     // const decodeMock = vi.spyOn(window.Image.prototype, 'decode').mockResolvedValue();
      const img = await snapdom.toJpg(testElement );
      expect(img).toBeInstanceOf(HTMLImageElement);
      expect(img.src).toMatch(/^data:image\/jpeg/);
     // decodeMock.mockRestore();
    });

    it('toWebp should return an HTMLImageElement with WebP data URL', async () => {
    //  const decodeMock = vi.spyOn(window.Image.prototype, 'decode').mockResolvedValue();
      const img = await snapdom.toWebp(testElement );
      expect(img).toBeInstanceOf(HTMLImageElement);
      expect(img.src).toMatch(/^data:image\/webp/);
    //  decodeMock.mockRestore();
    });

    it('toBlob should return a Blob of type image/svg+xml', async () => {
      const blob = await snapdom.toBlob(testElement );
      expect(blob).toBeInstanceOf(Blob);
      expect(blob.type).toBe('image/svg+xml');
    });
  });
});

import { describe, it, beforeEach, afterEach, afterAll } from 'vitest';
import { domToDataUrl} from 'https://unpkg.com/modern-screenshot';
import { snapdom }  from '../src/index';
//ok

describe('Output file size snapdom vs modern-screeenshot (cdn with averaging)', () => {
  let container;

  let report;

  beforeEach(async () => {
    container = document.createElement('div');
    container.style.width = '400px';
    container.style.height = '300px';
    container.style.background = 'linear-gradient(to right, red, blue)';
    container.innerHTML = '
Hello Benchmark

Testing multiple runs...
';
    document.body.appendChild(container);
  });

  afterEach(() => {
    document.body.removeChild(container);
  });

  afterAll(() => {
    console.log(report)
  })

  it('snapdom output file size should be smaller than modern-screenshot', async () => {

    // SnapDOM capture
    const snapdomDataURL = await snapdom.toRaw(container, {compress: true});
    const snapdomSizeKB = (snapdomDataURL.length * 3 / 4) / 1024; // Base64 to bytes approx

    // domToDataUrl capture
    const domToDataUrlDataURL = await domToDataUrl(container);
    const domToDataUrlSizeKB = (domToDataUrlDataURL.length * 3 / 4) / 1024; // Base64 to bytes approx

    const differencePercent = ((domToDataUrlSizeKB - snapdomSizeKB) / domToDataUrlSizeKB) * 100;

    report =`snapdom captured file size is ${differencePercent.toFixed(2)}% smaller compared to modern-screenshot (${snapdomSizeKB.toFixed(2)} KB vs. ${domToDataUrlSizeKB.toFixed(2)} KB)`

})

})

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { inlineImages } from '../src/modules/images.js';

describe('inlineImages', () => {
  let container;
  beforeEach(() => {
    container = document.createElement('div');
    document.body.appendChild(container);
  });
  afterEach(() => {
    document.body.removeChild(container);
  });

  it('converts <img> to dataURL if the image loads', async () => {
    const img = document.createElement('img');
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/w8AAn8B9p6Q2wAAAABJRU5ErkJggg==';
    container.appendChild(img);
    await inlineImages(container);
    expect(img.src.startsWith('data:image/')).toBe(true);
  });

  it('replaces <img> with a fallback if the image fails', async () => {
    const img = document.createElement('img');
    img.src = 'invalid-url.png';
    container.appendChild(img);
    await inlineImages(container);
    expect(container.querySelector('div')).not.toBeNull();
    expect(container.querySelector('img')).toBeNull();
  });
});

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { snapdom } from  '../src/index';

describe('snapdom.toJpg backgroundColor option', () => {
  let container;

  beforeEach(() => {
    container = document.createElement('div');
    container.style.width = '100px';
    container.style.height = '100px';
    container.style.background = 'transparent';
    document.body.appendChild(container);
  });

  it('applies white background by default', async () => {
    const img = await snapdom.toJpg(container );
    const canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    const pixel = ctx.getImageData(0, 0, 1, 1).data;
    // JPEG compresses, but for a solid color it should be near white
    expect(pixel[0]).toBeGreaterThan(240);
    expect(pixel[1]).toBeGreaterThan(240);
    expect(pixel[2]).toBeGreaterThan(240);
  });

  it('applies custom background color', async () => {
    const img = await snapdom.toJpg(container, { backgroundColor: '#00ff00'  });
    const canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    const pixel = ctx.getImageData(0, 0, 1, 1).data;
    // Green check (JPEG lossy, so check near values)
    expect(pixel[0]).toBeLessThan(30);    // red
    expect(pixel[1]).toBeGreaterThan(200); // green
    expect(pixel[2]).toBeLessThan(30);    // blue
  });
});

import { describe, it, expect } from 'vitest';
import { getStyleKey, collectUsedTagNames, getDefaultStyleForTag } from '../src/utils/cssTools.js';

describe('getStyleKey', () => {
  it('generates a non-empty style key', () => {
    const snapshot = { color: 'red', 'font-size': '12px' };
    const key = getStyleKey(snapshot, 'div');
    expect(typeof key).toBe('string');
    expect(key.length).toBeGreaterThan(0);
  });

  it('getStyleKey works with compress true', () => {
    const snapshot = { color: 'red', 'font-size': '12px' };
    const key = getStyleKey(snapshot, 'div', true);
    expect(typeof key).toBe('string');
  });
});

describe('collectUsedTagNames', () => {
  it('returns unique tag names', () => {
    const root = document.createElement('div');
    root.innerHTML = '

';
    const tags = collectUsedTagNames(root);
    expect(tags).toContain('div');
    expect(tags).toContain('span');
    expect(tags).toContain('p');
  });
});

describe('getDefaultStyleForTag', () => {
  it('returns a default style object', () => {
    const defaults = getDefaultStyleForTag('div');
    expect(typeof defaults).toBe('object');
  });

  it('getDefaultStyleForTag skips special tags', () => {
    expect(getDefaultStyleForTag('script')).toEqual({});
  });
});

/**
 * Entry point for snapDOM library exports.
 *
 * @file index.js
 */

export { snapdom } from './api/snapdom.js';
export { preCache } from './api/preCache.js';

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      // https://vitest.dev/guide/browser/playwright
      instances: [
        { browser: 'chromium' },
      ],
    },
    coverage: {
      provider: 'v8', // o 'istanbul'
      include: [
        'src/**/*.js',      // Solo archivos JS dentro de src/
      ],
    },
  },
})

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { snapdom } from  '../src/index';

describe('snapdom capture attributes', () => {
  let container;

  beforeEach(() => {
    container = document.createElement('div');
    container.style.width = '300px';
    container.style.height = '150px';
    document.body.appendChild(container);
  });

  afterEach(() => {
    document.body.removeChild(container);
  });

  it('should exclude elements with data-capture="exclude"', async () => {
    const excluded = document.createElement('div');
    excluded.setAttribute('data-capture', 'exclude');
    excluded.textContent = 'Should be excluded';
    container.appendChild(excluded);

    const svgDataUrl = await snapdom.toRaw(container);
    const svgText = decodeURIComponent(svgDataUrl.split(',')[1]);

    expect(svgText).not.toContain('Should be excluded');
  });

  it('should replace elements with data-capture="placeholder" and show placeholder text', async () => {
    const placeholder = document.createElement('div');
    placeholder.setAttribute('data-capture', 'placeholder');
    placeholder.setAttribute('data-placeholder-text', 'Placeholder here');
    placeholder.textContent = 'Original text';
    container.appendChild(placeholder);

    const svgDataUrl = await snapdom.toRaw(container);
    const svgText = decodeURIComponent(svgDataUrl.split(',')[1]);

    expect(svgText).toContain('Placeholder here');
    expect(svgText).not.toContain('Original text');
  });
});

import { it, expect } from 'vitest';
import * as snapdom from '../src/index.browser.js';

it('should import the browser bundle without errors', () => {
  expect(snapdom).toBeDefined();
});

async function inlineBackgroundImages (source, clone, options = {}) {}

This code segment functions to: Traverse hierarchical elements to process and replace referenced image assets with embedded data equivalents, synchronize visual presentation properties between source and cloned structures, and maintain style consistency across nested components during duplication operations.

```javascript
[TASK_BEGIN]
const sourceWalker = document.createTreeWalker(
  source,
  NodeFilter.SHOW_ELEMENT,
  null,
  false
);
const cloneWalker = document.createTreeWalker(
  clone,
  NodeFilter.SHOW_ELEMENT,
  null,
  false
);

const promises = [];

while (true) {
  const sNode = sourceWalker.nextNode();
  const cNode = cloneWalker.nextNode();

  if (!sNode || !cNode) break;

  const bgImage = getStyle(sNode, 'background-image');
  const entries = splitBackgroundImage(bgImage);
  if (entries.length === 0) continue;

  const processEntries = async () => {
    const processedEntries = await Promise.all(
      entries.map(entry => inlineSingleBackgroundEntry(entry, options, cache))
    );
    cNode.style.backgroundImage = processedEntries.join(', ');
  };

  promises.push(processEntries());
}

await Promise.all(promises);
[TASK_END]
```