mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
Enhanced extract-layout-structure.js with intelligent selector discovery: - Auto-detects missing main containers when <3 found - Scans large visible containers (≥500×300px) - Extracts class patterns (main/content/wrapper/container/page/layout/app) - Suggests new selectors to add to script - Returns exploration data with recommendations Added commonClassSelectors strategy: - .main, .content, .main-content, .page-content - .container.main, .wrapper > .main - div[class*="main-wrapper"], div[class*="content-wrapper"] Updated layout-extract.md with progressive exploration documentation. Version: 2.1.0 → 2.2.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
412 lines
12 KiB
JavaScript
412 lines
12 KiB
JavaScript
/**
|
||
* Extract Layout Structure from DOM - Enhanced Version
|
||
*
|
||
* Extracts real layout information from DOM to provide accurate
|
||
* structural data for UI replication.
|
||
*
|
||
* Features:
|
||
* - Framework detection (Nuxt.js, Next.js, React, Vue, Angular)
|
||
* - Multi-strategy container detection (strict → relaxed → class-based → framework-specific)
|
||
* - Intelligent main content detection with common class names support
|
||
* - Supports modern SPA frameworks
|
||
* - Detects non-semantic main containers (.main, .content, etc.)
|
||
* - Progressive exploration: Auto-discovers missing selectors when standard patterns fail
|
||
* - Suggests new class names to add to script based on actual page structure
|
||
*
|
||
* Progressive Exploration:
|
||
* When fewer than 3 main containers are found, the script automatically:
|
||
* 1. Analyzes all large visible containers (≥500×300px)
|
||
* 2. Extracts class name patterns (main/content/wrapper/container/page/etc.)
|
||
* 3. Suggests new selectors to add to the script
|
||
* 4. Returns exploration data in result.exploration
|
||
*
|
||
* Usage: Execute via Chrome DevTools evaluate_script
|
||
* Version: 2.2.0
|
||
*/
|
||
|
||
(() => {
|
||
/**
|
||
* Get element's bounding box relative to viewport
|
||
*/
|
||
const getBounds = (element) => {
|
||
const rect = element.getBoundingClientRect();
|
||
return {
|
||
x: Math.round(rect.x),
|
||
y: Math.round(rect.y),
|
||
width: Math.round(rect.width),
|
||
height: Math.round(rect.height)
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Extract layout properties from an element
|
||
*/
|
||
const extractLayoutProps = (element) => {
|
||
const s = window.getComputedStyle(element);
|
||
|
||
return {
|
||
// Core layout
|
||
display: s.display,
|
||
position: s.position,
|
||
|
||
// Flexbox
|
||
flexDirection: s.flexDirection,
|
||
justifyContent: s.justifyContent,
|
||
alignItems: s.alignItems,
|
||
flexWrap: s.flexWrap,
|
||
gap: s.gap,
|
||
|
||
// Grid
|
||
gridTemplateColumns: s.gridTemplateColumns,
|
||
gridTemplateRows: s.gridTemplateRows,
|
||
gridAutoFlow: s.gridAutoFlow,
|
||
|
||
// Dimensions
|
||
width: s.width,
|
||
height: s.height,
|
||
maxWidth: s.maxWidth,
|
||
minWidth: s.minWidth,
|
||
|
||
// Spacing
|
||
padding: s.padding,
|
||
margin: s.margin
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Identify layout pattern for an element
|
||
*/
|
||
const identifyPattern = (props) => {
|
||
const { display, flexDirection, gridTemplateColumns } = props;
|
||
|
||
if (display === 'flex' || display === 'inline-flex') {
|
||
if (flexDirection === 'column') return 'flex-column';
|
||
if (flexDirection === 'row') return 'flex-row';
|
||
return 'flex';
|
||
}
|
||
|
||
if (display === 'grid') {
|
||
const cols = gridTemplateColumns;
|
||
if (cols && cols !== 'none') {
|
||
const colCount = cols.split(' ').length;
|
||
return `grid-${colCount}col`;
|
||
}
|
||
return 'grid';
|
||
}
|
||
|
||
if (display === 'block') return 'block';
|
||
|
||
return display;
|
||
};
|
||
|
||
/**
|
||
* Detect frontend framework
|
||
*/
|
||
const detectFramework = () => {
|
||
if (document.querySelector('#__nuxt')) return { name: 'Nuxt.js', version: 'unknown' };
|
||
if (document.querySelector('#__next')) return { name: 'Next.js', version: 'unknown' };
|
||
if (document.querySelector('[data-reactroot]')) return { name: 'React', version: 'unknown' };
|
||
if (document.querySelector('[ng-version]')) return { name: 'Angular', version: 'unknown' };
|
||
if (window.Vue) return { name: 'Vue.js', version: window.Vue.version || 'unknown' };
|
||
return { name: 'Unknown', version: 'unknown' };
|
||
};
|
||
|
||
/**
|
||
* Build layout tree recursively
|
||
*/
|
||
const buildLayoutTree = (element, depth = 0, maxDepth = 3) => {
|
||
if (depth > maxDepth) return null;
|
||
|
||
const props = extractLayoutProps(element);
|
||
const bounds = getBounds(element);
|
||
const pattern = identifyPattern(props);
|
||
|
||
// Get semantic role
|
||
const tagName = element.tagName.toLowerCase();
|
||
const classes = Array.from(element.classList).slice(0, 3); // Max 3 classes
|
||
const role = element.getAttribute('role');
|
||
|
||
// Build node
|
||
const node = {
|
||
tag: tagName,
|
||
classes: classes,
|
||
role: role,
|
||
pattern: pattern,
|
||
bounds: bounds,
|
||
layout: {
|
||
display: props.display,
|
||
position: props.position
|
||
}
|
||
};
|
||
|
||
// Add flex/grid specific properties
|
||
if (props.display === 'flex' || props.display === 'inline-flex') {
|
||
node.layout.flexDirection = props.flexDirection;
|
||
node.layout.justifyContent = props.justifyContent;
|
||
node.layout.alignItems = props.alignItems;
|
||
node.layout.gap = props.gap;
|
||
}
|
||
|
||
if (props.display === 'grid') {
|
||
node.layout.gridTemplateColumns = props.gridTemplateColumns;
|
||
node.layout.gridTemplateRows = props.gridTemplateRows;
|
||
node.layout.gap = props.gap;
|
||
}
|
||
|
||
// Process children for container elements
|
||
if (props.display === 'flex' || props.display === 'grid' || props.display === 'block') {
|
||
const children = Array.from(element.children);
|
||
if (children.length > 0 && children.length < 50) { // Limit to 50 children
|
||
node.children = children
|
||
.map(child => buildLayoutTree(child, depth + 1, maxDepth))
|
||
.filter(child => child !== null);
|
||
}
|
||
}
|
||
|
||
return node;
|
||
};
|
||
|
||
/**
|
||
* Find main layout containers with multi-strategy approach
|
||
*/
|
||
const findMainContainers = () => {
|
||
const containers = [];
|
||
const found = new Set();
|
||
|
||
// Strategy 1: Strict selectors (body direct children)
|
||
const strictSelectors = [
|
||
'body > header',
|
||
'body > nav',
|
||
'body > main',
|
||
'body > footer'
|
||
];
|
||
|
||
// Strategy 2: Relaxed selectors (any level)
|
||
const relaxedSelectors = [
|
||
'header',
|
||
'nav',
|
||
'main',
|
||
'footer',
|
||
'[role="banner"]',
|
||
'[role="navigation"]',
|
||
'[role="main"]',
|
||
'[role="contentinfo"]'
|
||
];
|
||
|
||
// Strategy 3: Common class-based main content selectors
|
||
const commonClassSelectors = [
|
||
'.main',
|
||
'.content',
|
||
'.main-content',
|
||
'.page-content',
|
||
'.container.main',
|
||
'.wrapper > .main',
|
||
'div[class*="main-wrapper"]',
|
||
'div[class*="content-wrapper"]'
|
||
];
|
||
|
||
// Strategy 4: Framework-specific selectors
|
||
const frameworkSelectors = [
|
||
'#__nuxt header', '#__nuxt .main', '#__nuxt main', '#__nuxt footer',
|
||
'#__next header', '#__next .main', '#__next main', '#__next footer',
|
||
'#app header', '#app .main', '#app main', '#app footer',
|
||
'[data-app] header', '[data-app] .main', '[data-app] main', '[data-app] footer'
|
||
];
|
||
|
||
// Try all strategies
|
||
const allSelectors = [...strictSelectors, ...relaxedSelectors, ...commonClassSelectors, ...frameworkSelectors];
|
||
|
||
allSelectors.forEach(selector => {
|
||
try {
|
||
const elements = document.querySelectorAll(selector);
|
||
elements.forEach(element => {
|
||
// Avoid duplicates and invisible elements
|
||
if (!found.has(element) && element.offsetParent !== null) {
|
||
found.add(element);
|
||
const tree = buildLayoutTree(element, 0, 3);
|
||
if (tree && tree.bounds.width > 0 && tree.bounds.height > 0) {
|
||
containers.push(tree);
|
||
}
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.warn(`Selector failed: ${selector}`, e);
|
||
}
|
||
});
|
||
|
||
// Fallback: If no containers found, use body's direct children
|
||
if (containers.length === 0) {
|
||
Array.from(document.body.children).forEach(child => {
|
||
if (child.offsetParent !== null && !found.has(child)) {
|
||
const tree = buildLayoutTree(child, 0, 2);
|
||
if (tree && tree.bounds.width > 100 && tree.bounds.height > 100) {
|
||
containers.push(tree);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
return containers;
|
||
};
|
||
|
||
/**
|
||
* Progressive exploration: Discover main containers when standard selectors fail
|
||
* Analyzes large visible containers and suggests class name patterns
|
||
*/
|
||
const exploreMainContainers = () => {
|
||
const candidates = [];
|
||
const minWidth = 500;
|
||
const minHeight = 300;
|
||
|
||
// Find all large visible divs
|
||
const allDivs = document.querySelectorAll('div');
|
||
allDivs.forEach(div => {
|
||
const rect = div.getBoundingClientRect();
|
||
const style = window.getComputedStyle(div);
|
||
|
||
// Filter: large size, visible, not header/footer
|
||
if (rect.width >= minWidth &&
|
||
rect.height >= minHeight &&
|
||
div.offsetParent !== null &&
|
||
!div.closest('header') &&
|
||
!div.closest('footer')) {
|
||
|
||
const classes = Array.from(div.classList);
|
||
const area = rect.width * rect.height;
|
||
|
||
candidates.push({
|
||
element: div,
|
||
classes: classes,
|
||
area: area,
|
||
bounds: {
|
||
width: Math.round(rect.width),
|
||
height: Math.round(rect.height)
|
||
},
|
||
display: style.display,
|
||
depth: getElementDepth(div)
|
||
});
|
||
}
|
||
});
|
||
|
||
// Sort by area (largest first) and take top candidates
|
||
candidates.sort((a, b) => b.area - a.area);
|
||
|
||
// Extract unique class patterns from top candidates
|
||
const classPatterns = new Set();
|
||
candidates.slice(0, 20).forEach(c => {
|
||
c.classes.forEach(cls => {
|
||
// Identify potential main content class patterns
|
||
if (cls.match(/main|content|container|wrapper|page|body|layout|app/i)) {
|
||
classPatterns.add(cls);
|
||
}
|
||
});
|
||
});
|
||
|
||
return {
|
||
candidates: candidates.slice(0, 10).map(c => ({
|
||
classes: c.classes,
|
||
bounds: c.bounds,
|
||
display: c.display,
|
||
depth: c.depth
|
||
})),
|
||
suggestedSelectors: Array.from(classPatterns).map(cls => `.${cls}`)
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Get element depth in DOM tree
|
||
*/
|
||
const getElementDepth = (element) => {
|
||
let depth = 0;
|
||
let current = element;
|
||
while (current.parentElement) {
|
||
depth++;
|
||
current = current.parentElement;
|
||
}
|
||
return depth;
|
||
};
|
||
|
||
/**
|
||
* Analyze layout patterns
|
||
*/
|
||
const analyzePatterns = (containers) => {
|
||
const patterns = {
|
||
flexColumn: 0,
|
||
flexRow: 0,
|
||
grid: 0,
|
||
sticky: 0,
|
||
fixed: 0
|
||
};
|
||
|
||
const analyze = (node) => {
|
||
if (!node) return;
|
||
|
||
if (node.pattern === 'flex-column') patterns.flexColumn++;
|
||
if (node.pattern === 'flex-row') patterns.flexRow++;
|
||
if (node.pattern && node.pattern.startsWith('grid')) patterns.grid++;
|
||
if (node.layout.position === 'sticky') patterns.sticky++;
|
||
if (node.layout.position === 'fixed') patterns.fixed++;
|
||
|
||
if (node.children) {
|
||
node.children.forEach(analyze);
|
||
}
|
||
};
|
||
|
||
containers.forEach(analyze);
|
||
return patterns;
|
||
};
|
||
|
||
/**
|
||
* Main extraction function with progressive exploration
|
||
*/
|
||
const extractLayout = () => {
|
||
const framework = detectFramework();
|
||
const containers = findMainContainers();
|
||
const patterns = analyzePatterns(containers);
|
||
|
||
// Progressive exploration: if too few containers found, explore and suggest
|
||
let exploration = null;
|
||
const minExpectedContainers = 3; // At least header, main, footer
|
||
|
||
if (containers.length < minExpectedContainers) {
|
||
exploration = exploreMainContainers();
|
||
|
||
// Add warning message
|
||
exploration.warning = `Only ${containers.length} containers found. Consider adding these selectors to the script:`;
|
||
exploration.recommendation = exploration.suggestedSelectors.join(', ');
|
||
}
|
||
|
||
const result = {
|
||
metadata: {
|
||
extractedAt: new Date().toISOString(),
|
||
url: window.location.href,
|
||
framework: framework,
|
||
method: 'layout-structure-enhanced',
|
||
version: '2.2.0'
|
||
},
|
||
statistics: {
|
||
totalContainers: containers.length,
|
||
patterns: patterns
|
||
},
|
||
structure: containers
|
||
};
|
||
|
||
// Add exploration results if triggered
|
||
if (exploration) {
|
||
result.exploration = {
|
||
triggered: true,
|
||
reason: 'Insufficient containers found with standard selectors',
|
||
discoveredCandidates: exploration.candidates,
|
||
suggestedSelectors: exploration.suggestedSelectors,
|
||
warning: exploration.warning,
|
||
recommendation: exploration.recommendation
|
||
};
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
// Execute and return results
|
||
return extractLayout();
|
||
})();
|