dembrandt 0.15.0 → 0.16.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/lib/extractors/index.js +28 -12
- package/lib/extractors/logo.js +31 -1
- package/lib/formatters/pdf.js +5 -0
- package/lib/formatters/terminal.js +7 -1
- package/package.json +1 -1
package/lib/extractors/index.js
CHANGED
|
@@ -241,6 +241,7 @@ async function simulateHumanMouse(page) {
|
|
|
241
241
|
export async function extractBranding(url, spinner, browser, options = {}) {
|
|
242
242
|
const timeoutMultiplier = options.slow ? 3 : 1;
|
|
243
243
|
const timeouts = [];
|
|
244
|
+
const degraded = []; // post-extraction stages that failed but did not abort the run
|
|
244
245
|
|
|
245
246
|
spinner.text = "Creating browser context...";
|
|
246
247
|
|
|
@@ -556,6 +557,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
556
557
|
spinner.stop();
|
|
557
558
|
|
|
558
559
|
// Inject manifest theme_color / background_color as high-confidence palette entries
|
|
560
|
+
try {
|
|
559
561
|
if (manifest) {
|
|
560
562
|
const manifestColorEntries = [
|
|
561
563
|
manifest.themeColor && { color: manifest.themeColor, label: 'manifest:theme_color' },
|
|
@@ -615,6 +617,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
615
617
|
].filter(Boolean);
|
|
616
618
|
console.log(color.success(` ✓ Manifest: ${parts.join(', ')}`));
|
|
617
619
|
}
|
|
620
|
+
} catch (e) { degraded.push('manifest'); console.log(color.warning(' ! Manifest injection: failed (continuing)')); }
|
|
618
621
|
console.log(colors.palette.length > 0 ? color.success(` ✓ Colors: ${colors.palette.length} found`) : color.warning(` ! Colors: 0 found`));
|
|
619
622
|
console.log(typography.styles.length > 0 ? color.success(` ✓ Typography: ${typography.styles.length} styles`) : color.warning(` ! Typography: 0 styles`));
|
|
620
623
|
console.log(spacing.commonValues.length > 0 ? color.success(` ✓ Spacing: ${spacing.commonValues.length} values`) : color.warning(` ! Spacing: 0 values`));
|
|
@@ -634,8 +637,10 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
634
637
|
console.log();
|
|
635
638
|
|
|
636
639
|
// Hover/focus state extraction
|
|
637
|
-
spinner.start("Extracting hover/focus state colors...");
|
|
638
640
|
const hoverFocusColors = [];
|
|
641
|
+
const interactiveStatePairs = []; // { fg, bg, state, tag } — raw rgb strings, normalized later (consumed by WCAG stage)
|
|
642
|
+
try {
|
|
643
|
+
spinner.start("Extracting hover/focus state colors...");
|
|
639
644
|
|
|
640
645
|
function splitMultiValueColors(colorValue) {
|
|
641
646
|
if (!colorValue) return [];
|
|
@@ -655,7 +660,6 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
655
660
|
`);
|
|
656
661
|
|
|
657
662
|
const sampled = interactiveElements.slice(0, 20);
|
|
658
|
-
const interactiveStatePairs = []; // { fg, bg, state, tag } — raw rgb strings, normalized later
|
|
659
663
|
|
|
660
664
|
for (const element of sampled) {
|
|
661
665
|
try {
|
|
@@ -837,9 +841,11 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
837
841
|
console.log(hoverFocusColors.length > 0 ?
|
|
838
842
|
color.success(` ✓ Hover/focus: ${hoverFocusColors.length} state colors found`) :
|
|
839
843
|
color.warning(` ! Hover/focus: 0 state colors found`));
|
|
844
|
+
} catch (e) { spinner.stop(); degraded.push('hover-focus'); console.log(color.warning(' ! Hover/focus: failed (continuing)')); }
|
|
840
845
|
|
|
841
846
|
// Dark mode
|
|
842
847
|
if (options.darkMode) {
|
|
848
|
+
try {
|
|
843
849
|
spinner.start("Extracting dark mode colors...");
|
|
844
850
|
await page.evaluate(() => {
|
|
845
851
|
document.documentElement.setAttribute("data-theme", "dark");
|
|
@@ -867,10 +873,12 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
867
873
|
|
|
868
874
|
spinner.stop();
|
|
869
875
|
console.log(color.success(` ✓ Dark mode: +${darkModeColors.palette.length} colors`));
|
|
876
|
+
} catch (e) { spinner.stop(); degraded.push('dark-mode'); console.log(color.warning(' ! Dark mode: failed (continuing)')); }
|
|
870
877
|
}
|
|
871
878
|
|
|
872
879
|
// Mobile viewport
|
|
873
880
|
if (options.mobile) {
|
|
881
|
+
try {
|
|
874
882
|
spinner.start("Extracting mobile viewport colors...");
|
|
875
883
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
876
884
|
await page.waitForTimeout(500 * timeoutMultiplier);
|
|
@@ -885,6 +893,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
885
893
|
|
|
886
894
|
spinner.stop();
|
|
887
895
|
console.log(color.success(` ✓ Mobile: +${mobileColors.palette.length} colors`));
|
|
896
|
+
} catch (e) { spinner.stop(); degraded.push('mobile'); console.log(color.warning(' ! Mobile: failed (continuing)')); }
|
|
888
897
|
}
|
|
889
898
|
|
|
890
899
|
spinner.stop();
|
|
@@ -992,16 +1001,19 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
992
1001
|
...(options.wcag ? { wcag } : {}),
|
|
993
1002
|
};
|
|
994
1003
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1004
|
+
let isCanvasOnly = false;
|
|
1005
|
+
try {
|
|
1006
|
+
isCanvasOnly = await page.evaluate(() => {
|
|
1007
|
+
const canvases = document.querySelectorAll("canvas");
|
|
1008
|
+
const hasRealContent = document.body.textContent.trim().length > 200;
|
|
1009
|
+
const hasManyCanvases = canvases.length > 3;
|
|
1010
|
+
const hasWebGL = Array.from(canvases).some((c) => {
|
|
1011
|
+
const ctx = c.getContext("webgl") || c.getContext("webgl2");
|
|
1012
|
+
return !!ctx;
|
|
1013
|
+
});
|
|
1014
|
+
return hasManyCanvases && hasWebGL && !hasRealContent;
|
|
1002
1015
|
});
|
|
1003
|
-
|
|
1004
|
-
});
|
|
1016
|
+
} catch { isCanvasOnly = false; }
|
|
1005
1017
|
|
|
1006
1018
|
if (isCanvasOnly) {
|
|
1007
1019
|
result.note = "This website uses canvas/WebGL rendering. Design system cannot be extracted from DOM.";
|
|
@@ -1009,7 +1021,9 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
1009
1021
|
}
|
|
1010
1022
|
|
|
1011
1023
|
if (options.screenshotPath) {
|
|
1012
|
-
|
|
1024
|
+
try {
|
|
1025
|
+
await page.screenshot({ path: options.screenshotPath, fullPage: false });
|
|
1026
|
+
} catch (e) { degraded.push('screenshot'); }
|
|
1013
1027
|
}
|
|
1014
1028
|
|
|
1015
1029
|
if (options.includeRawColors) {
|
|
@@ -1024,6 +1038,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
1024
1038
|
}
|
|
1025
1039
|
}
|
|
1026
1040
|
|
|
1041
|
+
if (degraded.length) result.meta.degraded = degraded;
|
|
1042
|
+
|
|
1027
1043
|
return result;
|
|
1028
1044
|
} catch (error) {
|
|
1029
1045
|
spinner.fail("Extraction failed");
|
package/lib/extractors/logo.js
CHANGED
|
@@ -262,10 +262,37 @@ export async function extractLogo(page, url) {
|
|
|
262
262
|
position: { top: rect.top, left: rect.left },
|
|
263
263
|
};
|
|
264
264
|
} else if (el.tagName === 'SVG' || el.tagName === 'svg') {
|
|
265
|
+
// Inline SVG: the logo lives in the DOM, not at a URL. Capture the
|
|
266
|
+
// resolved color (currentColor → the element's `color`), serialize the
|
|
267
|
+
// markup, and emit a self-contained data URI so the logo is usable.
|
|
268
|
+
const color = toHex(computed.color);
|
|
269
|
+
const ariaLabel = el.getAttribute('aria-label') || null;
|
|
270
|
+
|
|
271
|
+
let markup = null;
|
|
272
|
+
let dataUri = null;
|
|
273
|
+
try {
|
|
274
|
+
const clone = el.cloneNode(true);
|
|
275
|
+
// Bake the resolved color in so `currentColor` fills render standalone.
|
|
276
|
+
if (color) clone.style.color = color;
|
|
277
|
+
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
278
|
+
const serialized = clone.outerHTML;
|
|
279
|
+
// Skip pathological inline SVGs (sprite sheets, embedded rasters).
|
|
280
|
+
if (serialized && serialized.length <= 50000) {
|
|
281
|
+
markup = serialized;
|
|
282
|
+
dataUri = 'data:image/svg+xml;utf8,' + encodeURIComponent(serialized);
|
|
283
|
+
}
|
|
284
|
+
} catch {}
|
|
285
|
+
|
|
265
286
|
return {
|
|
266
287
|
source: 'svg',
|
|
267
288
|
context,
|
|
289
|
+
inline: true,
|
|
290
|
+
// Link target the logo points at; the logo itself is `markup`/`dataUri`.
|
|
268
291
|
url: parentLink ? parentLink.href : baseUrl,
|
|
292
|
+
color,
|
|
293
|
+
ariaLabel,
|
|
294
|
+
markup,
|
|
295
|
+
dataUri,
|
|
269
296
|
width: el.width?.baseVal?.value || rect.width,
|
|
270
297
|
height: el.height?.baseVal?.value || rect.height,
|
|
271
298
|
type: logoType,
|
|
@@ -557,7 +584,10 @@ export async function extractLogo(page, url) {
|
|
|
557
584
|
try { favicons.push({ type: 'twitter:image', url: new URL(twitterImage.getAttribute('content'), baseUrl).href, sizes: null }); } catch {}
|
|
558
585
|
}
|
|
559
586
|
|
|
560
|
-
|
|
587
|
+
// Only synthesize the /favicon.ico fallback when the page declares no icon at
|
|
588
|
+
// all. Sites that ship their icon under another name (e.g. favicon-purple.ico)
|
|
589
|
+
// often 404 on the bare /favicon.ico, which would surface as a broken entry.
|
|
590
|
+
if (!favicons.some(f => /icon/i.test(f.type || ''))) {
|
|
561
591
|
favicons.push({ type: 'favicon.ico', url: new URL('/favicon.ico', baseUrl).href, sizes: null });
|
|
562
592
|
}
|
|
563
593
|
|
package/lib/formatters/pdf.js
CHANGED
|
@@ -846,6 +846,11 @@ ${(() => {
|
|
|
846
846
|
function getLogoImageUrl(data) {
|
|
847
847
|
const isImageUrl = (url) => /\.(svg|png|jpg|jpeg|webp|gif)(\?.*)?$/i.test(url);
|
|
848
848
|
|
|
849
|
+
// Inline SVG logos carry their own self-contained data URI.
|
|
850
|
+
if (data.logo?.inline && data.logo.dataUri) {
|
|
851
|
+
return data.logo.dataUri;
|
|
852
|
+
}
|
|
853
|
+
|
|
849
854
|
if (data.logo?.url && isImageUrl(data.logo.url)) {
|
|
850
855
|
return data.logo.url;
|
|
851
856
|
}
|
|
@@ -69,7 +69,13 @@ function displayLogo(logo) {
|
|
|
69
69
|
|
|
70
70
|
console.log(chalk.dim('├─') + ' ' + chalk.bold('Logo'));
|
|
71
71
|
|
|
72
|
-
if (logo.
|
|
72
|
+
if (logo.inline) {
|
|
73
|
+
const colorInfo = logo.color ? ` · ${logo.color}` : '';
|
|
74
|
+
console.log(chalk.dim('│ ├─') + ' ' + chalk.dim(`inline ${logo.source || 'svg'}${colorInfo}`));
|
|
75
|
+
if (logo.url && logo.url !== '/') {
|
|
76
|
+
console.log(chalk.dim('│ ├─') + ' ' + chalk.blue(terminalLink(logo.url)));
|
|
77
|
+
}
|
|
78
|
+
} else if (logo.url) {
|
|
73
79
|
console.log(chalk.dim('│ ├─') + ' ' + chalk.blue(terminalLink(logo.url)));
|
|
74
80
|
}
|
|
75
81
|
|