/** * extractors/advanced.ts — Clone Architect Phase 5 * Extracteurs CSS avancés (@keyframes, z-index, transform 3D, pseudo-elements, * @container queries, grid layouts, responsive per-breakpoint). * Tous sont best-effort avec fallback gracieux. */ import type { Page } from 'playwright'; /** * Scanne tous les stylesheets accessibles pour extraire les @keyframes. * Fallback silencieux sur cross-origin. */ export async function extractKeyframes(page: Page): Promise>>> { return page.evaluate(`(function() { var result = {}; var sheets = document.styleSheets; for (var i = 0; i < sheets.length; i++) { try { var rules = sheets[i].cssRules || sheets[i].rules; if (!rules) continue; for (var j = 0; j < rules.length; j++) { var rule = rules[j]; if (rule && rule.constructor && rule.constructor.name === 'CSSKeyframesRule') { var name = rule.name; if (result[name]) continue; var frames = {}; var kfRules = rule.cssRules; for (var k = 0; k < kfRules.length; k++) { var frame = kfRules[k]; var key = frame.keyText; var styles = {}; var style = frame.style; for (var p = 0; p < style.length; p++) { var prop = style[p]; var val = style.getPropertyValue(prop); if (val) styles[prop] = val.trim(); } frames[key] = styles; } result[name] = frames; if (Object.keys(result).length >= 200) return result; } } } catch (e) { /* cross-origin sheet, skip */ } } return result; })()`) as any; } /** * Extrait z-index non-triviaux + stacking context parent. */ export async function extractZIndexMap(page: Page): Promise> { return page.evaluate(`(function() { function getPath(el) { if (!el || el === document.body) return 'body'; if (el.id) return '#' + el.id; var classes = (el.className || '').toString().split(' ').filter(Boolean).slice(0, 2).join('.'); var tag = el.tagName.toLowerCase(); return classes ? tag + '.' + classes : tag; } function isStackingContext(el) { var cs = getComputedStyle(el); if (cs.position !== 'static') return true; if (parseFloat(cs.opacity) < 1) return true; if (cs.transform !== 'none') return true; if (cs.filter !== 'none') return true; if (cs.mixBlendMode !== 'normal') return true; return false; } var result = []; var elements = document.querySelectorAll('*'); var count = 0; for (var i = 0; i < elements.length && count < 100; i++) { var el = elements[i]; var cs = getComputedStyle(el); var z = parseInt(cs.zIndex); if (isNaN(z) || z === 0) continue; var parent = el.parentElement; var stackingRoot = 'body'; while (parent) { if (isStackingContext(parent) || parent === document.body) { stackingRoot = getPath(parent); break; } parent = parent.parentElement; } result.push({ selector: getPath(el), z: z, stackingRoot: stackingRoot }); count++; } return result; })()`) as any; } /** * Extrait les transforms 3D et parse les fonctions (rotate, translate3d, perspective, matrix3d). */ export async function extractTransform3DMap(page: Page): Promise> { return page.evaluate(`(function() { function getPath(el) { if (!el || el === document.body) return 'body'; if (el.id) return '#' + el.id; var classes = (el.className || '').toString().split(' ').filter(Boolean).slice(0, 2).join('.'); return el.tagName.toLowerCase() + (classes ? '.' + classes : ''); } function parseTransform(css) { if (!css || css === 'none') return null; if (css.indexOf('matrix') === 0) { var m = css.match(/matrix(3d)?\\(([^)]+)\\)/); if (!m) return null; return { type: m[1] ? 'matrix3d' : 'matrix', values: m[2].split(',').map(function(v){return parseFloat(v.trim());}) }; } var fns = []; var regex = /(\\w+)\\(([^)]+)\\)/g; var match; while ((match = regex.exec(css)) !== null) { fns.push({ fn: match[1], args: match[2].split(',').map(function(s){return s.trim();}) }); } return fns.length ? { type: 'functions', fns: fns } : null; } var result = {}; var elements = document.querySelectorAll('*'); var count = 0; for (var i = 0; i < elements.length && count < 150; i++) { var el = elements[i]; var cs = getComputedStyle(el); var transform = cs.transform; if (!transform || transform === 'none') continue; var parsed = parseTransform(transform); if (!parsed) continue; var key = getPath(el); if (result[key]) continue; result[key] = { raw: transform, parsed: parsed, transformOrigin: cs.transformOrigin, perspective: cs.perspective !== 'none' ? cs.perspective : undefined, }; count++; } return result; })()`) as any; } /** * Container queries (@container) + container-type/name sur éléments. */ export async function extractContainerQueries(page: Page): Promise<{queries: Array<{name?: string; condition: string; rulesCount: number}>; containers: string[]}> { return page.evaluate(`(function() { var result = []; var sheets = document.styleSheets; for (var i = 0; i < sheets.length; i++) { try { var rules = sheets[i].cssRules || sheets[i].rules; if (!rules) continue; for (var j = 0; j < rules.length; j++) { var rule = rules[j]; if (rule && rule.constructor && rule.constructor.name === 'CSSContainerRule') { result.push({ name: rule.containerName || undefined, condition: rule.containerQuery || rule.conditionText || '', rulesCount: rule.cssRules ? rule.cssRules.length : 0, }); if (result.length >= 50) break; } if (rule && rule.constructor && rule.constructor.name === 'CSSMediaRule' && rule.cssRules) { for (var k = 0; k < rule.cssRules.length; k++) { var inner = rule.cssRules[k]; if (inner && inner.constructor && inner.constructor.name === 'CSSContainerRule') { result.push({ name: inner.containerName || undefined, condition: inner.containerQuery || inner.conditionText || '', rulesCount: inner.cssRules ? inner.cssRules.length : 0, }); } } } } } catch (e) { /* cross-origin, skip */ } } var elements = document.querySelectorAll('*'); var containers = new Set(); for (var m = 0; m < elements.length; m++) { var cs = getComputedStyle(elements[m]); var ctype = cs.containerType; if (ctype && ctype !== 'normal') { var cname = cs.containerName; containers.add(ctype + ':' + (cname || 'anonymous')); } } return { queries: result, containers: Array.from(containers) }; })()`) as any; } /** * grid-template-areas + named lines: capture les layouts nommés. */ export async function extractGridLayouts(page: Page): Promise> { return page.evaluate(`(function() { function getPath(el) { if (!el || el === document.body) return 'body'; if (el.id) return '#' + el.id; var classes = (el.className || '').toString().split(' ').filter(Boolean).slice(0, 2).join('.'); return el.tagName.toLowerCase() + (classes ? '.' + classes : ''); } var result = []; var elements = document.querySelectorAll('*'); var count = 0; for (var i = 0; i < elements.length && count < 50; i++) { var el = elements[i]; var cs = getComputedStyle(el); if (cs.display !== 'grid' && cs.display !== 'inline-grid') continue; var areas = cs.gridTemplateAreas; var cols = cs.gridTemplateColumns; var rows = cs.gridTemplateRows; if ((!areas || areas === 'none') && (!cols || cols === 'none') && (!rows || rows === 'none')) continue; result.push({ selector: getPath(el), areas: (areas && areas !== 'none') ? areas : undefined, cols: (cols && cols !== 'none') ? cols : undefined, rows: (rows && rows !== 'none') ? rows : undefined, }); count++; } return result; })()`) as any; } /** * Pseudo-elements ::before / ::after — capture content + styles critiques. */ export async function extractPseudoElements(page: Page): Promise; after?: Record }>> { return page.evaluate(`(function() { function nthPath(el) { if (!el || el === document.body) return 'body'; var path = []; var current = el; var depth = 0; while (current && current !== document.body && depth < 8) { var parent = current.parentElement; if (!parent) break; var siblings = Array.from(parent.children); var idx = siblings.indexOf(current); var tag = current.tagName.toLowerCase(); path.unshift(tag + ':nth-child(' + (idx + 1) + ')'); current = parent; depth++; } return path.length ? 'body > ' + path.join(' > ') : 'body'; } var PSEUDO_PROPS = [ 'content', 'display', 'position', 'top', 'left', 'right', 'bottom', 'width', 'height', 'backgroundColor', 'color', 'backgroundImage', 'border', 'borderRadius', 'transform', 'opacity', 'fontSize', 'fontFamily', 'fontWeight', 'padding', 'margin', 'boxShadow', ]; var result = {}; var elements = document.querySelectorAll('*'); var count = 0; for (var i = 0; i < elements.length && count < 100; i++) { var el = elements[i]; var data = {}; for (var p = 0; p < 2; p++) { var pseudo = p === 0 ? '::before' : '::after'; var cs = getComputedStyle(el, pseudo); var content = cs.content; if (!content || content === 'none' || content === 'normal') continue; var styles = { content: content }; for (var k = 0; k < PSEUDO_PROPS.length; k++) { var prop = PSEUDO_PROPS[k]; if (prop === 'content') continue; var val = cs[prop]; if (val && val !== 'none' && val !== 'normal' && val !== '0px' && val !== 'auto' && val !== 'rgba(0, 0, 0, 0)') { styles[prop] = val; } } data[pseudo.replace('::', '')] = styles; } if (Object.keys(data).length > 0) { var key = nthPath(el); if (!result[key]) { result[key] = data; count++; } } } return result; })()`) as any; } /** * Responsive snapshot: snapshot des styles clés sur 360px + 768px. */ export async function extractResponsiveSnapshot( page: Page, currentViewport: { width: number; height: number }, ): Promise>>> { const TARGET_WIDTHS = [360, 768, 1440].filter(w => w !== currentViewport.width); const result: Record>> = {}; const originalSize = { width: currentViewport.width, height: currentViewport.height }; for (const width of TARGET_WIDTHS) { try { await page.setViewportSize({ width, height: 900 }); await page.waitForTimeout(250); const snapshot = await page.evaluate(`(function() { function getPath(el) { if (!el) return ''; if (el.id) return '#' + el.id; var classes = (el.className || '').toString().split(' ').filter(Boolean).slice(0, 2).join('.'); return el.tagName.toLowerCase() + (classes ? '.' + classes : ''); } var targets = [ 'header', 'nav', 'main', 'footer', 'section.hero, [class*="hero"]', '[class*="card"]:not([class*="cards"])', 'button, [class*="btn"]', 'h1', 'h2', '[class*="container"]', ]; var result = {}; var CRITICAL_PROPS = [ 'display', 'flexDirection', 'gridTemplateColumns', 'gridTemplateRows', 'padding', 'margin', 'gap', 'fontSize', 'lineHeight', 'width', 'height', 'maxWidth', 'position', 'top', 'left', 'right', 'bottom', 'textAlign', ]; for (var t = 0; t < targets.length; t++) { var els = document.querySelectorAll(targets[t]); var limit = Math.min(els.length, 3); for (var i = 0; i < limit; i++) { var el = els[i]; var path = getPath(el); if (!path || result[path]) continue; var cs = getComputedStyle(el); var styles = {}; for (var p = 0; p < CRITICAL_PROPS.length; p++) { var prop = CRITICAL_PROPS[p]; var val = cs[prop]; if (val && val !== 'none' && val !== 'normal' && val !== '0px' && val !== 'auto') { styles[prop] = val; } } if (Object.keys(styles).length > 0) { result[path] = styles; } } } return result; })()`) as Record>; result[width] = snapshot; } catch (err) { console.warn(` ⚠️ Responsive ${width}px failed: ${(err as Error).message}`); } } try { await page.setViewportSize(originalSize); await page.waitForTimeout(100); } catch { /* ignore */ } return result; }