@trops/dash-core 0.1.160 → 0.1.162

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.
@@ -29,11 +29,12 @@ var require$$2$4 = require('os');
29
29
  var require$$4$1 = require('url');
30
30
  var require$$2$3 = require('vm');
31
31
  var require$$1$5 = require('croner');
32
+ var require$$1$6 = require('node-vibrant/node');
33
+ var require$$3$4 = require('http');
32
34
  var require$$0$c = require('events');
33
- var require$$2$5 = require('http');
34
- var require$$3$4 = require('net');
35
+ var require$$3$5 = require('net');
35
36
  var require$$4$2 = require('tls');
36
- var require$$1$6 = require('crypto');
37
+ var require$$1$7 = require('crypto');
37
38
  var require$$0$a = require('zlib');
38
39
  var require$$0$b = require('buffer');
39
40
 
@@ -3748,7 +3749,7 @@ const { getFileContents: getFileContents$5, writeToFile: writeToFile$2 } = file;
3748
3749
  const ObjectsToCsv = require$$5;
3749
3750
  const Transform = transform;
3750
3751
  const { extractColorsFromImageURL: extractColorsFromImageURL$1 } = color;
3751
- const https$1 = require$$8;
3752
+ const https$2 = require$$8;
3752
3753
  const appName$5 = "Dashboard";
3753
3754
 
