design-clone 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -34
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -171
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +9 -86
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +7 -14
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +2 -22
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +533 -286
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +14 -17
- package/src/ai/prompts/design-tokens/basic.md +80 -0
- package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
- package/src/ai/prompts/design-tokens/section.md +48 -0
- package/src/ai/prompts/design-tokens/with-css.md +87 -0
- package/src/ai/prompts/structure-analysis/basic.md +55 -0
- package/src/ai/prompts/structure-analysis/with-context.md +59 -0
- package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
- package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
- package/src/ai/prompts/ux-audit/aggregation.md +42 -0
- package/src/ai/prompts/ux-audit/desktop.md +92 -0
- package/src/ai/prompts/ux-audit/mobile.md +93 -0
- package/src/ai/prompts/ux-audit/tablet.md +92 -0
- package/src/core/animation/animation-extractor-ast.js +183 -0
- package/src/core/animation/animation-extractor-output.js +152 -0
- package/src/core/animation/animation-extractor.js +178 -0
- package/src/core/animation/state-capture-detection.js +200 -0
- package/src/core/animation/state-capture.js +193 -0
- package/src/core/capture/browser-context-pool.js +96 -0
- package/src/core/capture/multi-page-screenshot-page.js +110 -0
- package/src/core/capture/multi-page-screenshot.js +208 -0
- package/src/core/capture/screenshot-extraction.js +186 -0
- package/src/core/capture/screenshot-helpers.js +175 -0
- package/src/core/capture/screenshot-orchestrator.js +174 -0
- package/src/core/capture/screenshot-viewport.js +93 -0
- package/src/core/capture/screenshot.js +192 -0
- package/src/core/content/content-counter-dom.js +191 -0
- package/src/core/content/content-counter.js +76 -0
- package/src/core/css/breakpoint-detector.js +66 -0
- package/src/core/css/chromium-defaults.json +23 -0
- package/src/core/css/computed-style-extractor.js +102 -0
- package/src/core/css/css-chunker.js +103 -0
- package/src/core/css/filter-css-dead-code.js +120 -0
- package/src/core/css/filter-css-html-analyzer.js +110 -0
- package/src/core/css/filter-css-selector-matcher.js +172 -0
- package/src/core/css/filter-css.js +206 -0
- package/src/core/css/merge-css-atrule-processor.js +158 -0
- package/src/core/css/merge-css-file-io.js +68 -0
- package/src/core/css/merge-css.js +148 -0
- package/src/core/detection/framework-detector-routing.js +68 -0
- package/src/core/detection/framework-detector-signals.js +65 -0
- package/src/core/detection/framework-detector.js +198 -0
- package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
- package/src/core/dimension/dimension-extractor.js +317 -0
- package/src/core/dimension/dimension-output-ai-summary.js +111 -0
- package/src/core/dimension/dimension-output.js +173 -0
- package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
- package/src/core/dimension/dom-tree-analyzer.js +191 -0
- package/src/core/discovery/app-state-snapshot-capture.js +195 -0
- package/src/core/discovery/app-state-snapshot-utils.js +178 -0
- package/src/core/discovery/app-state-snapshot.js +131 -0
- package/src/core/discovery/discover-pages-routes.js +84 -0
- package/src/core/discovery/discover-pages-utils.js +177 -0
- package/src/core/discovery/discover-pages.js +191 -0
- package/src/core/html/html-extractor-inline-styler.js +70 -0
- package/src/core/html/html-extractor.js +147 -0
- package/src/core/html/semantic-enhancer-mappings.js +200 -0
- package/src/core/html/semantic-enhancer-page.js +148 -0
- package/src/core/html/semantic-enhancer.js +135 -0
- package/src/core/links/rewrite-links-css-rewriter.js +53 -0
- package/src/core/links/rewrite-links.js +173 -0
- package/src/core/media/asset-validator.js +118 -0
- package/src/core/media/extract-assets-downloader.js +187 -0
- package/src/core/media/extract-assets-page-scraper.js +115 -0
- package/src/core/media/extract-assets.js +159 -0
- package/src/core/media/video-capture-convert.js +200 -0
- package/src/core/media/video-capture.js +201 -0
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
- package/src/core/section/section-detector-strategies.js +139 -0
- package/src/core/section/section-detector-utils.js +100 -0
- package/src/core/section/section-detector.js +88 -0
- package/src/core/tests/test-section-cropper.js +2 -2
- package/src/core/tests/test-section-detector.js +2 -2
- package/src/post-process/enhance-assets.js +29 -4
- package/src/post-process/fetch-images-unsplash-client.js +123 -0
- package/src/post-process/fetch-images.js +60 -263
- package/src/post-process/inject-gosnap.js +88 -0
- package/src/post-process/inject-icons-svg-replacer.js +76 -0
- package/src/post-process/inject-icons.js +47 -200
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +29 -118
- package/src/route-discoverers/index.js +1 -1
- package/src/shared/config.js +38 -0
- package/src/shared/error-codes.js +31 -0
- package/src/shared/viewports.js +46 -0
- package/src/utils/browser.js +0 -7
- package/src/utils/helpers.js +4 -0
- package/src/utils/log.js +12 -0
- package/src/utils/playwright-loader.js +76 -0
- package/src/utils/playwright.js +3 -69
- package/src/utils/progress.js +32 -0
- package/src/verification/generate-audit-report-css-fixes.js +52 -0
- package/src/verification/generate-audit-report-sections.js +158 -0
- package/src/verification/generate-audit-report.js +5 -281
- package/src/verification/quality-scorer.js +92 -0
- package/src/verification/verify-footer-checks.js +103 -0
- package/src/verification/verify-footer-helpers.js +178 -0
- package/src/verification/verify-footer.js +23 -381
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +23 -365
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +13 -259
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +17 -285
- package/src/verification/verify-slider-checks.js +115 -0
- package/src/verification/verify-slider-constants.js +65 -0
- package/src/verification/verify-slider-helpers.js +164 -0
- package/src/verification/verify-slider.js +23 -414
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -316
- package/docs/design-clone-architecture.md +0 -492
- package/docs/pixel-perfect.md +0 -117
- package/docs/project-roadmap.md +0 -382
- package/docs/troubleshooting.md +0 -170
- package/requirements.txt +0 -5
- package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
- package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
- package/src/ai/analyze-structure.py +0 -375
- package/src/ai/extract-design-tokens.py +0 -782
- package/src/ai/prompts/__init__.py +0 -2
- package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
- package/src/ai/prompts/design_tokens.py +0 -316
- package/src/ai/prompts/structure_analysis.py +0 -592
- package/src/ai/prompts/ux_audit.py +0 -198
- package/src/ai/ux-audit.js +0 -596
- package/src/core/animation-extractor.js +0 -526
- package/src/core/app-state-snapshot.js +0 -511
- package/src/core/content-counter.js +0 -342
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -438
- package/src/core/dimension-output.js +0 -305
- package/src/core/discover-pages.js +0 -542
- package/src/core/dom-tree-analyzer.js +0 -298
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/framework-detector.js +0 -538
- package/src/core/html-extractor.js +0 -212
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -380
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -701
- package/src/core/section-detector.js +0 -386
- package/src/core/semantic-enhancer.js +0 -492
- package/src/core/state-capture.js +0 -598
- package/src/core/video-capture.js +0 -546
- package/src/utils/__init__.py +0 -16
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/env.py +0 -134
- /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
- /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
- /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
|
+
import { validateBounds, sanitizeName } from './section-cropper-helpers.js';
|
|
14
15
|
|
|
15
16
|
// Try to import Sharp
|
|
16
17
|
let sharp = null;
|
|
@@ -21,7 +22,7 @@ try {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// Default configuration
|
|
24
|
-
const DEFAULT_OPTIONS = {
|
|
25
|
+
export const DEFAULT_OPTIONS = {
|
|
25
26
|
minHeight: 100, // Skip sections smaller than this
|
|
26
27
|
quality: 90, // PNG quality
|
|
27
28
|
compressionLevel: 6, // PNG compression (0-9)
|
|
@@ -34,21 +35,18 @@ const DEFAULT_OPTIONS = {
|
|
|
34
35
|
* @param {Array} sections - Array of section objects with bounds
|
|
35
36
|
* @param {string} outputDir - Base output directory
|
|
36
37
|
* @param {Object} options - Configuration options
|
|
37
|
-
* @returns {Promise<Array
|
|
38
|
+
* @returns {Promise<{sections: Array, skipped: Array, summary: string, directory: string}>}
|
|
38
39
|
*/
|
|
39
40
|
export async function cropSections(screenshotPath, sections, outputDir, options = {}) {
|
|
40
41
|
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
41
42
|
|
|
42
|
-
// Check Sharp availability
|
|
43
43
|
if (!sharp) {
|
|
44
44
|
throw new Error('Sharp is not installed. Run: npm install sharp');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// Create sections directory
|
|
48
47
|
const sectionsDir = path.join(outputDir, 'sections');
|
|
49
48
|
await fs.mkdir(sectionsDir, { recursive: true });
|
|
50
49
|
|
|
51
|
-
// Get source image metadata
|
|
52
50
|
const metadata = await sharp(screenshotPath).metadata();
|
|
53
51
|
const imageWidth = metadata.width;
|
|
54
52
|
const imageHeight = metadata.height;
|
|
@@ -57,47 +55,26 @@ export async function cropSections(screenshotPath, sections, outputDir, options
|
|
|
57
55
|
const skipped = [];
|
|
58
56
|
|
|
59
57
|
for (const section of sections) {
|
|
60
|
-
// Validate and clamp bounds
|
|
61
58
|
const bounds = validateBounds(section.bounds, imageWidth, imageHeight);
|
|
62
59
|
|
|
63
|
-
// Skip tiny sections
|
|
64
60
|
if (bounds.height < config.minHeight) {
|
|
65
|
-
skipped.push({
|
|
66
|
-
index: section.index,
|
|
67
|
-
name: section.name,
|
|
68
|
-
reason: `Height ${bounds.height}px < ${config.minHeight}px minimum`
|
|
69
|
-
});
|
|
61
|
+
skipped.push({ index: section.index, name: section.name, reason: `Height ${bounds.height}px < ${config.minHeight}px minimum` });
|
|
70
62
|
continue;
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
// Skip zero-dimension sections
|
|
74
65
|
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
75
|
-
skipped.push({
|
|
76
|
-
index: section.index,
|
|
77
|
-
name: section.name,
|
|
78
|
-
reason: 'Zero or negative dimensions'
|
|
79
|
-
});
|
|
66
|
+
skipped.push({ index: section.index, name: section.name, reason: 'Zero or negative dimensions' });
|
|
80
67
|
continue;
|
|
81
68
|
}
|
|
82
69
|
|
|
83
|
-
// Generate output filename
|
|
84
70
|
const safeName = sanitizeName(section.name);
|
|
85
71
|
const filename = `section-${section.index}-${safeName}.png`;
|
|
86
72
|
const outputPath = path.join(sectionsDir, filename);
|
|
87
73
|
|
|
88
74
|
try {
|
|
89
|
-
// Crop and save
|
|
90
75
|
await sharp(screenshotPath)
|
|
91
|
-
.extract({
|
|
92
|
-
|
|
93
|
-
top: bounds.top,
|
|
94
|
-
width: bounds.width,
|
|
95
|
-
height: bounds.height
|
|
96
|
-
})
|
|
97
|
-
.png({
|
|
98
|
-
quality: config.quality,
|
|
99
|
-
compressionLevel: config.compressionLevel
|
|
100
|
-
})
|
|
76
|
+
.extract({ left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height })
|
|
77
|
+
.png({ quality: config.quality, compressionLevel: config.compressionLevel })
|
|
101
78
|
.toFile(outputPath);
|
|
102
79
|
|
|
103
80
|
results.push({
|
|
@@ -106,25 +83,15 @@ export async function cropSections(screenshotPath, sections, outputDir, options
|
|
|
106
83
|
filename,
|
|
107
84
|
path: outputPath,
|
|
108
85
|
relativePath: path.join('sections', filename),
|
|
109
|
-
bounds: {
|
|
110
|
-
x: bounds.left,
|
|
111
|
-
y: bounds.top,
|
|
112
|
-
width: bounds.width,
|
|
113
|
-
height: bounds.height
|
|
114
|
-
},
|
|
86
|
+
bounds: { x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height },
|
|
115
87
|
role: section.role || 'unknown',
|
|
116
88
|
selector: section.selector || null
|
|
117
89
|
});
|
|
118
90
|
} catch (err) {
|
|
119
|
-
skipped.push({
|
|
120
|
-
index: section.index,
|
|
121
|
-
name: section.name,
|
|
122
|
-
reason: `Crop error: ${err.message}`
|
|
123
|
-
});
|
|
91
|
+
skipped.push({ index: section.index, name: section.name, reason: `Crop error: ${err.message}` });
|
|
124
92
|
}
|
|
125
93
|
}
|
|
126
94
|
|
|
127
|
-
// Write summary JSON
|
|
128
95
|
const summary = {
|
|
129
96
|
source: path.basename(screenshotPath),
|
|
130
97
|
sourceWidth: imageWidth,
|
|
@@ -139,49 +106,7 @@ export async function cropSections(screenshotPath, sections, outputDir, options
|
|
|
139
106
|
const summaryPath = path.join(sectionsDir, 'sections.json');
|
|
140
107
|
await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2));
|
|
141
108
|
|
|
142
|
-
return {
|
|
143
|
-
sections: results,
|
|
144
|
-
skipped,
|
|
145
|
-
summary: summaryPath,
|
|
146
|
-
directory: sectionsDir
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Validate and clamp bounds to image dimensions
|
|
152
|
-
* @param {Object} bounds - Section bounds {x, y, width, height}
|
|
153
|
-
* @param {number} imageWidth - Source image width
|
|
154
|
-
* @param {number} imageHeight - Source image height
|
|
155
|
-
* @returns {Object} Validated bounds {left, top, width, height}
|
|
156
|
-
*/
|
|
157
|
-
function validateBounds(bounds, imageWidth, imageHeight) {
|
|
158
|
-
// Clamp starting position
|
|
159
|
-
const left = Math.max(0, Math.round(bounds.x));
|
|
160
|
-
const top = Math.max(0, Math.round(bounds.y));
|
|
161
|
-
|
|
162
|
-
// Calculate max possible dimensions
|
|
163
|
-
const maxWidth = imageWidth - left;
|
|
164
|
-
const maxHeight = imageHeight - top;
|
|
165
|
-
|
|
166
|
-
// Clamp dimensions
|
|
167
|
-
const width = Math.min(Math.round(bounds.width), maxWidth);
|
|
168
|
-
const height = Math.min(Math.round(bounds.height), maxHeight);
|
|
169
|
-
|
|
170
|
-
return { left, top, width, height };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Sanitize section name for filename
|
|
175
|
-
* @param {string} name - Section name
|
|
176
|
-
* @returns {string} Safe filename
|
|
177
|
-
*/
|
|
178
|
-
function sanitizeName(name) {
|
|
179
|
-
return name
|
|
180
|
-
.toLowerCase()
|
|
181
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
182
|
-
.replace(/-+/g, '-')
|
|
183
|
-
.replace(/^-|-$/g, '')
|
|
184
|
-
.substring(0, 50) || 'unnamed';
|
|
109
|
+
return { sections: results, skipped, summary: summaryPath, directory: sectionsDir };
|
|
185
110
|
}
|
|
186
111
|
|
|
187
112
|
/**
|
|
@@ -195,7 +120,7 @@ export function isSharpAvailable() {
|
|
|
195
120
|
/**
|
|
196
121
|
* Get cropper summary for logging
|
|
197
122
|
* @param {Object} result - Result from cropSections
|
|
198
|
-
* @returns {Object}
|
|
123
|
+
* @returns {Object}
|
|
199
124
|
*/
|
|
200
125
|
export function getCropperSummary(result) {
|
|
201
126
|
return {
|
|
@@ -205,5 +130,3 @@ export function getCropperSummary(result) {
|
|
|
205
130
|
totalSize: result.sections.reduce((sum, s) => sum + (s.bounds.width * s.bounds.height), 0)
|
|
206
131
|
};
|
|
207
132
|
}
|
|
208
|
-
|
|
209
|
-
export { DEFAULT_OPTIONS };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section detection strategies for page analysis.
|
|
3
|
+
*
|
|
4
|
+
* Four progressive strategies: semantic HTML, class patterns,
|
|
5
|
+
* large direct children, viewport chunking fallback.
|
|
6
|
+
* Used by section-detector.js (main orchestrator).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SECTION_CLASS_PATTERNS } from './section-detector-utils.js';
|
|
10
|
+
|
|
11
|
+
/** @param {Object} rect @param {number} scrollY @returns {Object} */
|
|
12
|
+
function toBounds(rect, scrollY) {
|
|
13
|
+
return { x: Math.round(rect.x), y: Math.round(rect.y + scrollY), width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find semantic HTML sections (header, main, section, footer)
|
|
18
|
+
* @param {import('playwright').Page} page
|
|
19
|
+
* @param {Object} pageDimensions
|
|
20
|
+
* @param {Object} config
|
|
21
|
+
* @returns {Promise<Array>}
|
|
22
|
+
*/
|
|
23
|
+
export async function findSemanticSections(page, pageDimensions, config) {
|
|
24
|
+
return await page.evaluate(({ minHeight }) => {
|
|
25
|
+
const sections = [];
|
|
26
|
+
const processed = new Set();
|
|
27
|
+
const selectors = [
|
|
28
|
+
'header:not(header header)', 'main > section', 'main > article',
|
|
29
|
+
'body > section', 'body > article', '[data-section]', 'footer:not(footer footer)'
|
|
30
|
+
];
|
|
31
|
+
for (const selector of selectors) {
|
|
32
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
33
|
+
if (processed.has(el)) continue;
|
|
34
|
+
const rect = el.getBoundingClientRect();
|
|
35
|
+
if (rect.height < minHeight) continue;
|
|
36
|
+
let name = el.tagName.toLowerCase();
|
|
37
|
+
if (el.hasAttribute('data-section')) name = el.getAttribute('data-section');
|
|
38
|
+
else if (el.id) name = el.id;
|
|
39
|
+
else if (el.className) {
|
|
40
|
+
const m = el.className.toString().toLowerCase()
|
|
41
|
+
.match(/\b(hero|about|services|features|contact|footer|header|nav|cta|testimonials|pricing|faq|team|blog|news)\b/);
|
|
42
|
+
if (m) name = m[1];
|
|
43
|
+
}
|
|
44
|
+
sections.push({ name, role: el.tagName.toLowerCase(),
|
|
45
|
+
selector: el.id ? `#${el.id}` : el.tagName.toLowerCase(),
|
|
46
|
+
bounds: { x: Math.round(rect.x), y: Math.round(rect.y + window.scrollY), width: Math.round(rect.width), height: Math.round(rect.height) } });
|
|
47
|
+
processed.add(el);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return sections;
|
|
51
|
+
}, { minHeight: config.minSectionHeight });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find sections by class pattern matching
|
|
56
|
+
* @param {import('playwright').Page} page
|
|
57
|
+
* @param {Object} pageDimensions
|
|
58
|
+
* @param {Object} config
|
|
59
|
+
* @returns {Promise<Array>}
|
|
60
|
+
*/
|
|
61
|
+
export async function findClassPatternSections(page, pageDimensions, config) {
|
|
62
|
+
return await page.evaluate(({ patterns, minHeight }) => {
|
|
63
|
+
const sections = [];
|
|
64
|
+
const processed = new Set();
|
|
65
|
+
const elements = document.querySelectorAll(patterns.map(p => `[class*="${p}"]`).join(', '));
|
|
66
|
+
for (const el of elements) {
|
|
67
|
+
const parent = el.parentElement;
|
|
68
|
+
if (!parent || (parent.tagName !== 'BODY' && parent.tagName !== 'MAIN')) continue;
|
|
69
|
+
if (processed.has(el)) continue;
|
|
70
|
+
const rect = el.getBoundingClientRect();
|
|
71
|
+
if (rect.height < minHeight) continue;
|
|
72
|
+
const cls = el.className.toString().toLowerCase();
|
|
73
|
+
let name = patterns.find(p => cls.includes(p)) || 'section';
|
|
74
|
+
sections.push({ name, role: 'class-pattern',
|
|
75
|
+
selector: el.id ? `#${el.id}` : `.${el.className.toString().split(' ')[0]}`,
|
|
76
|
+
bounds: { x: Math.round(rect.x), y: Math.round(rect.y + window.scrollY), width: Math.round(rect.width), height: Math.round(rect.height) } });
|
|
77
|
+
processed.add(el);
|
|
78
|
+
}
|
|
79
|
+
return sections;
|
|
80
|
+
}, { patterns: SECTION_CLASS_PATTERNS, minHeight: config.minSectionHeight });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find large direct children of main/body as sections
|
|
85
|
+
* @param {import('playwright').Page} page
|
|
86
|
+
* @param {Object} pageDimensions
|
|
87
|
+
* @param {Object} config
|
|
88
|
+
* @returns {Promise<Array>}
|
|
89
|
+
*/
|
|
90
|
+
export async function findLargeChildSections(page, pageDimensions, config) {
|
|
91
|
+
return await page.evaluate(({ minHeight }) => {
|
|
92
|
+
const sections = [];
|
|
93
|
+
const skip = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'LINK', 'META'];
|
|
94
|
+
const skipSemantic = ['HEADER', 'FOOTER', 'SECTION', 'ARTICLE'];
|
|
95
|
+
const generic = ['sd', 'container', 'wrapper', 'div', 'block', 'row', 'col', 'section'];
|
|
96
|
+
for (const container of [document.querySelector('main'), document.body].filter(Boolean)) {
|
|
97
|
+
for (const child of container.children) {
|
|
98
|
+
if (skip.includes(child.tagName) || skipSemantic.includes(child.tagName)) continue;
|
|
99
|
+
const rect = child.getBoundingClientRect();
|
|
100
|
+
const absoluteY = rect.y + window.scrollY;
|
|
101
|
+
if (rect.height < Math.max(300, window.innerHeight * 0.2)) continue;
|
|
102
|
+
let name = child.id || '';
|
|
103
|
+
if (!name && child.className) {
|
|
104
|
+
const first = child.className.toString().split(' ')[0].toLowerCase();
|
|
105
|
+
if (!generic.includes(first)) name = first;
|
|
106
|
+
}
|
|
107
|
+
if (!name) {
|
|
108
|
+
const r = absoluteY / (document.body.scrollHeight || 1);
|
|
109
|
+
name = r < 0.15 ? 'top-section' : r < 0.35 ? 'upper-content' : r < 0.55 ? 'middle-content' : r < 0.75 ? 'lower-content' : 'bottom-section';
|
|
110
|
+
name = `${name}-${sections.length}`;
|
|
111
|
+
}
|
|
112
|
+
sections.push({ name: name.toLowerCase().replace(/[^a-z0-9-]/g, '-'), role: 'large-block',
|
|
113
|
+
selector: child.id ? `#${child.id}` : child.tagName.toLowerCase(),
|
|
114
|
+
bounds: { x: Math.round(rect.x), y: Math.round(absoluteY), width: Math.round(rect.width), height: Math.round(rect.height) } });
|
|
115
|
+
}
|
|
116
|
+
if (sections.length > 0 && container.tagName === 'MAIN') break;
|
|
117
|
+
}
|
|
118
|
+
return sections;
|
|
119
|
+
}, { minHeight: config.minSectionHeight });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generate viewport chunks as fallback when no sections found.
|
|
124
|
+
* @param {Object} pageDimensions - { width, height }
|
|
125
|
+
* @param {Object} config - { viewportHeight, overlapRatio }
|
|
126
|
+
* @returns {Array}
|
|
127
|
+
*/
|
|
128
|
+
export function generateViewportChunks(pageDimensions, config) {
|
|
129
|
+
const { width, height } = pageDimensions;
|
|
130
|
+
const step = config.viewportHeight - Math.round(config.viewportHeight * config.overlapRatio);
|
|
131
|
+
const sections = [];
|
|
132
|
+
let y = 0;
|
|
133
|
+
for (let i = 0; y < height && i < 50; i++) {
|
|
134
|
+
sections.push({ name: `viewport-${i}`, role: 'viewport-chunk', selector: null,
|
|
135
|
+
bounds: { x: 0, y, width, height: Math.min(config.viewportHeight, height - y) } });
|
|
136
|
+
y += step;
|
|
137
|
+
}
|
|
138
|
+
return sections;
|
|
139
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility helpers and configuration for section detection.
|
|
3
|
+
*
|
|
4
|
+
* Contains SECTION_CLASS_PATTERNS, DEFAULT_OPTIONS, mergeSections (dedup
|
|
5
|
+
* by Y-overlap), applyPadding (clamp bounds to page), and getSectionSummary.
|
|
6
|
+
* Used by section-detector.js and section-detector-strategies.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/** Section class patterns to match against element class names */
|
|
14
|
+
export const SECTION_CLASS_PATTERNS = [
|
|
15
|
+
'hero', 'banner', 'header', 'navigation', 'nav',
|
|
16
|
+
'services', 'features', 'about', 'team', 'portfolio',
|
|
17
|
+
'testimonials', 'reviews', 'pricing', 'plans',
|
|
18
|
+
'faq', 'questions', 'blog', 'news', 'articles',
|
|
19
|
+
'contact', 'cta', 'call-to-action', 'newsletter',
|
|
20
|
+
'footer', 'partners', 'clients', 'gallery', 'showcase'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Default configuration for section detection */
|
|
24
|
+
export const DEFAULT_OPTIONS = {
|
|
25
|
+
minSections: 3,
|
|
26
|
+
maxSections: 20,
|
|
27
|
+
padding: 40,
|
|
28
|
+
fallbackToViewport: true,
|
|
29
|
+
viewportHeight: 900,
|
|
30
|
+
minSectionHeight: 150,
|
|
31
|
+
overlapRatio: 0.1 // 10% overlap for viewport fallback
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Helpers
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merge sections, removing duplicates based on Y overlap.
|
|
40
|
+
* A new section is skipped if it overlaps >50% with any existing section.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array} existing - Already-accepted sections
|
|
43
|
+
* @param {Array} newSections - Candidates to merge in
|
|
44
|
+
* @returns {Array} Merged deduplicated sections
|
|
45
|
+
*/
|
|
46
|
+
export function mergeSections(existing, newSections) {
|
|
47
|
+
const result = [...existing];
|
|
48
|
+
|
|
49
|
+
for (const section of newSections) {
|
|
50
|
+
const overlaps = result.some(s => {
|
|
51
|
+
const yOverlap = Math.max(0,
|
|
52
|
+
Math.min(s.bounds.y + s.bounds.height, section.bounds.y + section.bounds.height) -
|
|
53
|
+
Math.max(s.bounds.y, section.bounds.y)
|
|
54
|
+
);
|
|
55
|
+
const minHeight = Math.min(s.bounds.height, section.bounds.height);
|
|
56
|
+
return yOverlap > minHeight * 0.5; // >50% overlap
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!overlaps) {
|
|
60
|
+
result.push(section);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply padding to bounds, clamping to page dimensions.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} bounds - { x, y, width, height }
|
|
71
|
+
* @param {number} padding - Pixels to expand on each side
|
|
72
|
+
* @param {Object} pageDimensions - { width, height }
|
|
73
|
+
* @returns {Object} Padded and clamped bounds
|
|
74
|
+
*/
|
|
75
|
+
export function applyPadding(bounds, padding, pageDimensions) {
|
|
76
|
+
return {
|
|
77
|
+
x: Math.max(0, bounds.x - padding),
|
|
78
|
+
y: Math.max(0, bounds.y - padding),
|
|
79
|
+
width: Math.min(pageDimensions.width, bounds.width + padding * 2),
|
|
80
|
+
height: Math.min(
|
|
81
|
+
pageDimensions.height - Math.max(0, bounds.y - padding),
|
|
82
|
+
bounds.height + padding * 2
|
|
83
|
+
)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get section summary for logging / reporting.
|
|
89
|
+
*
|
|
90
|
+
* @param {Array} sections - Detected sections
|
|
91
|
+
* @returns {Object} Summary with count, names, totalHeight, hasViewportFallback
|
|
92
|
+
*/
|
|
93
|
+
export function getSectionSummary(sections) {
|
|
94
|
+
return {
|
|
95
|
+
count: sections.length,
|
|
96
|
+
names: sections.map(s => s.name),
|
|
97
|
+
totalHeight: sections.reduce((sum, s) => sum + s.bounds.height, 0),
|
|
98
|
+
hasViewportFallback: sections.some(s => s.role === 'viewport-chunk')
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section Detector
|
|
3
|
+
*
|
|
4
|
+
* Detect semantic page sections from DOM hierarchy for section-based
|
|
5
|
+
* screenshot analysis. Returns bounding boxes for cropping.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { detectSections } from './section-detector.js';
|
|
9
|
+
* const sections = await detectSections(page, { padding: 40 });
|
|
10
|
+
*
|
|
11
|
+
* Strategies (in order):
|
|
12
|
+
* 1. Semantic HTML: <header>, <main>, <section>, <footer>
|
|
13
|
+
* 2. data-section attributes
|
|
14
|
+
* 3. Class patterns: hero, services, features, about, contact
|
|
15
|
+
* 4. Large direct children of <main> or <body> (>200px height)
|
|
16
|
+
* 5. Fallback: viewport chunking if <minSections detected
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_OPTIONS,
|
|
21
|
+
SECTION_CLASS_PATTERNS,
|
|
22
|
+
mergeSections,
|
|
23
|
+
applyPadding,
|
|
24
|
+
getSectionSummary
|
|
25
|
+
} from './section-detector-utils.js';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
findSemanticSections,
|
|
29
|
+
findClassPatternSections,
|
|
30
|
+
findLargeChildSections,
|
|
31
|
+
generateViewportChunks
|
|
32
|
+
} from './section-detector-strategies.js';
|
|
33
|
+
|
|
34
|
+
// Re-export for backward compatibility
|
|
35
|
+
export { DEFAULT_OPTIONS, SECTION_CLASS_PATTERNS, getSectionSummary };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect page sections from DOM hierarchy
|
|
39
|
+
* @param {import('playwright').Page} page - Playwright page instance
|
|
40
|
+
* @param {Object} options - Configuration options
|
|
41
|
+
* @returns {Promise<Array>} Array of section objects with bounds
|
|
42
|
+
*/
|
|
43
|
+
export async function detectSections(page, options = {}) {
|
|
44
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
45
|
+
|
|
46
|
+
const pageDimensions = await page.evaluate(() => ({
|
|
47
|
+
width: document.documentElement.clientWidth,
|
|
48
|
+
height: Math.max(
|
|
49
|
+
document.body.scrollHeight,
|
|
50
|
+
document.documentElement.scrollHeight
|
|
51
|
+
)
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Strategy 1: Semantic HTML sections
|
|
55
|
+
let sections = await findSemanticSections(page, pageDimensions, config);
|
|
56
|
+
|
|
57
|
+
// Strategy 2: Class pattern matching
|
|
58
|
+
if (sections.length < config.minSections) {
|
|
59
|
+
const classSections = await findClassPatternSections(page, pageDimensions, config);
|
|
60
|
+
sections = mergeSections(sections, classSections);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Strategy 3: Large direct children
|
|
64
|
+
if (sections.length < config.minSections) {
|
|
65
|
+
const largeSections = await findLargeChildSections(page, pageDimensions, config);
|
|
66
|
+
sections = mergeSections(sections, largeSections);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Strategy 4: Viewport chunking fallback
|
|
70
|
+
if (sections.length < config.minSections && config.fallbackToViewport) {
|
|
71
|
+
sections = generateViewportChunks(pageDimensions, config);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply padding and validate bounds
|
|
75
|
+
sections = sections.map((section, idx) => ({
|
|
76
|
+
...section,
|
|
77
|
+
index: idx,
|
|
78
|
+
bounds: applyPadding(section.bounds, config.padding, pageDimensions)
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Sort by Y position and limit
|
|
82
|
+
sections = sections
|
|
83
|
+
.sort((a, b) => a.bounds.y - b.bounds.y)
|
|
84
|
+
.slice(0, config.maxSections);
|
|
85
|
+
|
|
86
|
+
// Re-index after sort
|
|
87
|
+
return sections.map((section, idx) => ({ ...section, index: idx }));
|
|
88
|
+
}
|
|
@@ -11,8 +11,8 @@ import { chromium } from 'playwright';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
-
import { detectSections, getSectionSummary } from '../section-detector.js';
|
|
15
|
-
import { cropSections, isSharpAvailable, getCropperSummary } from '../section-cropper.js';
|
|
14
|
+
import { detectSections, getSectionSummary } from '../section/section-detector.js';
|
|
15
|
+
import { cropSections, isSharpAvailable, getCropperSummary } from '../section/section-cropper.js';
|
|
16
16
|
|
|
17
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const projectRoot = path.join(__dirname, '../../..');
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Usage: node src/core/test-section-detector.js [url]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { detectSections, getSectionSummary } from '
|
|
8
|
-
import { getBrowser, getPage, closeBrowser } from '
|
|
7
|
+
import { detectSections, getSectionSummary } from '../section/section-detector.js';
|
|
8
|
+
import { getBrowser, getPage, closeBrowser } from '../../utils/browser.js';
|
|
9
9
|
|
|
10
10
|
const url = process.argv[2] || 'https://www.techno-concier.co.jp/';
|
|
11
11
|
|
|
@@ -19,6 +19,7 @@ import fs from 'fs/promises';
|
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import { fetchImages } from './fetch-images.js';
|
|
21
21
|
import { injectIcons } from './inject-icons.js';
|
|
22
|
+
import { injectGosnap } from './inject-gosnap.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Parse command line arguments
|
|
@@ -29,7 +30,8 @@ function parseArgs() {
|
|
|
29
30
|
outputDir: null,
|
|
30
31
|
verbose: false,
|
|
31
32
|
skipImages: false,
|
|
32
|
-
skipIcons: false
|
|
33
|
+
skipIcons: false,
|
|
34
|
+
skipGosnap: false
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -40,6 +42,8 @@ function parseArgs() {
|
|
|
40
42
|
options.skipImages = true;
|
|
41
43
|
} else if (arg === '--skip-icons') {
|
|
42
44
|
options.skipIcons = true;
|
|
45
|
+
} else if (arg === '--skip-gosnap') {
|
|
46
|
+
options.skipGosnap = true;
|
|
43
47
|
} else if (!arg.startsWith('-')) {
|
|
44
48
|
options.outputDir = arg;
|
|
45
49
|
}
|
|
@@ -67,7 +71,8 @@ async function enhanceAssets(outputDir, options = {}) {
|
|
|
67
71
|
const {
|
|
68
72
|
verbose = false,
|
|
69
73
|
skipImages = false,
|
|
70
|
-
skipIcons = false
|
|
74
|
+
skipIcons = false,
|
|
75
|
+
skipGosnap = false
|
|
71
76
|
} = options;
|
|
72
77
|
|
|
73
78
|
const htmlPath = path.join(outputDir, 'index.html');
|
|
@@ -87,7 +92,8 @@ async function enhanceAssets(outputDir, options = {}) {
|
|
|
87
92
|
const results = {
|
|
88
93
|
success: true,
|
|
89
94
|
images: null,
|
|
90
|
-
icons: null
|
|
95
|
+
icons: null,
|
|
96
|
+
gosnap: null
|
|
91
97
|
};
|
|
92
98
|
|
|
93
99
|
// Step 1: Fetch and replace images
|
|
@@ -117,6 +123,23 @@ async function enhanceAssets(outputDir, options = {}) {
|
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
// Step 3: Inject gosnap-widget
|
|
127
|
+
if (!skipGosnap) {
|
|
128
|
+
const pagesDir = path.join(outputDir, 'pages');
|
|
129
|
+
if (await fileExists(pagesDir)) {
|
|
130
|
+
console.log('Injecting gosnap-widget...');
|
|
131
|
+
try {
|
|
132
|
+
results.gosnap = await injectGosnap(pagesDir, verbose);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.warn(` Warning: gosnap injection failed: ${error.message}`);
|
|
135
|
+
results.gosnap = { success: false, error: error.message };
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log(' -> Skipping gosnap (no pages/ directory)');
|
|
139
|
+
results.gosnap = { skipped: true };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
console.log('✅ Asset enhancement complete');
|
|
121
144
|
|
|
122
145
|
return results;
|
|
@@ -132,6 +155,7 @@ if (!args.outputDir) {
|
|
|
132
155
|
console.error(' --verbose, -v Show detailed progress');
|
|
133
156
|
console.error(' --skip-images Skip Unsplash image fetching');
|
|
134
157
|
console.error(' --skip-icons Skip icon injection');
|
|
158
|
+
console.error(' --skip-gosnap Skip gosnap-widget injection');
|
|
135
159
|
console.error('');
|
|
136
160
|
console.error('Environment:');
|
|
137
161
|
console.error(' UNSPLASH_ACCESS_KEY Your Unsplash API key (optional)');
|
|
@@ -141,7 +165,8 @@ if (!args.outputDir) {
|
|
|
141
165
|
enhanceAssets(args.outputDir, {
|
|
142
166
|
verbose: args.verbose,
|
|
143
167
|
skipImages: args.skipImages,
|
|
144
|
-
skipIcons: args.skipIcons
|
|
168
|
+
skipIcons: args.skipIcons,
|
|
169
|
+
skipGosnap: args.skipGosnap
|
|
145
170
|
})
|
|
146
171
|
.then(result => {
|
|
147
172
|
if (args.verbose) {
|