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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Dimension Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extract exact pixel dimensions from page elements using
|
|
5
|
+
* getBoundingClientRect and getComputedStyle.
|
|
6
|
+
*
|
|
7
|
+
* Split into two page.evaluate passes:
|
|
8
|
+
* 1. Containers, typography, buttons, images (this file)
|
|
9
|
+
* 2. Card patterns + grid layouts (second evaluate using card-detector helpers)
|
|
10
|
+
*
|
|
11
|
+
* Card/grid helper sources live in dimension-extractor-card-detector.js.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { calculateSimilarity, detectLayoutType, calculateGap } from './dimension-extractor-card-detector.js';
|
|
15
|
+
|
|
16
|
+
// Section detection thresholds (passed into browser context)
|
|
17
|
+
const HERO_THRESHOLD = 0.25;
|
|
18
|
+
const FOOTER_THRESHOLD = 0.85;
|
|
19
|
+
const SIDEBAR_MAX_WIDTH = 400;
|
|
20
|
+
const MAX_BUTTONS = 10;
|
|
21
|
+
const MAX_IMAGES = 15;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract component dimensions from page.
|
|
25
|
+
* @param {import('playwright').Page} page
|
|
26
|
+
* @param {string} viewportName - 'desktop' | 'tablet' | 'mobile'
|
|
27
|
+
* @returns {Promise<Object>}
|
|
28
|
+
*/
|
|
29
|
+
export async function extractComponentDimensions(page, viewportName) {
|
|
30
|
+
const thresholds = { HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH, MAX_BUTTONS, MAX_IMAGES };
|
|
31
|
+
|
|
32
|
+
// Pass 1: containers, typography, buttons, images
|
|
33
|
+
const results = await page.evaluate(({ vpName, th }) => {
|
|
34
|
+
const data = {
|
|
35
|
+
viewport: vpName,
|
|
36
|
+
extractedAt: new Date().toISOString(),
|
|
37
|
+
containers: [], cards: [], typography: [], buttons: [], images: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// --- Shared helpers (browser context) ---
|
|
41
|
+
|
|
42
|
+
function extractDimensions(el) {
|
|
43
|
+
const r = el.getBoundingClientRect(), cs = window.getComputedStyle(el);
|
|
44
|
+
const pf = v => parseFloat(v) || 0;
|
|
45
|
+
return {
|
|
46
|
+
width: Math.round(r.width), height: Math.round(r.height),
|
|
47
|
+
x: Math.round(r.x), y: Math.round(r.y),
|
|
48
|
+
absoluteX: Math.round(r.x + window.scrollX), absoluteY: Math.round(r.y + window.scrollY),
|
|
49
|
+
paddingTop: pf(cs.paddingTop), paddingRight: pf(cs.paddingRight),
|
|
50
|
+
paddingBottom: pf(cs.paddingBottom), paddingLeft: pf(cs.paddingLeft),
|
|
51
|
+
marginTop: pf(cs.marginTop), marginRight: pf(cs.marginRight),
|
|
52
|
+
marginBottom: pf(cs.marginBottom), marginLeft: pf(cs.marginLeft),
|
|
53
|
+
display: cs.display, position: cs.position,
|
|
54
|
+
flexDirection: cs.flexDirection !== 'row' ? cs.flexDirection : undefined,
|
|
55
|
+
justifyContent: cs.justifyContent !== 'normal' ? cs.justifyContent : undefined,
|
|
56
|
+
alignItems: cs.alignItems !== 'normal' ? cs.alignItems : undefined,
|
|
57
|
+
gap: pf(cs.gap),
|
|
58
|
+
gridTemplateColumns: cs.gridTemplateColumns !== 'none' ? cs.gridTemplateColumns : undefined,
|
|
59
|
+
gridTemplateRows: cs.gridTemplateRows !== 'none' ? cs.gridTemplateRows : undefined,
|
|
60
|
+
backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined,
|
|
61
|
+
borderRadius: cs.borderRadius !== '0px' ? cs.borderRadius : undefined,
|
|
62
|
+
boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
|
|
63
|
+
fontSize: pf(cs.fontSize), fontWeight: cs.fontWeight, lineHeight: cs.lineHeight,
|
|
64
|
+
letterSpacing: cs.letterSpacing !== 'normal' ? cs.letterSpacing : undefined,
|
|
65
|
+
color: cs.color
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cleanObject(obj) {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
Object.entries(obj).filter(([_, v]) => v !== undefined && v !== null && v !== 0 && v !== '')
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
|
76
|
+
|
|
77
|
+
function detectSection(el) {
|
|
78
|
+
const r = el.getBoundingClientRect(), cs = window.getComputedStyle(el);
|
|
79
|
+
const tag = el.tagName.toLowerCase(), yr = (r.y + window.scrollY) / pageHeight;
|
|
80
|
+
if (tag === 'header' || el.closest('header')) return 'header';
|
|
81
|
+
if (tag === 'footer' || el.closest('footer')) return 'footer';
|
|
82
|
+
if (tag === 'aside' || el.closest('aside')) return 'sidebar';
|
|
83
|
+
if (tag === 'nav' || el.closest('nav')) return 'nav';
|
|
84
|
+
if (yr < th.HERO_THRESHOLD && r.height > 300) return 'hero';
|
|
85
|
+
if (yr > th.FOOTER_THRESHOLD) return 'footer';
|
|
86
|
+
if ((cs.position === 'fixed' || cs.position === 'sticky') && r.width < th.SIDEBAR_MAX_WIDTH) return 'sidebar';
|
|
87
|
+
return 'content';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Extraction functions ---
|
|
91
|
+
|
|
92
|
+
function extractContainers() {
|
|
93
|
+
const selectors = [
|
|
94
|
+
'section','main','article','header','footer',
|
|
95
|
+
'[role="main"]','[role="region"]',
|
|
96
|
+
'div[class*="container"]','div[class*="wrapper"]',
|
|
97
|
+
'div[class*="section"]','div[class*="content"]',
|
|
98
|
+
'div[class*="grid"]','div[class*="card"]'
|
|
99
|
+
];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
selectors.forEach(sel => {
|
|
102
|
+
try {
|
|
103
|
+
document.querySelectorAll(sel).forEach(el => {
|
|
104
|
+
if (seen.has(el)) return;
|
|
105
|
+
const rect = el.getBoundingClientRect();
|
|
106
|
+
if (rect.width < 100 || rect.height < 50) return;
|
|
107
|
+
const children = Array.from(el.children).filter(c => {
|
|
108
|
+
const cr = c.getBoundingClientRect(); return cr.width > 50 && cr.height > 30;
|
|
109
|
+
});
|
|
110
|
+
if (children.length < 2) return;
|
|
111
|
+
seen.add(el);
|
|
112
|
+
const dims = extractDimensions(el);
|
|
113
|
+
dims.selector = el.className
|
|
114
|
+
? `.${el.className.split(' ').filter(c => c && !c.includes(':')).slice(0, 2).join('.')}`
|
|
115
|
+
: el.tagName.toLowerCase();
|
|
116
|
+
dims.childCount = children.length;
|
|
117
|
+
dims.section = detectSection(el);
|
|
118
|
+
if (children.length >= 2 && (dims.display === 'flex' || dims.display === 'grid')) {
|
|
119
|
+
const fr = children[0].getBoundingClientRect();
|
|
120
|
+
const sr = children[1].getBoundingClientRect();
|
|
121
|
+
const g = Math.round(dims.flexDirection === 'column' ? sr.top - fr.bottom : sr.left - fr.right);
|
|
122
|
+
if (g > 0 && g < 200) dims.calculatedGap = g;
|
|
123
|
+
}
|
|
124
|
+
data.containers.push(cleanObject(dims));
|
|
125
|
+
});
|
|
126
|
+
} catch (e) { /* ignore */ }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractTypography() {
|
|
131
|
+
['h1','h2','h3','h4','h5','h6','p'].forEach(tag => {
|
|
132
|
+
try {
|
|
133
|
+
const els = document.querySelectorAll(tag);
|
|
134
|
+
if (!els.length) return;
|
|
135
|
+
const bySection = {};
|
|
136
|
+
for (const el of els) {
|
|
137
|
+
const rect = el.getBoundingClientRect();
|
|
138
|
+
if (rect.width < 50 || rect.height < 10) continue;
|
|
139
|
+
const section = detectSection(el);
|
|
140
|
+
const dims = extractDimensions(el);
|
|
141
|
+
if (!bySection[section]) bySection[section] = [];
|
|
142
|
+
if (bySection[section].length < 2) {
|
|
143
|
+
bySection[section].push({
|
|
144
|
+
selector: tag, section, fontSize: dims.fontSize, fontWeight: dims.fontWeight,
|
|
145
|
+
lineHeight: dims.lineHeight, letterSpacing: dims.letterSpacing,
|
|
146
|
+
color: dims.color, marginTop: dims.marginTop, marginBottom: dims.marginBottom,
|
|
147
|
+
textSample: el.textContent?.trim().slice(0, 40),
|
|
148
|
+
y: Math.round(rect.y + window.scrollY)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const items of Object.values(bySection)) data.typography.push(...items);
|
|
153
|
+
} catch (e) { /* ignore */ }
|
|
154
|
+
});
|
|
155
|
+
data.typography.sort((a, b) => a.y - b.y);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractButtons() {
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
['button','a[class*="btn"]','a[class*="button"]','[role="button"]','input[type="submit"]'].forEach(sel => {
|
|
161
|
+
try {
|
|
162
|
+
document.querySelectorAll(sel).forEach(el => {
|
|
163
|
+
if (seen.has(el) || data.buttons.length >= th.MAX_BUTTONS) return;
|
|
164
|
+
const rect = el.getBoundingClientRect();
|
|
165
|
+
if (rect.width < 40 || rect.height < 20) return;
|
|
166
|
+
seen.add(el);
|
|
167
|
+
const dims = extractDimensions(el);
|
|
168
|
+
data.buttons.push({
|
|
169
|
+
width: dims.width, height: dims.height,
|
|
170
|
+
paddingTop: dims.paddingTop, paddingRight: dims.paddingRight,
|
|
171
|
+
paddingBottom: dims.paddingBottom, paddingLeft: dims.paddingLeft,
|
|
172
|
+
fontSize: dims.fontSize, fontWeight: dims.fontWeight,
|
|
173
|
+
borderRadius: dims.borderRadius, backgroundColor: dims.backgroundColor,
|
|
174
|
+
color: dims.color, text: el.textContent?.trim().slice(0, 30)
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
} catch (e) { /* ignore */ }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractImages() {
|
|
182
|
+
try {
|
|
183
|
+
document.querySelectorAll('img').forEach(el => {
|
|
184
|
+
if (data.images.length >= th.MAX_IMAGES) return;
|
|
185
|
+
const rect = el.getBoundingClientRect();
|
|
186
|
+
if (rect.width < 80 || rect.height < 80) return;
|
|
187
|
+
data.images.push({
|
|
188
|
+
width: Math.round(rect.width), height: Math.round(rect.height),
|
|
189
|
+
aspectRatio: (rect.width / rect.height).toFixed(2),
|
|
190
|
+
x: Math.round(rect.x), y: Math.round(rect.y + window.scrollY)
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
} catch (e) { /* ignore */ }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
extractContainers();
|
|
197
|
+
extractTypography();
|
|
198
|
+
extractButtons();
|
|
199
|
+
extractImages();
|
|
200
|
+
return data;
|
|
201
|
+
}, { vpName: viewportName, th: thresholds });
|
|
202
|
+
|
|
203
|
+
// Pass 2: card patterns + grid layouts (using serialized helpers)
|
|
204
|
+
const helpers = {
|
|
205
|
+
calculateSimilarity: calculateSimilarity.toString(),
|
|
206
|
+
detectLayoutType: detectLayoutType.toString(),
|
|
207
|
+
calculateGap: calculateGap.toString()
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const { cards, gridLayouts } = await page.evaluate(({ containers, helpers }) => {
|
|
211
|
+
// eslint-disable-next-line no-new-func
|
|
212
|
+
const calculateSimilarity = new Function('return (' + helpers.calculateSimilarity + ')')();
|
|
213
|
+
// eslint-disable-next-line no-new-func
|
|
214
|
+
const detectLayoutType = new Function('return (' + helpers.detectLayoutType + ')')();
|
|
215
|
+
// eslint-disable-next-line no-new-func
|
|
216
|
+
const calculateGap = new Function('return (' + helpers.calculateGap + ')')();
|
|
217
|
+
|
|
218
|
+
const cards = [], gridLayouts = [];
|
|
219
|
+
let cardGroupId = 0;
|
|
220
|
+
|
|
221
|
+
containers.forEach(container => {
|
|
222
|
+
// Card patterns
|
|
223
|
+
if (container.childCount >= 2) {
|
|
224
|
+
try {
|
|
225
|
+
const parent = document.querySelector(container.selector);
|
|
226
|
+
if (!parent) return;
|
|
227
|
+
const children = Array.from(parent.children).filter(c => {
|
|
228
|
+
const cr = c.getBoundingClientRect(); return cr.width > 80 && cr.height > 60;
|
|
229
|
+
});
|
|
230
|
+
if (children.length < 2) return;
|
|
231
|
+
|
|
232
|
+
const childDims = children.map(c => {
|
|
233
|
+
const cr = c.getBoundingClientRect(), cs = window.getComputedStyle(c);
|
|
234
|
+
return {
|
|
235
|
+
width: Math.round(cr.width), height: Math.round(cr.height),
|
|
236
|
+
x: Math.round(cr.x), y: Math.round(cr.y),
|
|
237
|
+
paddingTop: parseFloat(cs.paddingTop) || 0,
|
|
238
|
+
paddingRight: parseFloat(cs.paddingRight) || 0,
|
|
239
|
+
paddingBottom: parseFloat(cs.paddingBottom) || 0,
|
|
240
|
+
paddingLeft: parseFloat(cs.paddingLeft) || 0,
|
|
241
|
+
marginTop: parseFloat(cs.marginTop) || 0,
|
|
242
|
+
marginBottom: parseFloat(cs.marginBottom) || 0,
|
|
243
|
+
borderRadius: cs.borderRadius,
|
|
244
|
+
boxShadow: cs.boxShadow !== 'none' ? cs.boxShadow : undefined,
|
|
245
|
+
backgroundColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const used = new Set(), groups = [];
|
|
250
|
+
for (let i = 0; i < childDims.length; i++) {
|
|
251
|
+
if (used.has(i)) continue;
|
|
252
|
+
const group = [childDims[i]]; used.add(i);
|
|
253
|
+
for (let j = i + 1; j < childDims.length; j++) {
|
|
254
|
+
if (!used.has(j) && calculateSimilarity(childDims[i], childDims[j]) >= 0.70) {
|
|
255
|
+
group.push(childDims[j]); used.add(j);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (group.length >= 2) groups.push(group);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
groups.forEach(group => {
|
|
262
|
+
const avg = (arr, key) => Math.round(arr.reduce((s, el) => s + (el[key] || 0), 0) / arr.length);
|
|
263
|
+
const layout = detectLayoutType(group);
|
|
264
|
+
cards.push({
|
|
265
|
+
id: `card-group-${++cardGroupId}`, parentSelector: container.selector,
|
|
266
|
+
count: group.length, layout, gap: calculateGap(group, layout),
|
|
267
|
+
avgDimensions: {
|
|
268
|
+
width: avg(group, 'width'), height: avg(group, 'height'),
|
|
269
|
+
paddingTop: avg(group, 'paddingTop'), paddingRight: avg(group, 'paddingRight'),
|
|
270
|
+
paddingBottom: avg(group, 'paddingBottom'), paddingLeft: avg(group, 'paddingLeft')
|
|
271
|
+
},
|
|
272
|
+
borderRadius: group[0].borderRadius !== '0px' ? group[0].borderRadius : undefined,
|
|
273
|
+
boxShadow: group[0].boxShadow, backgroundColor: group[0].backgroundColor
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
} catch (e) { /* ignore */ }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Grid layouts
|
|
280
|
+
if (container.display === 'grid' || container.display === 'flex') {
|
|
281
|
+
try {
|
|
282
|
+
const parent = document.querySelector(container.selector);
|
|
283
|
+
if (!parent) return;
|
|
284
|
+
const cs = window.getComputedStyle(parent);
|
|
285
|
+
if (parent.children.length < 2) return;
|
|
286
|
+
|
|
287
|
+
if (cs.display === 'grid') {
|
|
288
|
+
const cols = cs.gridTemplateColumns;
|
|
289
|
+
const colCount = cols && cols !== 'none'
|
|
290
|
+
? cols.split(' ').filter(c => c && c !== 'none').length
|
|
291
|
+
: Math.ceil(parent.children.length / 2);
|
|
292
|
+
gridLayouts.push({
|
|
293
|
+
selector: container.selector, display: 'grid',
|
|
294
|
+
columns: colCount, rows: Math.ceil(parent.children.length / colCount),
|
|
295
|
+
columnGap: parseFloat(cs.columnGap) || parseFloat(cs.gap) || 0,
|
|
296
|
+
rowGap: parseFloat(cs.rowGap) || parseFloat(cs.gap) || 0,
|
|
297
|
+
childCount: parent.children.length
|
|
298
|
+
});
|
|
299
|
+
} else if (cs.display === 'flex') {
|
|
300
|
+
gridLayouts.push({
|
|
301
|
+
selector: container.selector, display: 'flex',
|
|
302
|
+
flexDirection: cs.flexDirection, flexWrap: cs.flexWrap,
|
|
303
|
+
gap: parseFloat(cs.gap) || container.calculatedGap || 0,
|
|
304
|
+
childCount: parent.children.length
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} catch (e) { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return { cards, gridLayouts };
|
|
312
|
+
}, { containers: results.containers, helpers });
|
|
313
|
+
|
|
314
|
+
results.cards = cards;
|
|
315
|
+
results.gridLayouts = gridLayouts;
|
|
316
|
+
return results;
|
|
317
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Summary Generator for Dimension Output
|
|
3
|
+
*
|
|
4
|
+
* Generates a compact (<5KB) AI-friendly summary from full component-dimensions.json.
|
|
5
|
+
* Includes section-aware typography, exact measurements, and responsive breakpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Infer section padding from container data.
|
|
10
|
+
* @param {Array} containers
|
|
11
|
+
* @returns {string} e.g. "64px 0"
|
|
12
|
+
*/
|
|
13
|
+
function inferSectionPadding(containers) {
|
|
14
|
+
if (!containers || containers.length === 0) return '64px 0';
|
|
15
|
+
const paddings = containers.slice(0, 5).map(c => ({
|
|
16
|
+
v: c.paddingTop || c.paddingBottom || 64,
|
|
17
|
+
h: c.paddingLeft || c.paddingRight || 0
|
|
18
|
+
}));
|
|
19
|
+
const avgV = Math.round(paddings.reduce((s, p) => s + p.v, 0) / paddings.length);
|
|
20
|
+
const avgH = Math.round(paddings.reduce((s, p) => s + p.h, 0) / paddings.length);
|
|
21
|
+
return `${avgV}px ${avgH}px`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Infer card dimensions from card pattern data.
|
|
26
|
+
* @param {Array} cards
|
|
27
|
+
* @returns {{ width: string, height: string, padding: string }}
|
|
28
|
+
*/
|
|
29
|
+
function inferCardDimensions(cards) {
|
|
30
|
+
if (!cards || cards.length === 0) {
|
|
31
|
+
return { width: 'auto', height: 'auto', padding: '24px' };
|
|
32
|
+
}
|
|
33
|
+
const first = cards[0].avgDimensions || cards[0];
|
|
34
|
+
return {
|
|
35
|
+
width: first.width ? first.width + 'px' : 'auto',
|
|
36
|
+
height: first.height > 0 ? first.height + 'px' : 'auto',
|
|
37
|
+
padding: (first.paddingTop || first.padding || 24) + 'px'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert typographyBySection to AI-friendly format with px units.
|
|
43
|
+
* Uses desktop size first, then tablet, then mobile.
|
|
44
|
+
* @param {Object} typographyBySection
|
|
45
|
+
* @returns {Object}
|
|
46
|
+
*/
|
|
47
|
+
function inferTypographyBySection(typographyBySection) {
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [section, tags] of Object.entries(typographyBySection || {})) {
|
|
50
|
+
if (!tags || Object.keys(tags).length === 0) continue;
|
|
51
|
+
result[section] = {};
|
|
52
|
+
for (const [tag, sizes] of Object.entries(tags)) {
|
|
53
|
+
const size = sizes.desktop || sizes.tablet || sizes.mobile || 0;
|
|
54
|
+
if (size > 0) result[section][tag] = size + 'px';
|
|
55
|
+
}
|
|
56
|
+
if (Object.keys(result[section]).length === 0) delete result[section];
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate AI-friendly summary (compact, <5KB).
|
|
63
|
+
* Includes section-aware typography for accurate reconstruction.
|
|
64
|
+
* @param {Object} fullOutput - Full component-dimensions.json
|
|
65
|
+
* @returns {Object} Compact summary for AI prompts
|
|
66
|
+
*/
|
|
67
|
+
export function generateAISummary(fullOutput) {
|
|
68
|
+
const { viewports, summary } = fullOutput;
|
|
69
|
+
const desktop = viewports.desktop || {};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
_comment: 'USE THESE EXACT VALUES - DO NOT ESTIMATE',
|
|
73
|
+
EXACT_DIMENSIONS: {
|
|
74
|
+
container_max_width: summary.maxContainerWidth + 'px',
|
|
75
|
+
section_padding: inferSectionPadding(desktop.containers),
|
|
76
|
+
card_dimensions: inferCardDimensions(desktop.cards),
|
|
77
|
+
gap: summary.commonGap + 'px'
|
|
78
|
+
},
|
|
79
|
+
EXACT_TYPOGRAPHY: {
|
|
80
|
+
h1: (summary.typography.h1.desktop || 48) + 'px',
|
|
81
|
+
h2: (summary.typography.h2.desktop || 36) + 'px',
|
|
82
|
+
h3: (summary.typography.h3.desktop || 24) + 'px',
|
|
83
|
+
body: (summary.typography.body.desktop || 16) + 'px'
|
|
84
|
+
},
|
|
85
|
+
TYPOGRAPHY_BY_SECTION: inferTypographyBySection(summary.typographyBySection),
|
|
86
|
+
SECTIONS: {
|
|
87
|
+
hero: summary.sections?.hero || { found: false },
|
|
88
|
+
content: summary.sections?.content || { found: false },
|
|
89
|
+
header: summary.sections?.header || { found: false },
|
|
90
|
+
footer: summary.sections?.footer || { found: false },
|
|
91
|
+
sidebar: summary.sections?.sidebar || { found: false }
|
|
92
|
+
},
|
|
93
|
+
RESPONSIVE: {
|
|
94
|
+
desktop_breakpoint: summary.breakpoints.desktop + 'px',
|
|
95
|
+
tablet_breakpoint: summary.breakpoints.tablet + 'px',
|
|
96
|
+
mobile_breakpoint: summary.breakpoints.mobile + 'px',
|
|
97
|
+
typography_scaling: {
|
|
98
|
+
h1: {
|
|
99
|
+
desktop: (summary.typography.h1.desktop || 48) + 'px',
|
|
100
|
+
tablet: (summary.typography.h1.tablet || 36) + 'px',
|
|
101
|
+
mobile: (summary.typography.h1.mobile || 28) + 'px'
|
|
102
|
+
},
|
|
103
|
+
h2: {
|
|
104
|
+
desktop: (summary.typography.h2.desktop || 36) + 'px',
|
|
105
|
+
tablet: (summary.typography.h2.tablet || 28) + 'px',
|
|
106
|
+
mobile: (summary.typography.h2.mobile || 24) + 'px'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dimension Output Builder
|
|
3
|
+
*
|
|
4
|
+
* Build and format component dimension output for JSON files.
|
|
5
|
+
* Includes sanitization, cross-viewport summary, and AI-friendly format.
|
|
6
|
+
* AI summary generation lives in dimension-output-ai-summary.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { VIEWPORTS } from '../../shared/viewports.js';
|
|
10
|
+
export { generateAISummary } from './dimension-output-ai-summary.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build final component-dimensions.json output with proper schema.
|
|
14
|
+
* @param {Object} allViewportDimensions - Dimensions from all viewports
|
|
15
|
+
* @param {string} url - Source URL
|
|
16
|
+
* @returns {Object} Final JSON structure
|
|
17
|
+
*/
|
|
18
|
+
export function buildDimensionsOutput(allViewportDimensions, url) {
|
|
19
|
+
const output = {
|
|
20
|
+
meta: {
|
|
21
|
+
version: '1.0',
|
|
22
|
+
extractedAt: new Date().toISOString(),
|
|
23
|
+
url,
|
|
24
|
+
tool: 'design-clone/screenshot.js'
|
|
25
|
+
},
|
|
26
|
+
viewports: {},
|
|
27
|
+
summary: {}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (const [vpName, vpData] of Object.entries(allViewportDimensions)) {
|
|
31
|
+
output.viewports[vpName] = sanitizeViewportData(vpData, vpName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
output.summary = buildCrossViewportSummary(output.viewports);
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize viewport data for JSON output.
|
|
40
|
+
* Rounds numbers, truncates long strings, and limits array sizes.
|
|
41
|
+
* @param {Object} data - Raw viewport dimension data
|
|
42
|
+
* @param {string} vpName - Viewport name key
|
|
43
|
+
* @returns {Object}
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeViewportData(data, vpName) {
|
|
46
|
+
if (!data) return {};
|
|
47
|
+
|
|
48
|
+
const clean = JSON.parse(JSON.stringify(data));
|
|
49
|
+
clean.width = VIEWPORTS[vpName]?.width || 0;
|
|
50
|
+
clean.height = VIEWPORTS[vpName]?.height || 0;
|
|
51
|
+
|
|
52
|
+
function roundNumbers(obj) {
|
|
53
|
+
for (const key in obj) {
|
|
54
|
+
if (typeof obj[key] === 'number') {
|
|
55
|
+
obj[key] = Math.round(obj[key]);
|
|
56
|
+
} else if (Array.isArray(obj[key])) {
|
|
57
|
+
obj[key].forEach(item => roundNumbers(item));
|
|
58
|
+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
59
|
+
roundNumbers(obj[key]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return obj;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function truncateStrings(obj, maxLen = 80) {
|
|
66
|
+
for (const key in obj) {
|
|
67
|
+
if (typeof obj[key] === 'string' && obj[key].length > maxLen) {
|
|
68
|
+
obj[key] = obj[key].slice(0, maxLen) + '...';
|
|
69
|
+
} else if (Array.isArray(obj[key])) {
|
|
70
|
+
obj[key].forEach(item => truncateStrings(item, maxLen));
|
|
71
|
+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
72
|
+
truncateStrings(obj[key], maxLen);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Limit array sizes for token efficiency
|
|
79
|
+
if (clean.containers && clean.containers.length > 15) clean.containers = clean.containers.slice(0, 15);
|
|
80
|
+
if (clean.images && clean.images.length > 10) clean.images = clean.images.slice(0, 10);
|
|
81
|
+
if (clean.buttons && clean.buttons.length > 10) clean.buttons = clean.buttons.slice(0, 10);
|
|
82
|
+
|
|
83
|
+
return truncateStrings(roundNumbers(clean));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build cross-viewport summary for AI consumption.
|
|
88
|
+
* Includes section-aware typography and container data.
|
|
89
|
+
* @param {Object} viewports - Viewport data keyed by name (desktop, tablet, mobile)
|
|
90
|
+
* @returns {Object} Summary
|
|
91
|
+
*/
|
|
92
|
+
export function buildCrossViewportSummary(viewports) {
|
|
93
|
+
const summary = {
|
|
94
|
+
maxContainerWidth: 0,
|
|
95
|
+
commonGap: 0,
|
|
96
|
+
breakpoints: {
|
|
97
|
+
desktop: VIEWPORTS.desktop.width,
|
|
98
|
+
tablet: VIEWPORTS.tablet.width,
|
|
99
|
+
mobile: VIEWPORTS.mobile.width
|
|
100
|
+
},
|
|
101
|
+
typography: { h1: {}, h2: {}, h3: {}, body: {} },
|
|
102
|
+
typographyBySection: { hero: {}, content: {}, header: {}, footer: {}, sidebar: {} },
|
|
103
|
+
cardPatterns: { totalGroups: 0, avgCardSize: null },
|
|
104
|
+
sections: {
|
|
105
|
+
hero: { found: false, containerWidth: null },
|
|
106
|
+
content: { found: false, containerWidth: null },
|
|
107
|
+
header: { found: false, containerWidth: null },
|
|
108
|
+
footer: { found: false, containerWidth: null },
|
|
109
|
+
sidebar: { found: false, width: null }
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
for (const [vpName, vpData] of Object.entries(viewports)) {
|
|
114
|
+
if (!vpData) continue;
|
|
115
|
+
|
|
116
|
+
// Container section mapping
|
|
117
|
+
if (vpData.containers) {
|
|
118
|
+
for (const container of vpData.containers) {
|
|
119
|
+
if (container.width > summary.maxContainerWidth) {
|
|
120
|
+
summary.maxContainerWidth = container.width;
|
|
121
|
+
}
|
|
122
|
+
const section = container.section || 'content';
|
|
123
|
+
if (summary.sections[section]) {
|
|
124
|
+
summary.sections[section].found = true;
|
|
125
|
+
if (section === 'sidebar') {
|
|
126
|
+
if (!summary.sections[section].width || container.width > summary.sections[section].width) {
|
|
127
|
+
summary.sections[section].width = container.width;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (!summary.sections[section].containerWidth || container.width > summary.sections[section].containerWidth) {
|
|
131
|
+
summary.sections[section].containerWidth = container.width;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Typography by section
|
|
139
|
+
if (vpData.typography) {
|
|
140
|
+
for (const typo of vpData.typography) {
|
|
141
|
+
const tag = typo.selector?.toLowerCase();
|
|
142
|
+
const section = typo.section || 'content';
|
|
143
|
+
|
|
144
|
+
// Flat typography (backward compat)
|
|
145
|
+
if (tag === 'h1' && !summary.typography.h1[vpName]) summary.typography.h1[vpName] = typo.fontSize;
|
|
146
|
+
if (tag === 'h2' && !summary.typography.h2[vpName]) summary.typography.h2[vpName] = typo.fontSize;
|
|
147
|
+
if (tag === 'h3' && !summary.typography.h3[vpName]) summary.typography.h3[vpName] = typo.fontSize;
|
|
148
|
+
if (tag === 'p' && !summary.typography.body[vpName]) summary.typography.body[vpName] = typo.fontSize;
|
|
149
|
+
|
|
150
|
+
// Section-aware typography
|
|
151
|
+
if (!summary.typographyBySection[section]) summary.typographyBySection[section] = {};
|
|
152
|
+
if (!summary.typographyBySection[section][tag]) summary.typographyBySection[section][tag] = {};
|
|
153
|
+
if (!summary.typographyBySection[section][tag][vpName]) {
|
|
154
|
+
summary.typographyBySection[section][tag][vpName] = typo.fontSize;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Card patterns
|
|
160
|
+
if (vpData.cards && vpData.cards.length > 0) {
|
|
161
|
+
summary.cardPatterns.totalGroups += vpData.cards.length;
|
|
162
|
+
if (vpName === 'desktop' && vpData.cards[0]?.avgDimensions) {
|
|
163
|
+
summary.cardPatterns.avgCardSize = vpData.cards[0].avgDimensions;
|
|
164
|
+
}
|
|
165
|
+
const gaps = vpData.cards.map(g => g.gap).filter(g => g > 0);
|
|
166
|
+
if (gaps.length > 0) {
|
|
167
|
+
summary.commonGap = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return summary;
|
|
173
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Tree Post-Processors
|
|
3
|
+
*
|
|
4
|
+
* Node-side functions that operate on the traversed DOM tree result
|
|
5
|
+
* returned from page.evaluate. Builds the landmarks map and heading
|
|
6
|
+
* tree from the raw root node structure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a landmarks map from the traversed DOM tree.
|
|
11
|
+
* @param {Object} root - Root node from traverseDOM
|
|
12
|
+
* @returns {{ header, main, footer, nav: Array, aside: Array }}
|
|
13
|
+
*/
|
|
14
|
+
export function buildLandmarksMap(root) {
|
|
15
|
+
const landmarks = {
|
|
16
|
+
header: null,
|
|
17
|
+
main: null,
|
|
18
|
+
footer: null,
|
|
19
|
+
nav: [],
|
|
20
|
+
aside: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function walk(node) {
|
|
24
|
+
if (!node) return;
|
|
25
|
+
switch (node.role) {
|
|
26
|
+
case 'header-landmark': landmarks.header = node; break;
|
|
27
|
+
case 'main': landmarks.main = node; break;
|
|
28
|
+
case 'footer-landmark': landmarks.footer = node; break;
|
|
29
|
+
case 'nav': landmarks.nav.push(node); break;
|
|
30
|
+
case 'aside': landmarks.aside.push(node); break;
|
|
31
|
+
}
|
|
32
|
+
node.children.forEach(walk);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
walk(root);
|
|
36
|
+
return landmarks;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a heading tree with section context and position, sorted by Y.
|
|
41
|
+
* Text and fontSize are filled separately (see extractHeadingData).
|
|
42
|
+
* @param {Object} root - Root node from traverseDOM
|
|
43
|
+
* @returns {Array<{ level, section, nodeId, y, fontSize, text }>}
|
|
44
|
+
*/
|
|
45
|
+
export function buildHeadingTree(root) {
|
|
46
|
+
const headings = [];
|
|
47
|
+
|
|
48
|
+
function walk(node, sectionContext) {
|
|
49
|
+
if (!node) return;
|
|
50
|
+
|
|
51
|
+
let ctx = sectionContext;
|
|
52
|
+
if (node.role === 'header-landmark') ctx = 'header';
|
|
53
|
+
else if (node.role === 'main') ctx = 'content';
|
|
54
|
+
else if (node.role === 'footer-landmark') ctx = 'footer';
|
|
55
|
+
else if (node.role === 'aside') ctx = 'sidebar';
|
|
56
|
+
else if (node.role === 'hero') ctx = 'hero';
|
|
57
|
+
if (!ctx) ctx = node.section || 'content';
|
|
58
|
+
|
|
59
|
+
if (node.role?.startsWith('heading-')) {
|
|
60
|
+
headings.push({
|
|
61
|
+
level: parseInt(node.role.slice(-1)),
|
|
62
|
+
section: ctx,
|
|
63
|
+
nodeId: node.id,
|
|
64
|
+
y: node.dimensions.y,
|
|
65
|
+
fontSize: null,
|
|
66
|
+
text: null
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
node.children.forEach(c => walk(c, ctx));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
walk(root, null);
|
|
74
|
+
return headings.sort((a, b) => a.y - b.y);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Count total nodes and max depth in the traversed tree.
|
|
79
|
+
* @param {Object} root - Root node
|
|
80
|
+
* @returns {{ totalNodes: number, maxDepth: number }}
|
|
81
|
+
*/
|
|
82
|
+
export function countTreeStats(root) {
|
|
83
|
+
let totalNodes = 0;
|
|
84
|
+
let maxActualDepth = 0;
|
|
85
|
+
|
|
86
|
+
function count(n) {
|
|
87
|
+
if (!n) return;
|
|
88
|
+
totalNodes++;
|
|
89
|
+
maxActualDepth = Math.max(maxActualDepth, n.depth);
|
|
90
|
+
n.children.forEach(count);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
count(root);
|
|
94
|
+
return { totalNodes, maxDepth: maxActualDepth };
|
|
95
|
+
}
|