3754
3755
  const dataController$1 = {
@@ -3901,7 +3902,7 @@ const dataController$1 = {
3901
3902
 
3902
3903
  const writeStream = fs$8.createWriteStream(resolvedFilepath);
3903
3904
 
3904
- https$1
3905
+ https$2
3905
3906
  .get(url, (resp) => {
3906
3907
  resp.on("data", (chunk) => {
3907
3908
  writeStream.write(chunk);
@@ -38774,10 +38775,14 @@ css$1.stringify = stringify;
38774
38775
  *
38775
38776
  * Color extraction pipeline for generating themes from a website URL.
38776
38777
  * Extracts brand colors from HTML meta tags, CSS custom properties,
38777
- * and computed styles, then merges and ranks them into a palette.
38778
+ * computed styles, and favicon/logo images (via node-vibrant).
38778
38779
  */
38779
38780
 
38780
38781
  const css = css$1;
38782
+ const { Vibrant } = require$$1$6;
38783
+ const https$1 = require$$8;
38784
+ const http$2 = require$$3$4;
38785
+ const { URL: URL$2 } = require$$4$1;
38781
38786
 
38782
38787
  // ─── Color conversion helpers ───────────────────────────────────────────────
38783
38788
 
@@ -39072,6 +39077,178 @@ function extractComputedColors(computedStyles) {
39072
39077
  return results;
39073
39078
  }
39074
39079
 
39080
+ // ─── Favicon extraction ──────────────────────────────────────────────────────
39081
+
39082
+ /**
39083
+ * Parse HTML to find favicon and apple-touch-icon URLs.
39084
+ * Prefers apple-touch-icon (higher resolution) and largest available sizes.
39085
+ * @param {string} htmlContent - Raw HTML string
39086
+ * @returns {Array<{url: string, priority: number}>} Sorted by priority (highest first)
39087
+ */
39088
+ function extractFaviconUrls(htmlContent) {
39089
+ if (!htmlContent) return [];
39090
+ const icons = [];
39091
+
39092
+ // apple-touch-icon (higher resolution, best for extraction)
39093
+ const appleTouchPattern =
39094
+ /<link[^>]*rel\s*=\s*["']apple-touch-icon(?:-precomposed)?["'][^>]*>/gi;
39095
+ let match;
39096
+ while ((match = appleTouchPattern.exec(htmlContent)) !== null) {
39097
+ const hrefMatch = match[0].match(/href\s*=\s*["']([^"']+)["']/i);
39098
+ if (hrefMatch) {
39099
+ const sizesMatch = match[0].match(/sizes\s*=\s*["'](\d+)x(\d+)["']/i);
39100
+ const size = sizesMatch ? parseInt(sizesMatch[1], 10) : 180; // apple-touch-icon defaults to 180
39101
+ icons.push({ url: hrefMatch[1], priority: 100 + size });
39102
+ }
39103
+ }
39104
+
39105
+ // Standard favicon link tags (icon, shortcut icon)
39106
+ const iconPattern =
39107
+ /<link[^>]*rel\s*=\s*["'](?:shortcut\s+)?icon["'][^>]*>/gi;
39108
+ while ((match = iconPattern.exec(htmlContent)) !== null) {
39109
+ const hrefMatch = match[0].match(/href\s*=\s*["']([^"']+)["']/i);
39110
+ if (hrefMatch) {
39111
+ const sizesMatch = match[0].match(/sizes\s*=\s*["'](\d+)x(\d+)["']/i);
39112
+ const size = sizesMatch ? parseInt(sizesMatch[1], 10) : 16;
39113
+ icons.push({ url: hrefMatch[1], priority: size });
39114
+ }
39115
+ }
39116
+
39117
+ // Sort by priority descending (prefer largest / apple-touch-icon)
39118
+ icons.sort((a, b) => b.priority - a.priority);
39119
+ return icons;
39120
+ }
39121
+
39122
+ /**
39123
+ * Resolve a potentially relative URL against a base URL.
39124
+ * @param {string} href - The href from the HTML (may be relative)
39125
+ * @param {string} baseUrl - The page URL to resolve against
39126
+ * @returns {string|null} Absolute URL or null if invalid
39127
+ */
39128
+ function resolveUrl(href, baseUrl) {
39129
+ try {
39130
+ return new URL$2(href, baseUrl).href;
39131
+ } catch {
39132
+ return null;
39133
+ }
39134
+ }
39135
+
39136
+ /**
39137
+ * Fetch a URL and return the response as a Buffer.
39138
+ * Follows redirects (up to 5). Times out after 10 seconds.
39139
+ * @param {string} url - Absolute URL to fetch
39140
+ * @returns {Promise<Buffer>}
39141
+ */
39142
+ function fetchBuffer(url) {
39143
+ return new Promise((resolve, reject) => {
39144
+ const parsedUrl = new URL$2(url);
39145
+ const client = parsedUrl.protocol === "https:" ? https$1 : http$2;
39146
+ const request = client.get(
39147
+ url,
39148
+ { timeout: 10000, headers: { "User-Agent": "Dash/1.0" } },
39149
+ (res) => {
39150
+ // Follow redirects
39151
+ if (
39152
+ res.statusCode >= 300 &&
39153
+ res.statusCode < 400 &&
39154
+ res.headers.location
39155
+ ) {
39156
+ const redirectUrl = resolveUrl(res.headers.location, url);
39157
+ if (redirectUrl) {
39158
+ fetchBuffer(redirectUrl).then(resolve).catch(reject);
39159
+ return;
39160
+ }
39161
+ }
39162
+ if (res.statusCode !== 200) {
39163
+ reject(new Error(`HTTP ${res.statusCode}`));
39164
+ return;
39165
+ }
39166
+ const chunks = [];
39167
+ res.on("data", (chunk) => chunks.push(chunk));
39168
+ res.on("end", () => resolve(Buffer.concat(chunks)));
39169
+ res.on("error", reject);
39170
+ },
39171
+ );
39172
+ request.on("error", reject);
39173
+ request.on("timeout", () => {
39174
+ request.destroy();
39175
+ reject(new Error("Timeout"));
39176
+ });
39177
+ });
39178
+ }
39179
+
39180
+ /**
39181
+ * Extract colors from favicon/logo images using node-vibrant.
39182
+ * Tries icons in priority order (apple-touch-icon first, largest first).
39183
+ * Returns on the first successful extraction.
39184
+ *
39185
+ * @param {string} htmlContent - Raw HTML to parse for icon URLs
39186
+ * @param {string} baseUrl - Page URL for resolving relative icon paths
39187
+ * @returns {Promise<Array<{hex: string, source: string, confidence: number}>>}
39188
+ */
39189
+ async function extractFaviconColors(htmlContent, baseUrl) {
39190
+ const iconEntries = extractFaviconUrls(htmlContent);
39191
+ if (iconEntries.length === 0) {
39192
+ // Fallback: try /favicon.ico at the domain root
39193
+ try {
39194
+ const rootFavicon = new URL$2("/favicon.ico", baseUrl).href;
39195
+ iconEntries.push({ url: rootFavicon, priority: 1 });
39196
+ } catch {
39197
+ return [];
39198
+ }
39199
+ }
39200
+
39201
+ for (const entry of iconEntries) {
39202
+ const absoluteUrl = resolveUrl(entry.url, baseUrl);
39203
+ if (!absoluteUrl) continue;
39204
+
39205
+ try {
39206
+ const buffer = await fetchBuffer(absoluteUrl);
39207
+ const palette = await Vibrant.from(buffer).getPalette();
39208
+
39209
+ const results = [];
39210
+ const swatchNames = [
39211
+ "Vibrant",
39212
+ "DarkVibrant",
39213
+ "LightVibrant",
39214
+ "Muted",
39215
+ "DarkMuted",
39216
+ "LightMuted",
39217
+ ];
39218
+
39219
+ for (const name of swatchNames) {
39220
+ const swatch = palette[name];
39221
+ if (!swatch) continue;
39222
+ const [r, g, b] = swatch.rgb;
39223
+ const hex = rgbToHex({
39224
+ r: Math.round(r),
39225
+ g: Math.round(g),
39226
+ b: Math.round(b),
39227
+ });
39228
+ results.push({
39229
+ hex,
39230
+ source: "favicon",
39231
+ confidence: 0.7,
39232
+ });
39233
+ }
39234
+
39235
+ if (results.length > 0) {
39236
+ console.log(
39237
+ `[themeFromUrlController] Favicon vibrant: ${results.length} swatches from ${absoluteUrl}`,
39238
+ );
39239
+ return results;
39240
+ }
39241
+ } catch (err) {
39242
+ console.warn(
39243
+ `[themeFromUrlController] Favicon extraction failed for ${absoluteUrl}: ${err.message}`,
39244
+ );
39245
+ // Try next icon
39246
+ }
39247
+ }
39248
+
39249
+ return [];
39250
+ }
39251
+
39075
39252
  // ─── Merge & rank ────────────────────────────────────────────────────────────
39076
39253
 
39077
39254
  /**
@@ -39198,13 +39375,23 @@ function mergeAndRank(allColors, maxColors = 6) {
39198
39375
  /**
39199
39376
  * Extract a ranked color palette from website content.
39200
39377
  *
39378
+ * When `baseUrl` is provided, also extracts colors from favicon/logo images
39379
+ * via node-vibrant (async). Without `baseUrl`, runs synchronously using only
39380
+ * meta tags, CSS vars, and computed styles.
39381
+ *
39201
39382
  * @param {Object} params
39202
39383
  * @param {string} params.htmlContent - Raw HTML of the page
39203
39384
  * @param {string} params.cssContent - Concatenated CSS content
39204
39385
  * @param {Object} params.computedStyles - Map of selector → { color, backgroundColor, borderColor }
39205
- * @returns {{ palette: Array, rawCount: number }}
39206
- */
39207
- function extractColorsFromUrl({ htmlContent, cssContent, computedStyles }) {
39386
+ * @param {string} [params.baseUrl] - Page URL for resolving favicon paths (enables image extraction)
39387
+ * @returns {Promise<{ palette: Array, rawCount: number }>}
39388
+ */
39389
+ async function extractColorsFromUrl({
39390
+ htmlContent,
39391
+ cssContent,
39392
+ computedStyles,
39393
+ baseUrl,
39394
+ }) {
39208
39395
  console.log("[themeFromUrlController] Starting color extraction pipeline");
39209
39396
 
39210
39397
  const metaColors = extractMetaColors(htmlContent);
@@ -39222,7 +39409,27 @@ function extractColorsFromUrl({ htmlContent, cssContent, computedStyles }) {
39222
39409
  `[themeFromUrlController] Computed styles: ${computedColors.length} colors`,
39223
39410
  );
39224
39411
 
39225
- const allColors = [...metaColors, ...cssVarColors, ...computedColors];
39412
+ // Favicon extraction (async, requires baseUrl)
39413
+ let faviconColors = [];
39414
+ if (baseUrl) {
39415
+ try {
39416
+ faviconColors = await extractFaviconColors(htmlContent, baseUrl);
39417
+ console.log(
39418
+ `[themeFromUrlController] Favicon/logo: ${faviconColors.length} colors`,
39419
+ );
39420
+ } catch (err) {
39421
+ console.warn(
39422
+ `[themeFromUrlController] Favicon extraction failed: ${err.message}`,
39423
+ );
39424
+ }
39425
+ }
39426
+
39427
+ const allColors = [
39428
+ ...metaColors,
39429
+ ...cssVarColors,
39430
+ ...computedColors,
39431
+ ...faviconColors,
39432
+ ];
39226
39433
  console.log(`[themeFromUrlController] Total raw colors: ${allColors.length}`);
39227
39434
 
39228
39435
  const palette = mergeAndRank(allColors);
@@ -39238,6 +39445,8 @@ function extractColorsFromUrl({ htmlContent, cssContent, computedStyles }) {
39238
39445
 
39239
39446
  const themeFromUrlController$1 = {
39240
39447
  extractColorsFromUrl,
39448
+ extractFaviconUrls,
39449
+ extractFaviconColors,
39241
39450
  };
39242
39451
 
39243
39452
  var themeFromUrlController_1 = themeFromUrlController$1;
@@ -41465,7 +41674,7 @@ let Receiver$1 = class Receiver extends Writable {
41465
41674
  var receiver = Receiver$1;
41466
41675
 
41467
41676
  /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */
41468
- const { randomFillSync } = require$$1$6;
41677
+ const { randomFillSync } = require$$1$7;
41469
41678
 
41470
41679
  const PerMessageDeflate$2 = permessageDeflate;
41471
41680
  const { EMPTY_BUFFER: EMPTY_BUFFER$1, kWebSocket: kWebSocket$2, NOOP: NOOP$1 } = constants;
@@ -42560,10 +42769,10 @@ var extension$1 = { format: format$1, parse: parse$2 };
42560
42769
 
42561
42770
  const EventEmitter$1 = require$$0$c;
42562
42771
  const https = require$$8;
42563
- const http$1 = require$$2$5;
42564
- const net = require$$3$4;
42772
+ const http$1 = require$$3$4;
42773
+ const net = require$$3$5;
42565
42774
  const tls = require$$4$2;
42566
- const { randomBytes, createHash: createHash$1 } = require$$1$6;
42775
+ const { randomBytes, createHash: createHash$1 } = require$$1$7;
42567
42776
  const { URL: URL$1 } = require$$4$1;
42568
42777
 
42569
42778
  const PerMessageDeflate$1 = permessageDeflate;
@@ -44170,8 +44379,8 @@ var subprotocol$1 = { parse };
44170
44379
  /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */
44171
44380
 
44172
44381
  const EventEmitter = require$$0$c;
44173
- const http = require$$2$5;
44174
- const { createHash } = require$$1$6;
44382
+ const http = require$$3$4;
44383
+ const { createHash } = require$$1$7;
44175
44384
 
44176
44385
  const extension = extension$1;
44177
44386
  const PerMessageDeflate = permessageDeflate;