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.
@@ -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
- const isCanvasOnly = await page.evaluate(() => {
996
- const canvases = document.querySelectorAll("canvas");
997
- const hasRealContent = document.body.textContent.trim().length > 200;
998
- const hasManyCanvases = canvases.length > 3;
999
- const hasWebGL = Array.from(canvases).some((c) => {
1000
- const ctx = c.getContext("webgl") || c.getContext("webgl2");
1001
- return !!ctx;
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
- return hasManyCanvases && hasWebGL && !hasRealContent;
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
- await page.screenshot({ path: options.screenshotPath, fullPage: false });
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");
@@ -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
- if (!favicons.some(f => f.url.endsWith('/favicon.ico'))) {
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
 
@@ -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.url) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dembrandt",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Extract design tokens and publicly visible CSS information from any website",
5
5
  "mcpName": "io.github.dembrandt/dembrandt",
6
6
  "main": "index.js",