saturndocs 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth-FRCFGNYT.js +14 -0
  3. package/dist/brokenLinks-TTOBP4P7.js +48 -0
  4. package/dist/build-F5PWGP57.js +39 -0
  5. package/dist/chunk-3P2OFTXR.js +71 -0
  6. package/dist/chunk-3V753MRI.js +100 -0
  7. package/dist/chunk-5KX4ZK5E.js +104 -0
  8. package/dist/chunk-CMNX5DLQ.js +119 -0
  9. package/dist/chunk-GYRVGWDM.js +3499 -0
  10. package/dist/chunk-I6U4V7B4.js +1552 -0
  11. package/dist/chunk-J6P3VGGE.js +21 -0
  12. package/dist/chunk-KGJESHJC.js +37 -0
  13. package/dist/chunk-M2BZEDAS.js +57 -0
  14. package/dist/chunk-N7QPZMLP.js +82 -0
  15. package/dist/chunk-OEAEX5YW.js +51 -0
  16. package/dist/chunk-OZWTXMO3.js +584 -0
  17. package/dist/chunk-QLFODLVN.js +46 -0
  18. package/dist/chunk-R3X2DPFF.js +568 -0
  19. package/dist/chunk-S45UWCML.js +588 -0
  20. package/dist/chunk-SABK3R2P.js +2452 -0
  21. package/dist/chunk-STCCGFKC.js +246 -0
  22. package/dist/chunk-TKISMN3P.js +39 -0
  23. package/dist/chunk-U7PKMSB3.js +465 -0
  24. package/dist/chunk-VNYDYHXM.js +22 -0
  25. package/dist/chunk-VXEUNRVA.js +26 -0
  26. package/dist/chunk-XO4CUC7V.js +99 -0
  27. package/dist/cli.d.ts +1 -0
  28. package/dist/cli.js +95 -0
  29. package/dist/deploy-QU7DCKWU.js +11 -0
  30. package/dist/dev-VDSDCBO2.js +43 -0
  31. package/dist/index.d.ts +382 -0
  32. package/dist/index.js +191 -0
  33. package/dist/init-7T5TEPBH.js +14 -0
  34. package/dist/manage-MKH27U3B.js +12 -0
  35. package/dist/openapiCheck-KGBQJTIQ.js +66 -0
  36. package/dist/pages-HIPXRLSY.js +15 -0
  37. package/dist/read-DMSJUEDA.js +160 -0
  38. package/dist/read-ZEVUZNNB.js +146 -0
  39. package/dist/requests-H5MIGBIS.js +13 -0
  40. package/dist/schema-XJUEZSIW.js +13 -0
  41. package/dist/sites-I4WI2MPG.js +14 -0
  42. package/dist/status-7R3NOI5H.js +14 -0
  43. package/dist/suggestions-4WFQ3APX.js +739 -0
  44. package/dist/validate-VXL4PL42.js +30 -0
  45. package/package.json +68 -0
@@ -0,0 +1,2452 @@
1
+ import {
2
+ loadDocsConfig
3
+ } from "./chunk-R3X2DPFF.js";
4
+ import {
5
+ messageOf
6
+ } from "./chunk-VNYDYHXM.js";
7
+
8
+ // src/commands/init.ts
9
+ import { existsSync } from "fs";
10
+ import { mkdir, writeFile } from "fs/promises";
11
+ import { basename, join, relative, resolve } from "path";
12
+
13
+ // ../brand/dist/index.js
14
+ var DEFAULT_PAGE_MAX_BYTES = 2 * 1024 * 1024;
15
+ var DEFAULT_AUX_MAX_BYTES = 500 * 1024;
16
+ var DEFAULT_STYLESHEET_MAX_BYTES = 1024 * 1024;
17
+ var DEFAULT_LOGO_MAX_BYTES = 2 * 1024 * 1024;
18
+ var DEFAULT_MAX_REQUESTS = 25;
19
+ var DEFAULT_TIMEOUT_MS = 1e4;
20
+ var DEFAULT_MAX_REDIRECTS = 3;
21
+ var USER_AGENT = "SaturnDocsBrandExtractor/0.1 (+https://saturndocs.com; docs onboarding)";
22
+ var BrandFetcher = class {
23
+ fetchImpl;
24
+ maxRequests;
25
+ timeoutMs;
26
+ maxRedirects;
27
+ validateUrl;
28
+ count = 0;
29
+ constructor(options = {}) {
30
+ this.fetchImpl = options.fetchImpl ?? fetch;
31
+ this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
32
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33
+ this.maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
34
+ this.validateUrl = options.validateUrl;
35
+ }
36
+ get requestCount() {
37
+ return this.count;
38
+ }
39
+ get remainingRequests() {
40
+ return Math.max(0, this.maxRequests - this.count);
41
+ }
42
+ async fetchText(input, options = {}) {
43
+ const result = await this.fetchBytes(input, options);
44
+ return { ...result, text: new TextDecoder().decode(result.bytes) };
45
+ }
46
+ async fetchBytes(input, options = {}) {
47
+ let url = normalizeHttpUrl(input);
48
+ let redirects = 0;
49
+ await this.validate(url);
50
+ while (true) {
51
+ if (this.count >= this.maxRequests) {
52
+ throw new Error(`request budget exceeded (${this.maxRequests})`);
53
+ }
54
+ this.count += 1;
55
+ const response = await this.fetchOnce(url, options.accept);
56
+ const location = response.headers.get("location");
57
+ if (isRedirect(response.status) && location) {
58
+ if (redirects >= this.maxRedirects) {
59
+ throw new Error(`redirect limit exceeded (${this.maxRedirects})`);
60
+ }
61
+ const next = new URL(location, url);
62
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
63
+ throw new Error(`redirect target is not http/https: ${next.href}`);
64
+ }
65
+ await this.validate(next);
66
+ redirects += 1;
67
+ url = next;
68
+ continue;
69
+ }
70
+ if (!response.ok) {
71
+ throw new Error(`fetch failed for ${url.href}: HTTP ${response.status}`);
72
+ }
73
+ const maxBytes = options.maxBytes ?? DEFAULT_PAGE_MAX_BYTES;
74
+ const body = await readResponseBytes(
75
+ response,
76
+ maxBytes,
77
+ options.truncate === true
78
+ );
79
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
80
+ return {
81
+ url: url.toString(),
82
+ status: response.status,
83
+ contentType,
84
+ bytes: body.bytes,
85
+ truncated: body.truncated
86
+ };
87
+ }
88
+ }
89
+ async fetchOnce(url, accept) {
90
+ const controller = new AbortController();
91
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
92
+ try {
93
+ return await this.fetchImpl(url, {
94
+ redirect: "manual",
95
+ signal: controller.signal,
96
+ headers: {
97
+ "user-agent": USER_AGENT,
98
+ accept: accept ?? "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
99
+ }
100
+ });
101
+ } finally {
102
+ clearTimeout(timeout);
103
+ }
104
+ }
105
+ async validate(url) {
106
+ await this.validateUrl?.(url);
107
+ }
108
+ };
109
+ function normalizeHttpUrl(input) {
110
+ if (input instanceof URL) {
111
+ assertHttpUrl(input);
112
+ return input;
113
+ }
114
+ const raw = input.trim();
115
+ const hasHierarchicalScheme = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(raw);
116
+ const hasAnyScheme = /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(raw);
117
+ const candidate = hasHierarchicalScheme || hasAnyScheme ? raw : `https://${raw}`;
118
+ const url = new URL(candidate);
119
+ assertHttpUrl(url);
120
+ return url;
121
+ }
122
+ async function readResponseBytes(response, maxBytes, truncate2) {
123
+ const contentLength = response.headers.get("content-length");
124
+ let declaredExceedsCap = false;
125
+ if (contentLength !== null) {
126
+ const declared = Number(contentLength);
127
+ if (Number.isFinite(declared) && declared > maxBytes) {
128
+ declaredExceedsCap = true;
129
+ if (!truncate2) {
130
+ throw new Error(`response exceeded size cap (${maxBytes} bytes)`);
131
+ }
132
+ }
133
+ }
134
+ if (!response.body) {
135
+ const buffer = await response.arrayBuffer();
136
+ if (buffer.byteLength > maxBytes) {
137
+ if (!truncate2) {
138
+ throw new Error(`response exceeded size cap (${maxBytes} bytes)`);
139
+ }
140
+ return {
141
+ bytes: new Uint8Array(buffer.slice(0, maxBytes)),
142
+ truncated: true
143
+ };
144
+ }
145
+ return { bytes: new Uint8Array(buffer), truncated: false };
146
+ }
147
+ const reader = response.body.getReader();
148
+ const chunks = [];
149
+ let total = 0;
150
+ while (true) {
151
+ const { value, done } = await reader.read();
152
+ if (done) break;
153
+ if (!value) continue;
154
+ if (total + value.byteLength > maxBytes) {
155
+ await reader.cancel();
156
+ if (!truncate2) {
157
+ throw new Error(`response exceeded size cap (${maxBytes} bytes)`);
158
+ }
159
+ const remaining = maxBytes - total;
160
+ if (remaining > 0) {
161
+ chunks.push(value.subarray(0, remaining));
162
+ total += remaining;
163
+ }
164
+ return { bytes: concatChunks(chunks, total), truncated: true };
165
+ }
166
+ chunks.push(value);
167
+ total += value.byteLength;
168
+ if (truncate2 && declaredExceedsCap && total >= maxBytes) {
169
+ await reader.cancel();
170
+ return { bytes: concatChunks(chunks, total), truncated: true };
171
+ }
172
+ }
173
+ return { bytes: concatChunks(chunks, total), truncated: false };
174
+ }
175
+ function concatChunks(chunks, total) {
176
+ const output = new Uint8Array(total);
177
+ let offset = 0;
178
+ for (const chunk of chunks) {
179
+ output.set(chunk, offset);
180
+ offset += chunk.byteLength;
181
+ }
182
+ return output;
183
+ }
184
+ function assertHttpUrl(url) {
185
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
186
+ throw new Error(`Only http/https URLs are supported: ${url.href}`);
187
+ }
188
+ }
189
+ function isRedirect(status) {
190
+ return status >= 300 && status < 400;
191
+ }
192
+ var COLOR_LITERAL_RE = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3})(?![0-9a-f])|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/gi;
193
+ var SRGB_LINEAR_EPSILON = 8e-4;
194
+ function parseCssColor(value) {
195
+ const input = value.trim();
196
+ const hex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.exec(
197
+ input
198
+ );
199
+ if (hex) return parseHexColor(hex[1]);
200
+ const rgb = /^rgba?\((.*)\)$/i.exec(input);
201
+ if (rgb) return parseRgbColor(rgb[1]);
202
+ const hsl = /^hsla?\((.*)\)$/i.exec(input);
203
+ if (hsl) return parseHslColor(hsl[1]);
204
+ const oklch = /^oklch\((.*)\)$/i.exec(input);
205
+ if (oklch) return parseOklchColor(oklch[1]);
206
+ return void 0;
207
+ }
208
+ function colorToHex(color) {
209
+ return `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`;
210
+ }
211
+ function normalizeHexColor(value) {
212
+ const parsed = parseCssColor(value);
213
+ return parsed ? colorToHex(parsed) : void 0;
214
+ }
215
+ function rgbToHsl(color) {
216
+ const r = color.r / 255;
217
+ const g = color.g / 255;
218
+ const b = color.b / 255;
219
+ const max = Math.max(r, g, b);
220
+ const min = Math.min(r, g, b);
221
+ const l = (max + min) / 2;
222
+ if (max === min) return { h: 0, s: 0, l };
223
+ const delta = max - min;
224
+ const s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
225
+ let h;
226
+ if (max === r) {
227
+ h = (g - b) / delta + (g < b ? 6 : 0);
228
+ } else if (max === g) {
229
+ h = (b - r) / delta + 2;
230
+ } else {
231
+ h = (r - g) / delta + 4;
232
+ }
233
+ return { h: h * 60, s, l };
234
+ }
235
+ function saturationFitness(color) {
236
+ const hsl = rgbToHsl(color);
237
+ if (hsl.s < 0.15) return 0;
238
+ if (hsl.l > 0.93) return 0;
239
+ if (hsl.l < 0.07) return 0;
240
+ const midLightnessBoost = hsl.l >= 0.3 && hsl.l <= 0.7 ? 1.2 : 1;
241
+ return hsl.s * midLightnessBoost;
242
+ }
243
+ function scoreColorCandidates(candidates) {
244
+ const byHex = /* @__PURE__ */ new Map();
245
+ candidates.forEach((candidate, index) => {
246
+ const color = parseCssColor(candidate.value);
247
+ if (!color) return;
248
+ const fitness = saturationFitness(color);
249
+ if (fitness <= 0) return;
250
+ const hex = colorToHex(color);
251
+ const frequency = candidate.frequency ?? 1;
252
+ const score = candidate.weight * frequency * fitness;
253
+ const existing = byHex.get(hex);
254
+ if (existing) {
255
+ existing.score += score;
256
+ existing.frequency += frequency;
257
+ existing.provenance.add(candidate.provenance);
258
+ return;
259
+ }
260
+ byHex.set(hex, {
261
+ color,
262
+ score,
263
+ provenance: /* @__PURE__ */ new Set([candidate.provenance]),
264
+ frequency,
265
+ firstIndex: index
266
+ });
267
+ });
268
+ return [...byHex.entries()].map(([hex, value]) => ({
269
+ hex,
270
+ score: value.score,
271
+ color: value.color,
272
+ provenance: [...value.provenance],
273
+ frequency: value.frequency,
274
+ firstIndex: value.firstIndex
275
+ })).sort((a, b) => b.score - a.score || a.firstIndex - b.firstIndex).map((value) => {
276
+ const { firstIndex, ...rest } = value;
277
+ void firstIndex;
278
+ return rest;
279
+ });
280
+ }
281
+ function pickPrimaryColor(candidates) {
282
+ return scoreColorCandidates(candidates)[0];
283
+ }
284
+ function extractCssColorCandidates(css, provenance = "css") {
285
+ const candidates = [];
286
+ const declarationRe = /(?:^|[;{])\s*([-\w]+)\s*:\s*([^;{}]+)/g;
287
+ for (const match of css.matchAll(declarationRe)) {
288
+ const property = match[1];
289
+ const body = match[2];
290
+ const colorLiterals = extractColorLiterals(body);
291
+ if (colorLiterals.length === 0) continue;
292
+ const isBrandProperty = /primary|brand|accent/i.test(property);
293
+ for (const value of colorLiterals) {
294
+ candidates.push({
295
+ value,
296
+ weight: isBrandProperty ? 4 : 1,
297
+ provenance: `${provenance}:${property}`
298
+ });
299
+ }
300
+ }
301
+ return candidates;
302
+ }
303
+ function extractColorLiterals(input) {
304
+ const values = [];
305
+ for (const match of input.matchAll(COLOR_LITERAL_RE)) {
306
+ const value = match[0];
307
+ if (parseCssColor(value)) values.push(value);
308
+ }
309
+ return values;
310
+ }
311
+ function parseHexColor(hex) {
312
+ if (hex.length === 3) {
313
+ return {
314
+ r: parseInt(`${hex[0]}${hex[0]}`, 16),
315
+ g: parseInt(`${hex[1]}${hex[1]}`, 16),
316
+ b: parseInt(`${hex[2]}${hex[2]}`, 16),
317
+ a: 1
318
+ };
319
+ }
320
+ if (hex.length === 6 || hex.length === 8) {
321
+ return {
322
+ r: parseInt(hex.slice(0, 2), 16),
323
+ g: parseInt(hex.slice(2, 4), 16),
324
+ b: parseInt(hex.slice(4, 6), 16),
325
+ a: hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1
326
+ };
327
+ }
328
+ return void 0;
329
+ }
330
+ function parseRgbColor(body) {
331
+ const parts = splitFunctionArgs(body);
332
+ if (parts.length < 3 || parts.length > 4) return void 0;
333
+ const r = parseRgbComponent(parts[0]);
334
+ const g = parseRgbComponent(parts[1]);
335
+ const b = parseRgbComponent(parts[2]);
336
+ if (r === void 0 || g === void 0 || b === void 0) return void 0;
337
+ const a = parts[3] === void 0 ? 1 : parseAlpha(parts[3]);
338
+ if (a === void 0) return void 0;
339
+ return { r, g, b, a };
340
+ }
341
+ function parseHslColor(body) {
342
+ const parts = splitFunctionArgs(body);
343
+ if (parts.length < 3 || parts.length > 4) return void 0;
344
+ const hue = parseHue(parts[0]);
345
+ const saturation = parsePercent(parts[1]);
346
+ const lightness = parsePercent(parts[2]);
347
+ if (hue === void 0 || saturation === void 0 || lightness === void 0) {
348
+ return void 0;
349
+ }
350
+ const alpha = parts[3] === void 0 ? 1 : parseAlpha(parts[3]);
351
+ if (alpha === void 0) return void 0;
352
+ const rgb = hslToRgb(hue, saturation, lightness);
353
+ return { ...rgb, a: alpha };
354
+ }
355
+ function parseOklchColor(body) {
356
+ const args = splitOklchArgs(body);
357
+ if (!args || args.components.length !== 3) return void 0;
358
+ const lightness = parseOklchLightness(args.components[0]);
359
+ const chroma = parseOklchChroma(args.components[1]);
360
+ const hue = parseOklchHue(args.components[2]);
361
+ if (lightness === void 0 || chroma === void 0 || hue === void 0) {
362
+ return void 0;
363
+ }
364
+ const alpha = args.alpha === void 0 ? 1 : parseOklchAlpha(args.alpha);
365
+ if (alpha === void 0) return void 0;
366
+ const rgb = oklchToRgb(lightness, chroma, hue);
367
+ return { ...rgb, a: alpha };
368
+ }
369
+ function splitFunctionArgs(body) {
370
+ const slashParts = body.trim().split("/");
371
+ if (slashParts.length > 2) return [];
372
+ const colorPart = slashParts[0] ?? "";
373
+ const alphaPart = slashParts[1];
374
+ const parts = colorPart.includes(",") ? colorPart.split(",").map((part) => part.trim()) : colorPart.split(/\s+/).map((part) => part.trim());
375
+ if (alphaPart !== void 0) parts.push(alphaPart.trim());
376
+ return parts.filter((part) => part.length > 0);
377
+ }
378
+ function splitOklchArgs(body) {
379
+ const slashParts = body.trim().split("/");
380
+ if (slashParts.length > 2) return void 0;
381
+ const colorPart = slashParts[0]?.trim() ?? "";
382
+ if (colorPart.includes(",")) return void 0;
383
+ const components = colorPart.split(/\s+/).map((part) => part.trim()).filter((part) => part.length > 0);
384
+ if (slashParts.length === 1) return { components };
385
+ const alpha = slashParts[1]?.trim();
386
+ if (!alpha || /\s/.test(alpha)) return void 0;
387
+ return { components, alpha };
388
+ }
389
+ function parseRgbComponent(value) {
390
+ if (value.endsWith("%")) {
391
+ const percent = parseNumber(value.slice(0, -1));
392
+ if (percent === void 0 || percent < 0 || percent > 100) return void 0;
393
+ return Math.round(percent / 100 * 255);
394
+ }
395
+ const number = parseNumber(value);
396
+ if (number === void 0 || number < 0 || number > 255) return void 0;
397
+ return Math.round(number);
398
+ }
399
+ function parseOklchLightness(value) {
400
+ const input = value.trim();
401
+ if (isNone(input)) return 0;
402
+ if (input.endsWith("%")) {
403
+ const percent = parseNumber(input.slice(0, -1));
404
+ if (percent === void 0 || percent < 0 || percent > 100) {
405
+ return void 0;
406
+ }
407
+ return percent / 100;
408
+ }
409
+ const number = parseNumber(input);
410
+ if (number === void 0 || number < 0 || number > 1) return void 0;
411
+ return number;
412
+ }
413
+ function parseOklchChroma(value) {
414
+ const input = value.trim();
415
+ if (isNone(input)) return 0;
416
+ if (input.endsWith("%")) {
417
+ const percent = parseNumber(input.slice(0, -1));
418
+ if (percent === void 0 || percent < 0) return void 0;
419
+ return percent / 100 * 0.4;
420
+ }
421
+ const number = parseNumber(input);
422
+ if (number === void 0 || number < 0) return void 0;
423
+ return number;
424
+ }
425
+ function parseOklchHue(value) {
426
+ const input = value.trim();
427
+ if (isNone(input)) return 0;
428
+ if (/(?:rad|grad|turn)$/i.test(input)) return void 0;
429
+ const normalized = input.replace(/deg$/i, "");
430
+ const number = parseNumber(normalized);
431
+ if (number === void 0) return void 0;
432
+ return (number % 360 + 360) % 360;
433
+ }
434
+ function parseOklchAlpha(value) {
435
+ return isNone(value) ? 0 : parseAlpha(value);
436
+ }
437
+ function parseHue(value) {
438
+ const normalized = value.trim().replace(/deg$/i, "");
439
+ const number = parseNumber(normalized);
440
+ if (number === void 0) return void 0;
441
+ return (number % 360 + 360) % 360;
442
+ }
443
+ function parsePercent(value) {
444
+ const input = value.trim();
445
+ if (!input.endsWith("%")) return void 0;
446
+ const number = parseNumber(input.slice(0, -1));
447
+ if (number === void 0 || number < 0 || number > 100) return void 0;
448
+ return number / 100;
449
+ }
450
+ function parseAlpha(value) {
451
+ const input = value.trim();
452
+ if (input.endsWith("%")) {
453
+ const percent = parseNumber(input.slice(0, -1));
454
+ if (percent === void 0 || percent < 0 || percent > 100) return void 0;
455
+ return percent / 100;
456
+ }
457
+ const number = parseNumber(input);
458
+ if (number === void 0 || number < 0 || number > 1) return void 0;
459
+ return number;
460
+ }
461
+ function parseNumber(value) {
462
+ if (!/^-?(?:\d+|\d*\.\d+)$/.test(value.trim())) return void 0;
463
+ const number = Number(value);
464
+ return Number.isFinite(number) ? number : void 0;
465
+ }
466
+ function isNone(value) {
467
+ return value.trim().toLowerCase() === "none";
468
+ }
469
+ function hslToRgb(h, s, l) {
470
+ const c = (1 - Math.abs(2 * l - 1)) * s;
471
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
472
+ const m = l - c / 2;
473
+ let r1 = 0;
474
+ let g1 = 0;
475
+ let b1 = 0;
476
+ if (h < 60) {
477
+ r1 = c;
478
+ g1 = x;
479
+ } else if (h < 120) {
480
+ r1 = x;
481
+ g1 = c;
482
+ } else if (h < 180) {
483
+ g1 = c;
484
+ b1 = x;
485
+ } else if (h < 240) {
486
+ g1 = x;
487
+ b1 = c;
488
+ } else if (h < 300) {
489
+ r1 = x;
490
+ b1 = c;
491
+ } else {
492
+ r1 = c;
493
+ b1 = x;
494
+ }
495
+ return {
496
+ r: Math.round((r1 + m) * 255),
497
+ g: Math.round((g1 + m) * 255),
498
+ b: Math.round((b1 + m) * 255),
499
+ a: 1
500
+ };
501
+ }
502
+ function oklchToRgb(lightness, chroma, hue) {
503
+ const hueRadians = hue * Math.PI / 180;
504
+ const okA = chroma * Math.cos(hueRadians);
505
+ const okB = chroma * Math.sin(hueRadians);
506
+ const lPrime = lightness + 0.3963377774 * okA + 0.2158037573 * okB;
507
+ const mPrime = lightness - 0.1055613458 * okA - 0.0638541728 * okB;
508
+ const sPrime = lightness - 0.0894841775 * okA - 1.291485548 * okB;
509
+ const l = lPrime * lPrime * lPrime;
510
+ const m = mPrime * mPrime * mPrime;
511
+ const s = sPrime * sPrime * sPrime;
512
+ const linearR = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
513
+ const linearG = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
514
+ const linearB = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
515
+ return {
516
+ r: Math.round(gammaEncodeSrgb(linearR) * 255),
517
+ g: Math.round(gammaEncodeSrgb(linearG) * 255),
518
+ b: Math.round(gammaEncodeSrgb(linearB) * 255),
519
+ a: 1
520
+ };
521
+ }
522
+ function gammaEncodeSrgb(value) {
523
+ const clamped = value <= SRGB_LINEAR_EPSILON ? 0 : Math.max(0, Math.min(1, value));
524
+ if (clamped <= 31308e-7) return 12.92 * clamped;
525
+ return 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
526
+ }
527
+ function toHexByte(value) {
528
+ return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0").toUpperCase();
529
+ }
530
+ var TITLE_SEPARATOR_RE = /\s*(?:\||—|–|·|:)\s*|\s+-\s+/;
531
+ var GENERIC_TITLE_SEGMENTS = /* @__PURE__ */ new Set([
532
+ "blog",
533
+ "docs",
534
+ "documentation",
535
+ "home",
536
+ "login",
537
+ "overview",
538
+ "pricing",
539
+ "sign in",
540
+ "welcome"
541
+ ]);
542
+ function extractBrandText(html, pageUrl) {
543
+ const ogSiteName = getMetaContent(html, "property", "og:site_name");
544
+ const title = extractTitle(html);
545
+ const name = cleanText(ogSiteName) ?? (title ? stripTitleSuffix(title, pageUrl?.hostname) : void 0);
546
+ const description = cleanText(getMetaContent(html, "name", "description")) ?? cleanText(getMetaContent(html, "property", "og:description"));
547
+ return { name, description };
548
+ }
549
+ function extractThemeColorMetas(html) {
550
+ const metas = getTags(html, "meta").filter((tag) => tag.attrs.name?.toLowerCase() === "theme-color").flatMap((tag) => {
551
+ const value = cleanText(tag.attrs.content);
552
+ if (!value) return [];
553
+ return [{ value, media: cleanText(tag.attrs.media) }];
554
+ });
555
+ const light = metas.filter((meta) => /light/i.test(meta.media ?? ""));
556
+ if (light.length > 0) return light;
557
+ const nonDark = metas.filter((meta) => !/dark/i.test(meta.media ?? ""));
558
+ return nonDark.length > 0 ? nonDark : metas;
559
+ }
560
+ function extractManifestUrl(html, pageUrl) {
561
+ for (const tag of getTags(html, "link")) {
562
+ if (!relIncludes(tag.attrs.rel, "manifest")) continue;
563
+ const href = cleanText(tag.attrs.href);
564
+ if (!href) continue;
565
+ const resolved = resolveHttpUrl(href, pageUrl);
566
+ if (resolved) return resolved;
567
+ }
568
+ return void 0;
569
+ }
570
+ function extractStylesheetUrls(html, pageUrl, limit = 5) {
571
+ const urls = [];
572
+ const seen = /* @__PURE__ */ new Set();
573
+ for (const tag of getTags(html, "link")) {
574
+ if (!relIncludes(tag.attrs.rel, "stylesheet")) continue;
575
+ const href = cleanText(tag.attrs.href);
576
+ if (!href) continue;
577
+ const resolved = resolveHttpUrl(href, pageUrl);
578
+ if (!resolved) continue;
579
+ if (seen.has(resolved)) continue;
580
+ seen.add(resolved);
581
+ urls.push(resolved);
582
+ if (urls.length >= limit) break;
583
+ }
584
+ return urls;
585
+ }
586
+ function extractInlineStyleBlocks(html) {
587
+ const blocks = [];
588
+ const styleRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
589
+ for (const match of html.matchAll(styleRe)) {
590
+ blocks.push(match[1] ?? "");
591
+ }
592
+ return blocks;
593
+ }
594
+ function getMetaContent(html, key, expectedValue) {
595
+ const expected = expectedValue.toLowerCase();
596
+ for (const tag of getTags(html, "meta")) {
597
+ if (tag.attrs[key]?.toLowerCase() !== expected) continue;
598
+ const content = cleanText(tag.attrs.content);
599
+ if (content) return content;
600
+ }
601
+ return void 0;
602
+ }
603
+ function getTags(html, tagName) {
604
+ const escaped = escapeRegExp(tagName);
605
+ const tagRe = new RegExp(`<${escaped}\\b[^>]*>`, "gi");
606
+ const tags = [];
607
+ for (const match of html.matchAll(tagRe)) {
608
+ const raw = match[0];
609
+ tags.push({ raw, attrs: parseAttributes(raw), index: match.index ?? 0 });
610
+ }
611
+ return tags;
612
+ }
613
+ function parseAttributes(tag) {
614
+ const attrs = {};
615
+ const body = tag.replace(/^<\s*\/?\s*[\w:-]+/, "").replace(/\/?\s*>$/, "");
616
+ const attrRe = /([:\w.-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
617
+ for (const match of body.matchAll(attrRe)) {
618
+ const name = match[1].toLowerCase();
619
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
620
+ attrs[name] = decodeHtml(value);
621
+ }
622
+ return attrs;
623
+ }
624
+ function relIncludes(rel, token) {
625
+ if (!rel) return false;
626
+ return rel.toLowerCase().split(/\s+/).filter(Boolean).includes(token.toLowerCase());
627
+ }
628
+ function resolveHttpUrl(value, baseUrl) {
629
+ const input = value.trim();
630
+ if (input.length === 0 || input.startsWith("#")) return void 0;
631
+ try {
632
+ const url = new URL(input, baseUrl);
633
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
634
+ url.hash = "";
635
+ return url.toString();
636
+ } catch {
637
+ return void 0;
638
+ }
639
+ }
640
+ function cleanText(value) {
641
+ if (value === void 0) return void 0;
642
+ const cleaned = decodeHtml(stripTags(value)).replace(/\s+/g, " ").trim();
643
+ return cleaned.length > 0 ? cleaned : void 0;
644
+ }
645
+ function stripTitleSuffix(title, hostname) {
646
+ const cleaned = cleanText(title);
647
+ if (!cleaned) return void 0;
648
+ const segments = cleaned.split(TITLE_SEPARATOR_RE).map((segment) => segment.trim()).filter((segment) => segment.length > 1);
649
+ if (segments.length === 0) return cleaned;
650
+ const meaningful = segments.filter(
651
+ (segment) => !GENERIC_TITLE_SEGMENTS.has(segment.toLowerCase())
652
+ );
653
+ const pool = meaningful.length > 0 ? meaningful : segments;
654
+ const domainLabel = domainLabelOf(hostname);
655
+ if (domainLabel) {
656
+ const byDomain = pool.find(
657
+ (segment) => normalizeForMatch(segment) === domainLabel
658
+ );
659
+ if (byDomain) return byDomain;
660
+ }
661
+ return pool[0];
662
+ }
663
+ function domainLabelOf(hostname) {
664
+ if (!hostname) return void 0;
665
+ const labels = hostname.toLowerCase().split(".").filter(Boolean);
666
+ if (labels.length < 2) return labels[0];
667
+ return labels[labels.length - 2];
668
+ }
669
+ function normalizeForMatch(value) {
670
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
671
+ }
672
+ function extractTitle(html) {
673
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html);
674
+ return cleanText(match?.[1]);
675
+ }
676
+ function stripTags(value) {
677
+ return value.replace(/<[^>]+>/g, "");
678
+ }
679
+ function decodeHtml(value) {
680
+ return value.replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&apos;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">");
681
+ }
682
+ function escapeRegExp(value) {
683
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
684
+ }
685
+ function selectLogoAssets(html, pageUrl) {
686
+ const header = findHeaderLogoCandidates(html, pageUrl).sort(compareHeaderLogos);
687
+ const ogImage = findOgLogoCandidate(html, pageUrl);
688
+ const appleIcons = findLinkIconCandidates(
689
+ html,
690
+ pageUrl,
691
+ "apple-touch-icon"
692
+ ).sort(compareBySize);
693
+ const icons = findLinkIconCandidates(html, pageUrl, "icon").sort(compareBySize);
694
+ const logo = header[0] ?? ogImage ?? appleIcons[0] ?? icons[0];
695
+ const favicon = icons[0];
696
+ return {
697
+ logo,
698
+ favicon,
699
+ candidates: [
700
+ ...header,
701
+ ...ogImage ? [ogImage] : [],
702
+ ...appleIcons,
703
+ ...icons
704
+ ]
705
+ };
706
+ }
707
+ async function fetchBrandAsset(fetcher, url, kind) {
708
+ const fetched = await fetcher.fetchBytes(url, {
709
+ accept: "image/svg+xml,image/png,image/jpeg,image/webp,image/x-icon,*/*;q=0.8",
710
+ maxBytes: DEFAULT_LOGO_MAX_BYTES
711
+ });
712
+ const contentType = fetched.contentType;
713
+ const ext = extensionForContentType(contentType, kind);
714
+ if (!contentType || !ext) {
715
+ throw new Error(
716
+ `unsupported ${kind} content type${contentType ? `: ${contentType}` : ""}`
717
+ );
718
+ }
719
+ return {
720
+ url: fetched.url,
721
+ bytes: fetched.bytes,
722
+ contentType,
723
+ suggestedFilename: `${kind}.${ext}`
724
+ };
725
+ }
726
+ function extensionForContentType(contentType, kind = "logo") {
727
+ switch (contentType?.toLowerCase().split(";")[0]?.trim()) {
728
+ case "image/svg+xml":
729
+ return "svg";
730
+ case "image/png":
731
+ return "png";
732
+ case "image/jpeg":
733
+ case "image/jpg":
734
+ return "jpg";
735
+ case "image/webp":
736
+ return "webp";
737
+ // .ico is the most common favicon format but makes a poor logo.
738
+ case "image/x-icon":
739
+ case "image/vnd.microsoft.icon":
740
+ return kind === "favicon" ? "ico" : void 0;
741
+ default:
742
+ return void 0;
743
+ }
744
+ }
745
+ function findHeaderLogoCandidates(html, pageUrl) {
746
+ const regions = headerRegions(html);
747
+ const candidates = [];
748
+ let order = 0;
749
+ for (const region of regions) {
750
+ for (const tag of getTags(region, "img")) {
751
+ const haystack = [
752
+ tag.attrs.src,
753
+ tag.attrs.class,
754
+ tag.attrs.id,
755
+ tag.attrs.alt
756
+ ].join(" ");
757
+ if (!/logo/i.test(haystack)) continue;
758
+ const src = cleanText(tag.attrs.src);
759
+ if (!src) continue;
760
+ const url = resolveHttpUrl(src, pageUrl);
761
+ if (!url) continue;
762
+ candidates.push({
763
+ url,
764
+ source: "header-image",
765
+ isSvg: isSvgUrl(url) || tag.attrs.type === "image/svg+xml",
766
+ size: parseLargestSize(tag.attrs.sizes),
767
+ order: order++
768
+ });
769
+ }
770
+ const svgRe = /<svg\b[^>]*>[\s\S]*?<\/svg>/gi;
771
+ for (const match of region.matchAll(svgRe)) {
772
+ const svgMarkup = match[0];
773
+ const openTag = /^<svg\b[^>]*>/i.exec(svgMarkup)?.[0];
774
+ const svgAttrs = openTag ? parseAttributes(openTag) : {};
775
+ const useTags = getTags(svgMarkup, "use");
776
+ for (const useTag of useTags) {
777
+ const href = cleanText(useTag.attrs.href ?? useTag.attrs["xlink:href"]);
778
+ const haystack = [
779
+ svgAttrs.class,
780
+ svgAttrs.id,
781
+ svgAttrs["aria-label"],
782
+ href,
783
+ useTag.attrs.class,
784
+ useTag.attrs.id
785
+ ].join(" ");
786
+ if (!/logo/i.test(haystack) || !href) continue;
787
+ const url = resolveHttpUrl(href, pageUrl);
788
+ if (!url) continue;
789
+ candidates.push({
790
+ url,
791
+ source: "header-svg-use",
792
+ isSvg: true,
793
+ size: 0,
794
+ order: order++
795
+ });
796
+ }
797
+ }
798
+ }
799
+ return candidates;
800
+ }
801
+ function findOgLogoCandidate(html, pageUrl) {
802
+ const content = getMetaContent(html, "property", "og:image") ?? getMetaContent(html, "name", "og:image");
803
+ if (!content) return void 0;
804
+ const url = resolveHttpUrl(content, pageUrl);
805
+ if (!url) return void 0;
806
+ const parsed = new URL(url);
807
+ if (!parsed.pathname.toLowerCase().includes("logo")) return void 0;
808
+ return {
809
+ url,
810
+ source: "og-image",
811
+ isSvg: isSvgUrl(url),
812
+ size: 0,
813
+ order: 0
814
+ };
815
+ }
816
+ function findLinkIconCandidates(html, pageUrl, kind) {
817
+ const candidates = [];
818
+ let order = 0;
819
+ for (const tag of getTags(html, "link")) {
820
+ const rel = tag.attrs.rel?.toLowerCase() ?? "";
821
+ const isApple = rel.includes("apple-touch-icon");
822
+ const matches = kind === "apple-touch-icon" ? isApple : relIncludes(rel, "icon") && !isApple;
823
+ if (!matches) continue;
824
+ const href = cleanText(tag.attrs.href);
825
+ if (!href) continue;
826
+ const url = resolveHttpUrl(href, pageUrl);
827
+ if (!url) continue;
828
+ candidates.push({
829
+ url,
830
+ source: kind,
831
+ isSvg: isSvgUrl(url) || tag.attrs.type === "image/svg+xml",
832
+ size: parseLargestSize(tag.attrs.sizes),
833
+ order: order++
834
+ });
835
+ }
836
+ return candidates;
837
+ }
838
+ function headerRegions(html) {
839
+ const body = /<body\b[^>]*>([\s\S]*?)<\/body>/i.exec(html)?.[1] ?? html;
840
+ const regions = [];
841
+ const regionRe = /<(header|nav)\b[^>]*>[\s\S]*?<\/\1>/gi;
842
+ for (const match of body.matchAll(regionRe)) {
843
+ regions.push(match[0]);
844
+ }
845
+ if (regions.length > 0) return regions;
846
+ return [body.slice(0, Math.ceil(body.length * 0.3))];
847
+ }
848
+ function compareHeaderLogos(a, b) {
849
+ if (a.isSvg !== b.isSvg) return a.isSvg ? -1 : 1;
850
+ return a.order - b.order;
851
+ }
852
+ function compareBySize(a, b) {
853
+ return b.size - a.size || a.order - b.order;
854
+ }
855
+ function parseLargestSize(value) {
856
+ if (!value) return 0;
857
+ let largest = 0;
858
+ for (const token of value.split(/\s+/)) {
859
+ const match = /^(\d+)x(\d+)$/i.exec(token);
860
+ if (!match) continue;
861
+ const width = Number(match[1]);
862
+ const height = Number(match[2]);
863
+ if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
864
+ largest = Math.max(largest, width * height);
865
+ }
866
+ return largest;
867
+ }
868
+ function isSvgUrl(url) {
869
+ return new URL(url).pathname.toLowerCase().endsWith(".svg");
870
+ }
871
+ var FONT_FAMILY_PATTERN = /^[A-Za-z0-9 +-]+$/;
872
+ var MAX_AVAILABILITY_PROBES = 3;
873
+ var IGNORED_FAMILIES = new Set(
874
+ [
875
+ "serif",
876
+ "sans-serif",
877
+ "monospace",
878
+ "system-ui",
879
+ "ui-sans-serif",
880
+ "ui-serif",
881
+ "ui-monospace",
882
+ "cursive",
883
+ "fantasy",
884
+ "math",
885
+ "emoji",
886
+ "-apple-system",
887
+ "BlinkMacSystemFont",
888
+ "Segoe UI",
889
+ "Roboto",
890
+ "Helvetica",
891
+ "Helvetica Neue",
892
+ "Arial",
893
+ "Times",
894
+ "Times New Roman",
895
+ "Georgia",
896
+ "Courier",
897
+ "Courier New",
898
+ "Menlo",
899
+ "Monaco",
900
+ "Consolas",
901
+ "SF Mono",
902
+ "SFMono-Regular",
903
+ "Liberation Mono",
904
+ "Noto Sans",
905
+ "Noto Serif"
906
+ ].map((family) => family.toLowerCase())
907
+ );
908
+ async function detectFonts({
909
+ html,
910
+ stylesheets,
911
+ fetcher,
912
+ warnings
913
+ }) {
914
+ const cssSources = [
915
+ ...extractInlineStyleBlocks(html).map((text) => ({
916
+ text,
917
+ provenance: "inline style"
918
+ })),
919
+ ...stylesheets
920
+ ];
921
+ const googleFonts = collectGoogleFontSignals(html, cssSources, warnings);
922
+ const googleByFamily = new Map(
923
+ googleFonts.map((font) => [font.family.toLowerCase(), font])
924
+ );
925
+ const selfHostedFamilies = collectSelfHostedFamilies(stylesheets);
926
+ const declared = collectDeclaredFontSignals(cssSources);
927
+ const fonts = {};
928
+ const usedGoogleFamilies = /* @__PURE__ */ new Set();
929
+ const probes = /* @__PURE__ */ new Map();
930
+ let probeCount = 0;
931
+ const resolveDeclaredFont = async (slot, signal) => {
932
+ if ("unresolvedReference" in signal) {
933
+ warnings.push(
934
+ `Could not resolve font-family ${signal.unresolvedReference} for the ${slot} slot.`
935
+ );
936
+ return;
937
+ }
938
+ if (!validateFamily(signal.family, slot, warnings)) return;
939
+ const key = signal.family.toLowerCase();
940
+ const googleSignal = googleByFamily.get(key);
941
+ if (googleSignal) {
942
+ fonts[slot] = {
943
+ family: googleSignal.family,
944
+ source: "google",
945
+ ...googleSignal.weights ? { weights: googleSignal.weights } : {},
946
+ provenance: `${signal.provenance} ${slot} font-family + ${googleSignal.provenance}`
947
+ };
948
+ usedGoogleFamilies.add(key);
949
+ return;
950
+ }
951
+ let probe = probes.get(key);
952
+ if (!probe) {
953
+ if (probeCount >= MAX_AVAILABILITY_PROBES || fetcher.remainingRequests === 0) {
954
+ warnings.push(
955
+ `Could not verify detected ${slot} font "${signal.family}" on Google Fonts because the request budget is exhausted; keeping the starter font.`
956
+ );
957
+ return;
958
+ }
959
+ probeCount += 1;
960
+ probe = probeGoogleFonts(fetcher, signal.family);
961
+ probes.set(key, probe);
962
+ }
963
+ const probeResult = await probe;
964
+ if (probeResult === "available") {
965
+ fonts[slot] = {
966
+ family: signal.family,
967
+ source: "google",
968
+ provenance: `${signal.provenance} ${slot} font-family + google probe`
969
+ };
970
+ return;
971
+ }
972
+ if (probeResult === "inconclusive") {
973
+ warnings.push(
974
+ `Could not verify detected ${slot} font "${signal.family}" on Google Fonts; keeping the starter font.`
975
+ );
976
+ return;
977
+ }
978
+ if (selfHostedFamilies.has(key)) {
979
+ warnings.push(
980
+ `Detected ${slot} font "${signal.family}" is self-hosted and not on Google Fonts; keeping the starter font. The SaturnDocs team can license and self-host it later.`
981
+ );
982
+ } else {
983
+ warnings.push(
984
+ `Detected ${slot} font "${signal.family}" is not on Google Fonts; keeping the starter font.`
985
+ );
986
+ }
987
+ };
988
+ for (const slot of ["body", "heading", "mono"]) {
989
+ const signal = declared[slot];
990
+ if (signal) await resolveDeclaredFont(slot, signal);
991
+ }
992
+ for (const [index, slot] of ["body", "heading"].entries()) {
993
+ if (fonts[slot] || declared[slot]) continue;
994
+ let signal = googleFonts[index];
995
+ if (signal && usedGoogleFamilies.has(signal.family.toLowerCase())) {
996
+ signal = googleFonts.find(
997
+ (font) => !usedGoogleFamilies.has(font.family.toLowerCase())
998
+ );
999
+ }
1000
+ if (!signal) break;
1001
+ if (!validateFamily(signal.family, slot, warnings)) continue;
1002
+ fonts[slot] = googleFontForSlot(signal, "order-of-appearance assumption");
1003
+ usedGoogleFamilies.add(signal.family.toLowerCase());
1004
+ }
1005
+ if (!fonts.mono && !declared.mono) {
1006
+ const monoSignal = googleFonts.find(
1007
+ (font) => /(?:mono|code)/i.test(font.family)
1008
+ );
1009
+ if (monoSignal && validateFamily(monoSignal.family, "mono", warnings)) {
1010
+ fonts.mono = googleFontForSlot(
1011
+ monoSignal,
1012
+ "family name indicates a mono font"
1013
+ );
1014
+ }
1015
+ }
1016
+ return Object.keys(fonts).length > 0 ? fonts : void 0;
1017
+ }
1018
+ function collectGoogleFontSignals(html, cssSources, warnings) {
1019
+ const signals = [];
1020
+ for (const tag of getTags(html, "link")) {
1021
+ if (!relIncludes(tag.attrs.rel, "stylesheet")) continue;
1022
+ const href = tag.attrs.href?.trim();
1023
+ if (!href) continue;
1024
+ signals.push(...parseGoogleFontsUrl(href, "google-fonts link"));
1025
+ }
1026
+ for (const source of cssSources) {
1027
+ for (const url of extractImportUrls(source.text)) {
1028
+ signals.push(...parseGoogleFontsUrl(url, "google-fonts @import"));
1029
+ }
1030
+ }
1031
+ const deduplicated = /* @__PURE__ */ new Map();
1032
+ for (const signal of signals) {
1033
+ if (!FONT_FAMILY_PATTERN.test(signal.family)) {
1034
+ warnings.push(
1035
+ `Detected font "${signal.family}" has characters that are not allowed in docs.json; ignoring it.`
1036
+ );
1037
+ continue;
1038
+ }
1039
+ const key = signal.family.toLowerCase();
1040
+ const current = deduplicated.get(key);
1041
+ if (!current) {
1042
+ deduplicated.set(key, signal);
1043
+ continue;
1044
+ }
1045
+ const weights = uniqueSortedWeights([
1046
+ ...current.weights ?? [],
1047
+ ...signal.weights ?? []
1048
+ ]);
1049
+ if (weights.length > 0) current.weights = weights;
1050
+ }
1051
+ return [...deduplicated.values()];
1052
+ }
1053
+ function parseGoogleFontsUrl(rawUrl, provenance) {
1054
+ let url;
1055
+ try {
1056
+ url = new URL(rawUrl.replace(/&amp;/gi, "&"), "https://example.invalid");
1057
+ } catch {
1058
+ return [];
1059
+ }
1060
+ if (url.hostname.toLowerCase() !== "fonts.googleapis.com") return [];
1061
+ if (url.pathname !== "/css" && url.pathname !== "/css2") return [];
1062
+ const signals = [];
1063
+ for (const parameter of url.searchParams.getAll("family")) {
1064
+ const families = url.pathname === "/css" ? parameter.split("|") : [parameter];
1065
+ for (const familyParameter of families) {
1066
+ const parsed = parseGoogleFamilyParameter(familyParameter);
1067
+ if (parsed) signals.push({ ...parsed, provenance });
1068
+ }
1069
+ }
1070
+ return signals;
1071
+ }
1072
+ function parseGoogleFamilyParameter(parameter) {
1073
+ const separator = parameter.indexOf(":");
1074
+ const rawFamily = separator === -1 ? parameter : parameter.slice(0, separator);
1075
+ const family = rawFamily.replace(/\+/g, " ").replace(/\s+/g, " ").trim();
1076
+ if (!family) return void 0;
1077
+ const axesAndValues = separator === -1 ? "" : parameter.slice(separator + 1);
1078
+ const weights = parseGoogleWeights(axesAndValues);
1079
+ return {
1080
+ family,
1081
+ ...weights.length > 0 ? { weights } : {}
1082
+ };
1083
+ }
1084
+ function parseGoogleWeights(value) {
1085
+ if (!value) return [];
1086
+ const at = value.indexOf("@");
1087
+ if (at === -1) return [];
1088
+ const axes = value.slice(0, at).split(",").map((axis) => axis.trim().toLowerCase());
1089
+ const weightIndex = axes.indexOf("wght");
1090
+ if (weightIndex === -1) return [];
1091
+ const weights = value.slice(at + 1).split(";").map((tuple) => tuple.split(",")[weightIndex]?.trim()).filter(
1092
+ (weight) => typeof weight === "string" && weight.length > 0 && !weight.includes("..")
1093
+ ).map(Number);
1094
+ return uniqueSortedWeights(weights);
1095
+ }
1096
+ function uniqueSortedWeights(weights) {
1097
+ return [...new Set(weights)].filter((weight) => Number.isInteger(weight) && weight >= 1 && weight <= 1e3).sort((a, b) => a - b);
1098
+ }
1099
+ function extractImportUrls(css) {
1100
+ const urls = [];
1101
+ const importRe = /@import\s+(?:url\(\s*)?(?:(["'])(.*?)\1|([^\s)'";]+))\s*\)?[^;]*;/gi;
1102
+ for (const match of stripCssComments(css).matchAll(importRe)) {
1103
+ const url = (match[2] ?? match[3] ?? "").trim();
1104
+ if (url) urls.push(url);
1105
+ }
1106
+ return urls;
1107
+ }
1108
+ function collectSelfHostedFamilies(cssSources) {
1109
+ const families = /* @__PURE__ */ new Set();
1110
+ for (const source of cssSources) {
1111
+ for (const match of stripCssComments(source.text).matchAll(
1112
+ /@font-face\s*\{([^{}]*)\}/gi
1113
+ )) {
1114
+ const family = firstUsableFamily(declarationValue(match[1] ?? ""));
1115
+ if (family) families.add(family.toLowerCase());
1116
+ }
1117
+ }
1118
+ return families;
1119
+ }
1120
+ function collectDeclaredFontSignals(cssSources) {
1121
+ const signals = {};
1122
+ const customProperties = collectCustomProperties(cssSources);
1123
+ for (const source of cssSources) {
1124
+ const css = stripCssComments(source.text).replace(
1125
+ /@font-face\s*\{[^{}]*\}/gi,
1126
+ ""
1127
+ );
1128
+ for (const rule of parseCssRules(css)) {
1129
+ const selector = rule.selector;
1130
+ const familyValue = declarationValue(rule.declarations);
1131
+ if (!selector || !familyValue) continue;
1132
+ const slots = slotsForSelector(selector);
1133
+ if (slots.length === 0) continue;
1134
+ const variable = parseLeadingVarReference(familyValue);
1135
+ const resolvedValue = variable ? resolveLeadingVarReference(familyValue, customProperties, 1) : familyValue;
1136
+ if (variable && resolvedValue === void 0) {
1137
+ for (const slot of slots) {
1138
+ signals[slot] ??= {
1139
+ unresolvedReference: `var(${variable.property})`,
1140
+ provenance: source.provenance
1141
+ };
1142
+ }
1143
+ continue;
1144
+ }
1145
+ const family = firstUsableFamily(resolvedValue);
1146
+ if (!family) continue;
1147
+ for (const slot of slots) {
1148
+ signals[slot] ??= { family, provenance: source.provenance };
1149
+ }
1150
+ }
1151
+ }
1152
+ return signals;
1153
+ }
1154
+ function collectCustomProperties(cssSources) {
1155
+ const properties = /* @__PURE__ */ new Map();
1156
+ for (const source of cssSources) {
1157
+ const css = stripCssComments(source.text).replace(
1158
+ /@font-face\s*\{[^{}]*\}/gi,
1159
+ ""
1160
+ );
1161
+ for (const rule of parseCssRules(css)) {
1162
+ if (!slotsForSelector(rule.selector).includes("body")) continue;
1163
+ for (const match of rule.declarations.matchAll(
1164
+ /(?:^|;)\s*(--[^:\s;{}]+)\s*:\s*([^;}]+)/g
1165
+ )) {
1166
+ const property = match[1];
1167
+ const value = match[2]?.replace(/\s*!important\s*$/i, "").trim();
1168
+ if (property && value !== void 0) properties.set(property, value);
1169
+ }
1170
+ }
1171
+ }
1172
+ return properties;
1173
+ }
1174
+ function resolveLeadingVarReference(value, customProperties, remainingIndirections) {
1175
+ const reference = parseLeadingVarReference(value);
1176
+ if (!reference) return value;
1177
+ const declaredValue = customProperties.get(reference.property);
1178
+ const replacement = declaredValue ?? reference.fallback;
1179
+ if (replacement === void 0) return void 0;
1180
+ let resolvedReplacement = replacement;
1181
+ if (parseLeadingVarReference(replacement)) {
1182
+ if (remainingIndirections === 0) return void 0;
1183
+ const resolved = resolveLeadingVarReference(
1184
+ replacement,
1185
+ customProperties,
1186
+ remainingIndirections - 1
1187
+ );
1188
+ if (resolved === void 0) return void 0;
1189
+ resolvedReplacement = resolved;
1190
+ }
1191
+ return `${resolvedReplacement}${value.slice(reference.endIndex)}`;
1192
+ }
1193
+ function parseLeadingVarReference(value) {
1194
+ const startIndex = value.search(/\S/);
1195
+ if (startIndex === -1 || !value.slice(startIndex).toLowerCase().startsWith("var(")) {
1196
+ return void 0;
1197
+ }
1198
+ const openIndex = startIndex + 3;
1199
+ let depth = 1;
1200
+ let quote;
1201
+ let escaped = false;
1202
+ for (let index = openIndex + 1; index < value.length; index += 1) {
1203
+ const character = value[index];
1204
+ if (quote) {
1205
+ if (escaped) {
1206
+ escaped = false;
1207
+ } else if (character === "\\") {
1208
+ escaped = true;
1209
+ } else if (character === quote) {
1210
+ quote = void 0;
1211
+ }
1212
+ continue;
1213
+ }
1214
+ if (character === '"' || character === "'") {
1215
+ quote = character;
1216
+ continue;
1217
+ }
1218
+ if (character === "(") {
1219
+ depth += 1;
1220
+ continue;
1221
+ }
1222
+ if (character !== ")") continue;
1223
+ depth -= 1;
1224
+ if (depth !== 0) continue;
1225
+ const contents = value.slice(openIndex + 1, index);
1226
+ const separator = findTopLevelComma(contents);
1227
+ const property = contents.slice(0, separator === -1 ? void 0 : separator).trim();
1228
+ if (!property.startsWith("--") || /\s/.test(property)) return void 0;
1229
+ const fallback = separator === -1 ? void 0 : contents.slice(separator + 1).trim();
1230
+ return {
1231
+ property,
1232
+ ...fallback !== void 0 ? { fallback } : {},
1233
+ endIndex: index + 1
1234
+ };
1235
+ }
1236
+ return void 0;
1237
+ }
1238
+ function findTopLevelComma(value) {
1239
+ let depth = 0;
1240
+ let quote;
1241
+ let escaped = false;
1242
+ for (let index = 0; index < value.length; index += 1) {
1243
+ const character = value[index];
1244
+ if (quote) {
1245
+ if (escaped) {
1246
+ escaped = false;
1247
+ } else if (character === "\\") {
1248
+ escaped = true;
1249
+ } else if (character === quote) {
1250
+ quote = void 0;
1251
+ }
1252
+ continue;
1253
+ }
1254
+ if (character === '"' || character === "'") {
1255
+ quote = character;
1256
+ continue;
1257
+ }
1258
+ if (character === "(") {
1259
+ depth += 1;
1260
+ } else if (character === ")") {
1261
+ depth -= 1;
1262
+ } else if (character === "," && depth === 0) {
1263
+ return index;
1264
+ }
1265
+ }
1266
+ return -1;
1267
+ }
1268
+ function parseCssRules(css) {
1269
+ const rules = [];
1270
+ const stack = [];
1271
+ let segmentStart = 0;
1272
+ let quote;
1273
+ let escaped = false;
1274
+ for (let index = 0; index < css.length; index += 1) {
1275
+ const character = css[index];
1276
+ if (quote) {
1277
+ if (escaped) {
1278
+ escaped = false;
1279
+ } else if (character === "\\") {
1280
+ escaped = true;
1281
+ } else if (character === quote) {
1282
+ quote = void 0;
1283
+ }
1284
+ continue;
1285
+ }
1286
+ if (character === '"' || character === "'") {
1287
+ quote = character;
1288
+ continue;
1289
+ }
1290
+ if (character === "{") {
1291
+ stack.push({
1292
+ selector: css.slice(segmentStart, index).trim(),
1293
+ openIndex: index
1294
+ });
1295
+ segmentStart = index + 1;
1296
+ continue;
1297
+ }
1298
+ if (character !== "}") continue;
1299
+ const block = stack.pop();
1300
+ if (block) {
1301
+ const declarations = css.slice(block.openIndex + 1, index);
1302
+ if (!/[{}]/.test(declarations) && block.selector) {
1303
+ rules.push({ selector: block.selector, declarations });
1304
+ }
1305
+ }
1306
+ segmentStart = index + 1;
1307
+ }
1308
+ return rules;
1309
+ }
1310
+ function declarationValue(declarations) {
1311
+ const matches = [...declarations.matchAll(/(?:^|;)\s*font-family\s*:\s*([^;}]+)/gi)];
1312
+ return matches.at(-1)?.[1]?.trim();
1313
+ }
1314
+ function firstUsableFamily(stack) {
1315
+ if (!stack) return void 0;
1316
+ const normalizedStack = stack.replace(/\s*!important\s*$/i, "");
1317
+ for (const item of splitFontStack(normalizedStack)) {
1318
+ const family = stripQuotes(item.trim()).replace(/\s+/g, " ").trim();
1319
+ if (!family || IGNORED_FAMILIES.has(family.toLowerCase())) continue;
1320
+ return family;
1321
+ }
1322
+ return void 0;
1323
+ }
1324
+ function splitFontStack(value) {
1325
+ const items = [];
1326
+ let current = "";
1327
+ let quote;
1328
+ for (const character of value) {
1329
+ if ((character === '"' || character === "'") && !quote) {
1330
+ quote = character;
1331
+ } else if (character === quote) {
1332
+ quote = void 0;
1333
+ }
1334
+ if (character === "," && !quote) {
1335
+ items.push(current);
1336
+ current = "";
1337
+ } else {
1338
+ current += character;
1339
+ }
1340
+ }
1341
+ items.push(current);
1342
+ return items;
1343
+ }
1344
+ function stripQuotes(value) {
1345
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1346
+ return value.slice(1, -1);
1347
+ }
1348
+ return value;
1349
+ }
1350
+ function slotsForSelector(selector) {
1351
+ const slots = /* @__PURE__ */ new Set();
1352
+ for (const item of selector.split(",")) {
1353
+ if (/(^|[\s>+~])(?:html|body)(?=$|[\s.#:[>+~])/i.test(item) || /(^|[\s>+~]):root(?=$|[\s.#:[>+~])/i.test(item) || /(^|[\s>+~])\*(?=$|[\s.#:[>+~])/.test(item)) {
1354
+ slots.add("body");
1355
+ }
1356
+ if (/(^|[\s>+~])h[123](?=$|[\s.#:[>+~])/i.test(item)) {
1357
+ slots.add("heading");
1358
+ }
1359
+ if (/(^|[\s>+~])(?:code|pre|kbd|samp)(?=$|[\s.#:[>+~])/i.test(item)) {
1360
+ slots.add("mono");
1361
+ }
1362
+ }
1363
+ return [...slots];
1364
+ }
1365
+ function validateFamily(family, slot, warnings) {
1366
+ if (FONT_FAMILY_PATTERN.test(family)) return true;
1367
+ warnings.push(
1368
+ `Detected ${slot} font "${family}" has characters that are not allowed in docs.json; keeping the starter font.`
1369
+ );
1370
+ return false;
1371
+ }
1372
+ async function probeGoogleFonts(fetcher, family) {
1373
+ const encoded = encodeURIComponent(family).replace(/%20/g, "+");
1374
+ try {
1375
+ const response = await fetcher.fetchText(
1376
+ `https://fonts.googleapis.com/css2?family=${encoded}`,
1377
+ {
1378
+ accept: "text/css,*/*;q=0.8",
1379
+ maxBytes: DEFAULT_STYLESHEET_MAX_BYTES,
1380
+ truncate: true
1381
+ }
1382
+ );
1383
+ return response.status === 200 ? "available" : "inconclusive";
1384
+ } catch (error) {
1385
+ return /HTTP 400\b/.test(messageOf2(error)) ? "unavailable" : "inconclusive";
1386
+ }
1387
+ }
1388
+ function googleFontForSlot(signal, assumption) {
1389
+ return {
1390
+ family: signal.family,
1391
+ source: "google",
1392
+ ...signal.weights ? { weights: signal.weights } : {},
1393
+ provenance: `${signal.provenance} (${assumption})`
1394
+ };
1395
+ }
1396
+ function stripCssComments(css) {
1397
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
1398
+ }
1399
+ function messageOf2(error) {
1400
+ return error instanceof Error ? error.message : String(error);
1401
+ }
1402
+ var CONFIDENCE_FIELDS = [
1403
+ "seeds.background.light",
1404
+ "seeds.background.dark",
1405
+ "seeds.text.light",
1406
+ "seeds.text.dark",
1407
+ "seeds.accent.light",
1408
+ "seeds.accent.dark",
1409
+ "fonts.heading",
1410
+ "fonts.body",
1411
+ "radius"
1412
+ ];
1413
+ function deriveThemeCandidate(html, stylesheets) {
1414
+ const css = [
1415
+ ...extractInlineStyleBlocks(html),
1416
+ ...extractStyleAttributes(html),
1417
+ ...stylesheets
1418
+ ].join("\n");
1419
+ const rules = parseRules(css);
1420
+ const customProperties = collectCustomProperties2(rules);
1421
+ const confidence = Object.fromEntries(
1422
+ CONFIDENCE_FIELDS.map((field) => [
1423
+ field,
1424
+ { level: "low", reason: `No reliable ${field} signal was found.` }
1425
+ ])
1426
+ );
1427
+ const seeds = {};
1428
+ const backgrounds = { light: [], dark: [] };
1429
+ const texts = { light: [], dark: [] };
1430
+ for (const rule of rules) {
1431
+ if (!isRootOrBodySelector(rule.selector)) continue;
1432
+ const background = declarationColor(
1433
+ rule.declarations,
1434
+ ["background-color", "background"],
1435
+ customProperties
1436
+ );
1437
+ const text = declarationColor(rule.declarations, ["color"], customProperties);
1438
+ const explicitMode = selectorMode(rule.selector);
1439
+ if (background) {
1440
+ const mode = explicitMode ?? modeFromBackground(background);
1441
+ backgrounds[mode].push({
1442
+ value: background,
1443
+ weight: isBodySelector(rule.selector) ? 3 : 2,
1444
+ reason: `${rule.selector.trim()} background declaration`
1445
+ });
1446
+ }
1447
+ if (text) {
1448
+ const mode = explicitMode ?? modeFromText(text);
1449
+ texts[mode].push({
1450
+ value: text,
1451
+ weight: isBodySelector(rule.selector) ? 3 : 2,
1452
+ reason: `${rule.selector.trim()} color declaration`
1453
+ });
1454
+ }
1455
+ }
1456
+ for (const mode of ["light", "dark"]) {
1457
+ const background = chooseSignal(backgrounds[mode]);
1458
+ setModeSignal(seeds, "background", mode, background, confidence);
1459
+ const text = chooseSignal(texts[mode]);
1460
+ setModeSignal(seeds, "text", mode, text, confidence);
1461
+ }
1462
+ const pageMode = backgrounds.dark.length > 0 && backgrounds.light.length === 0 ? "dark" : "light";
1463
+ const accents = { light: [], dark: [] };
1464
+ for (const rule of rules) {
1465
+ const explicitMode = selectorMode(rule.selector);
1466
+ if (isRootSelector(rule.selector)) {
1467
+ for (const declaration of rule.declarations) {
1468
+ if (!declaration.property.startsWith("--")) continue;
1469
+ if (!/(?:brand|primary|accent|link|button)/i.test(declaration.property)) continue;
1470
+ const color = resolveColor(declaration.value, customProperties);
1471
+ if (!color || !isAccentColor(color)) continue;
1472
+ accents[explicitMode ?? pageMode].push({
1473
+ value: color,
1474
+ weight: 4,
1475
+ reason: `${declaration.property} brand custom property`
1476
+ });
1477
+ }
1478
+ }
1479
+ if (!isAccentSelector(rule.selector)) continue;
1480
+ for (const declaration of rule.declarations) {
1481
+ if (!/^(?:color|background|background-color|border-color)$/.test(declaration.property)) {
1482
+ continue;
1483
+ }
1484
+ const color = resolveColor(declaration.value, customProperties);
1485
+ if (!color || !isAccentColor(color)) continue;
1486
+ accents[explicitMode ?? pageMode].push({
1487
+ value: color,
1488
+ weight: 1,
1489
+ reason: `${rule.selector.trim()} ${declaration.property} declaration`
1490
+ });
1491
+ }
1492
+ }
1493
+ for (const mode of ["light", "dark"]) {
1494
+ setModeSignal(seeds, "accent", mode, chooseSignal(accents[mode]), confidence);
1495
+ }
1496
+ const bodyFonts = [];
1497
+ const headingFonts = [];
1498
+ for (const rule of rules) {
1499
+ const font = declarationValue2(rule.declarations, "font-family", customProperties);
1500
+ if (!font) continue;
1501
+ if (isHeadingSelector(rule.selector)) {
1502
+ headingFonts.push({ value: cleanFontStack(font), weight: 2, reason: `${rule.selector.trim()} font-family` });
1503
+ }
1504
+ if (isRootOrBodySelector(rule.selector)) {
1505
+ bodyFonts.push({ value: cleanFontStack(font), weight: isBodySelector(rule.selector) ? 3 : 2, reason: `${rule.selector.trim()} font-family` });
1506
+ }
1507
+ }
1508
+ const heading = chooseSignal(headingFonts);
1509
+ const body = chooseSignal(bodyFonts);
1510
+ const fonts = {};
1511
+ setScalarSignal(fonts, "heading", heading, confidence, "fonts.heading");
1512
+ setScalarSignal(fonts, "body", body, confidence, "fonts.body");
1513
+ const radii = [];
1514
+ for (const rule of rules) {
1515
+ for (const declaration of rule.declarations) {
1516
+ if (declaration.property !== "border-radius") continue;
1517
+ const resolved = resolveCustomProperty(declaration.value, customProperties);
1518
+ const bucket = radiusBucket(resolved);
1519
+ const weight = radiusSignalWeight(rule.selector);
1520
+ if (!bucket || weight === 0) continue;
1521
+ radii.push({ value: bucket, weight, reason: `${rule.selector.trim()} border-radius` });
1522
+ }
1523
+ }
1524
+ const radius = chooseSignal(radii);
1525
+ const candidate = { seeds, confidence };
1526
+ if (Object.keys(fonts).length > 0) candidate.fonts = fonts;
1527
+ if (radius.value) {
1528
+ candidate.radius = radius.value;
1529
+ confidence.radius = {
1530
+ level: radius.level,
1531
+ reason: radius.reason
1532
+ };
1533
+ } else {
1534
+ confidence.radius = { level: "low", reason: radius.reason };
1535
+ }
1536
+ return candidate;
1537
+ }
1538
+ function deriveThemeRecipe(primaryColor, candidate) {
1539
+ const fallback = rgbToHsl(
1540
+ parseCssColor(primaryColor) ?? parseCssColor("#4F46E5")
1541
+ );
1542
+ const lightEvidence = confidentSeed(candidate, "background", "light");
1543
+ const darkEvidence = confidentSeed(candidate, "background", "dark");
1544
+ const lightAnchor = readingAnchor(lightEvidence, darkEvidence, fallback);
1545
+ const darkAnchor = readingAnchor(darkEvidence, lightEvidence, fallback);
1546
+ const lightBackground = canvasColor("light", lightAnchor);
1547
+ const darkBackground = contrastSafeDarkCanvas(canvasColor("dark", darkAnchor));
1548
+ const lightSurface = surfaceColor("light", lightBackground);
1549
+ const darkSurface = surfaceColor("dark", darkBackground);
1550
+ const text = confidentPair(candidate, "text");
1551
+ const accent = confidentPair(candidate, "accent");
1552
+ const recipe = {
1553
+ base: "default",
1554
+ seeds: {
1555
+ background: {
1556
+ light: hslToHex(lightBackground),
1557
+ dark: hslToHex(darkBackground)
1558
+ },
1559
+ surface: {
1560
+ light: hslToHex(lightSurface),
1561
+ dark: hslToHex(darkSurface)
1562
+ },
1563
+ ...text ? { text } : {},
1564
+ ...accent ? { accent } : {}
1565
+ }
1566
+ };
1567
+ if (candidate?.confidence.radius?.level === "high" && candidate.radius) {
1568
+ recipe.radius = candidate.radius === "large" ? "medium" : candidate.radius;
1569
+ }
1570
+ return recipe;
1571
+ }
1572
+ function confidentSeed(candidate, field, mode) {
1573
+ const value = candidate?.seeds[field]?.[mode];
1574
+ if (!value || candidate?.confidence[`seeds.${field}.${mode}`]?.level !== "high" || !parseCssColor(value)) {
1575
+ return void 0;
1576
+ }
1577
+ return value;
1578
+ }
1579
+ function confidentPair(candidate, field) {
1580
+ const light = confidentSeed(candidate, field, "light");
1581
+ const dark = confidentSeed(candidate, field, "dark");
1582
+ if (!light && !dark) return void 0;
1583
+ return { ...light ? { light } : {}, ...dark ? { dark } : {} };
1584
+ }
1585
+ function readingAnchor(preferred, alternate, fallback) {
1586
+ for (const value of [preferred, alternate]) {
1587
+ const parsed = value ? parseCssColor(value) : void 0;
1588
+ if (!parsed) continue;
1589
+ const hsl = rgbToHsl(parsed);
1590
+ if (hsl.s >= 0.06) {
1591
+ return { h: hsl.h, s: hsl.s, fromBackground: true };
1592
+ }
1593
+ }
1594
+ return { h: fallback.h, s: fallback.s, fromBackground: false };
1595
+ }
1596
+ function canvasColor(mode, anchor) {
1597
+ if (mode === "light") {
1598
+ return {
1599
+ h: anchor.h,
1600
+ s: anchor.fromBackground ? Math.min(anchor.s * 0.45, 0.18) : clamp(anchor.s * 0.1, 0.03, 0.12),
1601
+ l: 0.975
1602
+ };
1603
+ }
1604
+ const saturation = anchor.fromBackground ? Math.min(anchor.s, 0.5) : clamp(anchor.s * 0.28, 0.12, 0.34);
1605
+ return {
1606
+ h: anchor.h,
1607
+ // Red, orange, yellow, and yellow-green accents easily turn a dark
1608
+ // reading canvas into brown or olive. Preserve only a quiet trace of
1609
+ // those hues; cool blue and purple canvases can carry more color without
1610
+ // becoming visually muddy.
1611
+ s: isCoolCanvasHue(anchor.h) ? saturation : Math.min(saturation, 0.06),
1612
+ // Keep enough headroom for the renderer's high-contrast reading text.
1613
+ // A 10% HSL lightness remains comfortable while allowing white to clear
1614
+ // the default theme's 17.59:1 body-text contrast target.
1615
+ l: 0.1
1616
+ };
1617
+ }
1618
+ function isCoolCanvasHue(hue) {
1619
+ const normalized = (hue % 360 + 360) % 360;
1620
+ return normalized >= 160 && normalized <= 310;
1621
+ }
1622
+ function contrastSafeDarkCanvas(canvas) {
1623
+ const minimumWhiteContrast = 17.6;
1624
+ let lightness = canvas.l;
1625
+ while (lightness > 0.06) {
1626
+ const candidate = { ...canvas, l: lightness };
1627
+ const rgb = parseCssColor(hslToHex(candidate));
1628
+ const luminance = relativeLuminance(rgb.r, rgb.g, rgb.b);
1629
+ if (1.05 / (luminance + 0.05) >= minimumWhiteContrast) return candidate;
1630
+ lightness -= 25e-4;
1631
+ }
1632
+ return { ...canvas, l: lightness };
1633
+ }
1634
+ function relativeLuminance(red, green, blue) {
1635
+ const linear = (channel) => {
1636
+ const value = channel / 255;
1637
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
1638
+ };
1639
+ return 0.2126 * linear(red) + 0.7152 * linear(green) + 0.0722 * linear(blue);
1640
+ }
1641
+ function surfaceColor(mode, background) {
1642
+ return {
1643
+ h: background.h,
1644
+ s: background.s * (mode === "light" ? 0.65 : 0.9),
1645
+ l: Math.min(mode === "light" ? 0.995 : 0.2, background.l + (mode === "light" ? 0.015 : 0.055))
1646
+ };
1647
+ }
1648
+ function hslToHex(color) {
1649
+ const hue = (color.h % 360 + 360) % 360;
1650
+ const chroma = (1 - Math.abs(2 * color.l - 1)) * color.s;
1651
+ const segment = hue / 60;
1652
+ const x = chroma * (1 - Math.abs(segment % 2 - 1));
1653
+ const [red, green, blue] = segment < 1 ? [chroma, x, 0] : segment < 2 ? [x, chroma, 0] : segment < 3 ? [0, chroma, x] : segment < 4 ? [0, x, chroma] : segment < 5 ? [x, 0, chroma] : [chroma, 0, x];
1654
+ const match = color.l - chroma / 2;
1655
+ return colorToHex({
1656
+ r: (red + match) * 255,
1657
+ g: (green + match) * 255,
1658
+ b: (blue + match) * 255,
1659
+ a: 1
1660
+ });
1661
+ }
1662
+ function clamp(value, minimum, maximum) {
1663
+ return Math.min(maximum, Math.max(minimum, value));
1664
+ }
1665
+ function extractStyleAttributes(html) {
1666
+ const blocks = [];
1667
+ for (const match of html.matchAll(/<([a-z][\w-]*)\b[^>]*\bstyle\s*=\s*(["'])(.*?)\2[^>]*>/gis)) {
1668
+ blocks.push(`${match[1]} { ${match[3]} }`);
1669
+ }
1670
+ return blocks;
1671
+ }
1672
+ function parseRules(css) {
1673
+ const clean = css.replace(/\/\*[\s\S]*?\*\//g, "");
1674
+ const rules = [];
1675
+ for (const block of clean.split("}")) {
1676
+ const openBrace = block.lastIndexOf("{");
1677
+ if (openBrace < 0) continue;
1678
+ const previousOpenBrace = block.lastIndexOf("{", openBrace - 1);
1679
+ const selector = block.slice(previousOpenBrace + 1, openBrace).trim();
1680
+ if (selector.startsWith("@") || selector.length === 0) continue;
1681
+ const declarations = [];
1682
+ for (const part of block.slice(openBrace + 1).split(";")) {
1683
+ const declaration = /^\s*([-\w]+)\s*:\s*(.*?)\s*$/.exec(part);
1684
+ if (!declaration || declaration[2].length === 0) continue;
1685
+ declarations.push({
1686
+ property: declaration[1].toLowerCase(),
1687
+ value: declaration[2].replace(/\s*!important\s*$/i, "").trim()
1688
+ });
1689
+ }
1690
+ rules.push({ selector, declarations });
1691
+ }
1692
+ return rules;
1693
+ }
1694
+ function collectCustomProperties2(rules) {
1695
+ const properties = /* @__PURE__ */ new Map();
1696
+ for (const rule of rules) {
1697
+ if (!isRootSelector(rule.selector)) continue;
1698
+ for (const declaration of rule.declarations) {
1699
+ if (declaration.property.startsWith("--")) {
1700
+ properties.set(declaration.property, declaration.value);
1701
+ }
1702
+ }
1703
+ }
1704
+ return properties;
1705
+ }
1706
+ function declarationValue2(declarations, property, customProperties) {
1707
+ const declaration = [...declarations].reverse().find((entry) => entry.property === property);
1708
+ return declaration ? resolveCustomProperty(declaration.value, customProperties) : void 0;
1709
+ }
1710
+ function declarationColor(declarations, properties, customProperties) {
1711
+ for (const property of properties) {
1712
+ const value = declarationValue2(declarations, property, customProperties);
1713
+ const color = value ? resolveColor(value, customProperties) : void 0;
1714
+ if (color) return color;
1715
+ }
1716
+ return void 0;
1717
+ }
1718
+ function resolveCustomProperty(value, properties) {
1719
+ let resolved = value.trim();
1720
+ for (let index = 0; index < 4; index++) {
1721
+ const match = /var\(\s*(--[-\w]+)(?:\s*,\s*([^)]+))?\s*\)/.exec(resolved);
1722
+ if (!match) break;
1723
+ const replacement = properties.get(match[1]) ?? match[2];
1724
+ if (!replacement) break;
1725
+ resolved = resolved.replace(match[0], replacement.trim());
1726
+ }
1727
+ return resolved.trim();
1728
+ }
1729
+ function resolveColor(value, properties) {
1730
+ const resolved = resolveCustomProperty(value, properties);
1731
+ const literal = /#(?:[\da-f]{3}|[\da-f]{6}|[\da-f]{8})(?![\da-f])|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/i.exec(resolved)?.[0] ?? { white: "#ffffff", black: "#000000" }[resolved.toLowerCase()];
1732
+ const parsed = literal ? parseCssColor(literal) : void 0;
1733
+ return parsed ? colorToHex(parsed) : void 0;
1734
+ }
1735
+ function chooseSignal(signals) {
1736
+ if (signals.length === 0) {
1737
+ return { level: "low", reason: "No matching declaration was found." };
1738
+ }
1739
+ const grouped = /* @__PURE__ */ new Map();
1740
+ for (const signal of signals) {
1741
+ const current = grouped.get(signal.value) ?? { score: 0, count: 0, reasons: [] };
1742
+ current.score += signal.weight;
1743
+ current.count++;
1744
+ current.reasons.push(signal.reason);
1745
+ grouped.set(signal.value, current);
1746
+ }
1747
+ const ranked = [...grouped.entries()].sort(
1748
+ (left, right) => right[1].score - left[1].score || right[1].count - left[1].count
1749
+ );
1750
+ const first = ranked[0];
1751
+ const second = ranked[1];
1752
+ if (second && first[1].score === second[1].score && first[1].count === second[1].count) {
1753
+ return {
1754
+ level: "low",
1755
+ reason: `Conflicting declarations equally support ${first[0]} and ${second[0]}.`
1756
+ };
1757
+ }
1758
+ return {
1759
+ value: first[0],
1760
+ level: first[1].count > 1 || first[1].score >= 3 ? "high" : "medium",
1761
+ reason: `${first[1].reasons[0]}${first[1].count > 1 ? ` (${first[1].count} matching declarations)` : ""}.`
1762
+ };
1763
+ }
1764
+ function setModeSignal(seeds, field, mode, signal, confidence) {
1765
+ const key = `seeds.${field}.${mode}`;
1766
+ confidence[key] = { level: signal.level, reason: signal.reason };
1767
+ if (!signal.value) return;
1768
+ const pair = seeds[field] ?? {};
1769
+ pair[mode] = signal.value;
1770
+ seeds[field] = pair;
1771
+ }
1772
+ function setScalarSignal(target, field, signal, confidence, confidenceKey) {
1773
+ confidence[confidenceKey] = { level: signal.level, reason: signal.reason };
1774
+ if (signal.value) target[field] = signal.value;
1775
+ }
1776
+ function selectorMode(selector) {
1777
+ if (/(?:^|[\s.[#:_-])dark(?:$|[\s\]#.(:_-])|data-theme\s*=\s*["']?dark/i.test(selector)) {
1778
+ return "dark";
1779
+ }
1780
+ if (/(?:^|[\s.[#:_-])light(?:$|[\s\]#.(:_-])|data-theme\s*=\s*["']?light/i.test(selector)) {
1781
+ return "light";
1782
+ }
1783
+ return void 0;
1784
+ }
1785
+ function modeFromBackground(color) {
1786
+ return rgbToHsl(parseCssColor(color)).l < 0.4 ? "dark" : "light";
1787
+ }
1788
+ function modeFromText(color) {
1789
+ return rgbToHsl(parseCssColor(color)).l > 0.6 ? "dark" : "light";
1790
+ }
1791
+ function isAccentColor(color) {
1792
+ const hsl = rgbToHsl(parseCssColor(color));
1793
+ return hsl.s >= 0.18 && hsl.l >= 0.08 && hsl.l <= 0.92;
1794
+ }
1795
+ function isRootSelector(selector) {
1796
+ return selectorList(selector).some(
1797
+ (part) => /^(?::root|html)(?:(?:[.#:]|\[)\S*)?$/i.test(withoutAttributeSpaces(part))
1798
+ );
1799
+ }
1800
+ function isBodySelector(selector) {
1801
+ return selectorList(selector).some((part) => {
1802
+ const compact = withoutAttributeSpaces(part);
1803
+ const compound = String.raw`(?:(?:[.#:]|\[)\S*)?`;
1804
+ return new RegExp(`^body${compound}$`, "i").test(compact) || new RegExp(`^html${compound}\\s+(?:>\\s*)?body${compound}$`, "i").test(compact);
1805
+ });
1806
+ }
1807
+ function isRootOrBodySelector(selector) {
1808
+ return isRootSelector(selector) || isBodySelector(selector);
1809
+ }
1810
+ function selectorList(selector) {
1811
+ return selector.split(",").map((part) => part.trim()).filter(Boolean);
1812
+ }
1813
+ function withoutAttributeSpaces(selector) {
1814
+ return selector.replace(/\[[^\]]*\]/g, (attribute) => attribute.replace(/\s+/g, ""));
1815
+ }
1816
+ function isHeadingSelector(selector) {
1817
+ return /(^|[\s,>+~])h[1-6](?:\b|[.#:[>])|\.(?:heading|headline|title)\b/i.test(` ${selector}`);
1818
+ }
1819
+ function isAccentSelector(selector) {
1820
+ return /(^|[\s,>+~])a(?:\b|[.#:[>])|(^|[\s,>+~])button(?:\b|[.#:[>])|\.(?:button|btn|cta)\b|\[role\s*=\s*["']?button/i.test(` ${selector}`);
1821
+ }
1822
+ function radiusSignalWeight(selector) {
1823
+ if (/\boptgroup\b/i.test(selector)) return 0;
1824
+ if (/(?:^|[\s.#:[>_-])(?:kbd|code|pre|badge|avatar|icon|orbit|hamburger|iframe|embed|widget|navigator|spara)(?:$|[\s.#:[>_-])/i.test(` ${selector} `)) {
1825
+ return 0;
1826
+ }
1827
+ if (/(?:^|[\s,>+~])button(?:\b|[.#:[>])|\.(?:button|btn|cta)\b/i.test(` ${selector}`)) {
1828
+ return 3;
1829
+ }
1830
+ if (/\.(?:card|panel|modal|dialog)\b|(?:^|[\s,>+~])(?:input|select|textarea)(?:\b|[.#:[>])/i.test(` ${selector}`)) {
1831
+ return 2;
1832
+ }
1833
+ return 1;
1834
+ }
1835
+ function cleanFontStack(value) {
1836
+ return value.replace(/\s+/g, " ").trim();
1837
+ }
1838
+ function radiusBucket(value) {
1839
+ const match = /^(-?(?:\d+|\d*\.\d+))(px|rem|em)?(?:\s|$)/i.exec(value.trim());
1840
+ if (!match) return void 0;
1841
+ let pixels = Number(match[1]);
1842
+ if (!Number.isFinite(pixels) || pixels < 0) return void 0;
1843
+ if (match[2]?.toLowerCase() === "rem" || match[2]?.toLowerCase() === "em") {
1844
+ pixels *= 16;
1845
+ }
1846
+ if (pixels === 0) return "none";
1847
+ if (pixels <= 4) return "small";
1848
+ if (pixels <= 10) return "medium";
1849
+ return "large";
1850
+ }
1851
+ var DEFAULT_PRIMARY_COLOR = "#4F46E5";
1852
+ async function extractBrandFromUrl(inputUrl, options = {}) {
1853
+ const sourceUrl = normalizeHttpUrl(inputUrl).toString();
1854
+ const fetcher = new BrandFetcher(options);
1855
+ const warnings = [];
1856
+ const defaultPrimaryColor = options.defaultPrimaryColor ?? DEFAULT_PRIMARY_COLOR;
1857
+ let page;
1858
+ try {
1859
+ page = await fetcher.fetchText(sourceUrl, {
1860
+ accept: "text/html,application/xhtml+xml,*/*;q=0.8",
1861
+ maxBytes: DEFAULT_PAGE_MAX_BYTES
1862
+ });
1863
+ } catch (error) {
1864
+ warnings.push(`Could not fetch page ${sourceUrl}: ${messageOf22(error)}`);
1865
+ warnings.push(
1866
+ `No usable brand color found; using ${defaultPrimaryColor}.`
1867
+ );
1868
+ return {
1869
+ sourceUrl,
1870
+ primaryColor: defaultPrimaryColor,
1871
+ warnings,
1872
+ requestCount: fetcher.requestCount
1873
+ };
1874
+ }
1875
+ let pageUrl = new URL(page.url);
1876
+ let analyzedHtml = page.text;
1877
+ const colorCandidates = [];
1878
+ const fontStylesheets = [];
1879
+ for (const meta of extractThemeColorMetas(page.text)) {
1880
+ colorCandidates.push({
1881
+ value: meta.value,
1882
+ weight: 3,
1883
+ provenance: meta.media ? `meta theme-color (${meta.media})` : "meta theme-color"
1884
+ });
1885
+ }
1886
+ const manifestUrl = extractManifestUrl(page.text, pageUrl);
1887
+ if (manifestUrl) {
1888
+ const themeColor = await fetchManifestThemeColor(
1889
+ fetcher,
1890
+ manifestUrl,
1891
+ warnings
1892
+ );
1893
+ if (themeColor) {
1894
+ colorCandidates.push({
1895
+ value: themeColor,
1896
+ weight: 3,
1897
+ provenance: "web manifest theme_color"
1898
+ });
1899
+ }
1900
+ }
1901
+ for (const style of extractInlineStyleBlocks(page.text)) {
1902
+ colorCandidates.push(...extractCssColorCandidates(style, "inline style"));
1903
+ }
1904
+ const stylesheetUrls = extractStylesheetUrls(page.text, pageUrl, 5);
1905
+ for (const stylesheetUrl of stylesheetUrls) {
1906
+ try {
1907
+ const css = await fetcher.fetchText(stylesheetUrl, {
1908
+ accept: "text/css,*/*;q=0.8",
1909
+ maxBytes: DEFAULT_STYLESHEET_MAX_BYTES,
1910
+ truncate: true
1911
+ });
1912
+ if (css.truncated) {
1913
+ warnings.push(
1914
+ `stylesheet ${css.url} truncated at ${DEFAULT_STYLESHEET_MAX_BYTES} bytes`
1915
+ );
1916
+ }
1917
+ colorCandidates.push(
1918
+ ...extractCssColorCandidates(css.text, `stylesheet ${stylesheetUrl}`)
1919
+ );
1920
+ fontStylesheets.push({
1921
+ text: css.text,
1922
+ provenance: `stylesheet ${stylesheetUrl}`
1923
+ });
1924
+ } catch (error) {
1925
+ warnings.push(
1926
+ `Could not fetch stylesheet ${stylesheetUrl}: ${messageOf22(error)}`
1927
+ );
1928
+ }
1929
+ }
1930
+ const staticText = extractBrandText(page.text, pageUrl);
1931
+ const staticAssets = selectLogoAssets(page.text, pageUrl);
1932
+ const staticThemeCandidate = deriveThemeCandidate(
1933
+ page.text,
1934
+ fontStylesheets.map((stylesheet) => stylesheet.text)
1935
+ );
1936
+ const staticBestColor = pickPrimaryColor(colorCandidates);
1937
+ if (options.renderPage && needsRenderedFallback(
1938
+ staticText,
1939
+ staticAssets,
1940
+ staticBestColor,
1941
+ staticThemeCandidate
1942
+ )) {
1943
+ try {
1944
+ const rendered = validateRenderedPage(
1945
+ await options.renderPage(page.url, {
1946
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
1947
+ })
1948
+ );
1949
+ if (rendered.finalUrl) pageUrl = normalizeHttpUrl(rendered.finalUrl);
1950
+ analyzedHtml = rendered.html;
1951
+ for (const [index, stylesheet] of (rendered.stylesheets ?? []).entries()) {
1952
+ colorCandidates.push(
1953
+ ...extractCssColorCandidates(
1954
+ stylesheet,
1955
+ `rendered stylesheet ${index + 1}`
1956
+ )
1957
+ );
1958
+ fontStylesheets.push({
1959
+ text: stylesheet,
1960
+ provenance: `rendered stylesheet ${index + 1}`
1961
+ });
1962
+ }
1963
+ } catch (error) {
1964
+ warnings.push(`Rendered brand fallback failed: ${messageOf22(error)}`);
1965
+ }
1966
+ }
1967
+ const text = extractBrandText(analyzedHtml, pageUrl);
1968
+ const themeCandidate = deriveThemeCandidate(
1969
+ analyzedHtml,
1970
+ fontStylesheets.map((stylesheet) => stylesheet.text)
1971
+ );
1972
+ const semanticAccent = highConfidenceAccent(themeCandidate);
1973
+ const bestColor = pickPrimaryColor(colorCandidates);
1974
+ const primaryColor = semanticAccent ?? bestColor?.hex ?? defaultPrimaryColor;
1975
+ if (!bestColor && !semanticAccent) {
1976
+ warnings.push(`No usable brand color found; using ${defaultPrimaryColor}.`);
1977
+ }
1978
+ const assets = selectLogoAssets(analyzedHtml, pageUrl);
1979
+ const logo = assets.logo ? await maybeFetchAsset(fetcher, assets.logo, "logo", warnings) : void 0;
1980
+ const favicon = assets.favicon ? await maybeFetchAsset(fetcher, assets.favicon, "favicon", warnings) : void 0;
1981
+ const fonts = await detectFonts({
1982
+ html: analyzedHtml,
1983
+ stylesheets: fontStylesheets,
1984
+ fetcher,
1985
+ warnings
1986
+ });
1987
+ return {
1988
+ sourceUrl,
1989
+ finalUrl: pageUrl.toString(),
1990
+ name: text.name,
1991
+ description: text.description,
1992
+ primaryColor,
1993
+ logo,
1994
+ favicon,
1995
+ fonts,
1996
+ themeCandidate,
1997
+ warnings,
1998
+ requestCount: fetcher.requestCount
1999
+ };
2000
+ }
2001
+ function highConfidenceAccent(theme) {
2002
+ for (const mode of ["light", "dark"]) {
2003
+ if (theme.confidence[`seeds.accent.${mode}`]?.level === "high") {
2004
+ const value = theme.seeds.accent?.[mode];
2005
+ if (value) return value;
2006
+ }
2007
+ }
2008
+ return void 0;
2009
+ }
2010
+ var MAX_RENDERED_HTML_CHARS = 2 * 1024 * 1024;
2011
+ var MAX_RENDERED_STYLESHEET_CHARS = 256 * 1024;
2012
+ var MAX_RENDERED_STYLESHEETS = 4;
2013
+ function needsRenderedFallback(text, assets, primary, theme) {
2014
+ return !text.name || !assets.logo || !primary || !hasHighConfidence(
2015
+ theme,
2016
+ "seeds.background.light",
2017
+ "seeds.background.dark"
2018
+ ) || !hasHighConfidence(theme, "seeds.text.light", "seeds.text.dark");
2019
+ }
2020
+ function hasHighConfidence(theme, ...fields) {
2021
+ return fields.some((field) => theme.confidence[field]?.level === "high");
2022
+ }
2023
+ function validateRenderedPage(page) {
2024
+ if (!page || typeof page.html !== "string" || page.html.length === 0) {
2025
+ throw new Error("provider returned no rendered HTML");
2026
+ }
2027
+ if (page.html.length > MAX_RENDERED_HTML_CHARS) {
2028
+ throw new Error(
2029
+ `rendered HTML exceeded ${MAX_RENDERED_HTML_CHARS} characters`
2030
+ );
2031
+ }
2032
+ const stylesheets = page.stylesheets ?? [];
2033
+ if (stylesheets.length > MAX_RENDERED_STYLESHEETS) {
2034
+ throw new Error(
2035
+ `provider returned more than ${MAX_RENDERED_STYLESHEETS} rendered stylesheets`
2036
+ );
2037
+ }
2038
+ if (stylesheets.some(
2039
+ (stylesheet) => typeof stylesheet !== "string" || stylesheet.length > MAX_RENDERED_STYLESHEET_CHARS
2040
+ )) {
2041
+ throw new Error(
2042
+ `rendered stylesheet exceeded ${MAX_RENDERED_STYLESHEET_CHARS} characters`
2043
+ );
2044
+ }
2045
+ if (page.finalUrl) normalizeHttpUrl(page.finalUrl);
2046
+ return page;
2047
+ }
2048
+ function deriveDocsJson(extraction, overrides = {}) {
2049
+ const name = cleanOption(overrides.name) ?? cleanOption(extraction.name) ?? "docs";
2050
+ const primary = cleanOption(overrides.primary) ?? cleanOption(extraction.primaryColor) ?? DEFAULT_PRIMARY_COLOR;
2051
+ const description = cleanOption(extraction.description) ?? `Documentation for ${name}.`;
2052
+ const config = {
2053
+ name,
2054
+ description,
2055
+ background: "linear-wash",
2056
+ colors: {
2057
+ primary
2058
+ },
2059
+ navigation: {
2060
+ groups: [
2061
+ {
2062
+ group: "Getting started",
2063
+ pages: ["index", "quickstart"]
2064
+ }
2065
+ ]
2066
+ }
2067
+ };
2068
+ if (extraction.themeCandidate) {
2069
+ config.theme = deriveThemeRecipe(primary, extraction.themeCandidate);
2070
+ }
2071
+ const logoPath = assetDocsPath(extraction.logo);
2072
+ const faviconPath = assetDocsPath(extraction.favicon);
2073
+ if (logoPath) config.logo = logoPath;
2074
+ if (faviconPath) config.favicon = faviconPath;
2075
+ const fonts = docsFonts(extraction.fonts);
2076
+ if (fonts) config.fonts = fonts;
2077
+ return config;
2078
+ }
2079
+ function docsFonts(extraction) {
2080
+ if (!extraction) return void 0;
2081
+ const fonts = {};
2082
+ if (extraction.body) {
2083
+ fonts.body = docsFont(extraction.body);
2084
+ fonts.heading = docsFont(
2085
+ extraction.heading?.family === extraction.body.family ? extraction.heading : extraction.body
2086
+ );
2087
+ } else if (extraction.heading) {
2088
+ fonts.heading = docsFont(extraction.heading);
2089
+ }
2090
+ if (extraction.mono) fonts.mono = docsFont(extraction.mono);
2091
+ return Object.keys(fonts).length > 0 ? fonts : void 0;
2092
+ }
2093
+ function docsFont(font) {
2094
+ return {
2095
+ family: font.family,
2096
+ source: font.source,
2097
+ ...font.weights ? { weights: font.weights } : {}
2098
+ };
2099
+ }
2100
+ async function fetchManifestThemeColor(fetcher, manifestUrl, warnings) {
2101
+ try {
2102
+ const manifest = await fetcher.fetchText(manifestUrl, {
2103
+ accept: "application/manifest+json,application/json,text/json,*/*;q=0.8",
2104
+ maxBytes: DEFAULT_AUX_MAX_BYTES
2105
+ });
2106
+ const parsed = JSON.parse(manifest.text);
2107
+ return typeof parsed.theme_color === "string" ? parsed.theme_color : void 0;
2108
+ } catch (error) {
2109
+ warnings.push(`Could not fetch manifest ${manifestUrl}: ${messageOf22(error)}`);
2110
+ return void 0;
2111
+ }
2112
+ }
2113
+ async function maybeFetchAsset(fetcher, candidate, kind, warnings) {
2114
+ try {
2115
+ const downloaded = await fetchBrandAsset(fetcher, candidate.url, kind);
2116
+ return { ...downloaded, source: candidate.source };
2117
+ } catch (error) {
2118
+ warnings.push(
2119
+ `Could not download ${kind} ${candidate.url}: ${messageOf22(error)}`
2120
+ );
2121
+ return void 0;
2122
+ }
2123
+ }
2124
+ function assetDocsPath(asset) {
2125
+ return asset ? `/${asset.suggestedFilename}` : void 0;
2126
+ }
2127
+ function cleanOption(value) {
2128
+ const cleaned = value?.trim();
2129
+ return cleaned && cleaned.length > 0 ? cleaned : void 0;
2130
+ }
2131
+ function messageOf22(error) {
2132
+ return error instanceof Error ? error.message : String(error);
2133
+ }
2134
+
2135
+ // src/commands/init.ts
2136
+ import pc from "picocolors";
2137
+ function registerInit(program) {
2138
+ program.command("init").description("Scaffold a new SaturnDocs docs project").argument("[dir]", "directory to scaffold", ".").option("--name <name>", "site name").option("--primary <css-color>", "primary brand color").option("--from-url <url>", "extract brand details from a website").option("--yes", "accepted for future interactive prompts").action(async (dir, options) => {
2139
+ try {
2140
+ const result = await scaffoldDocsProject(dir, options);
2141
+ if (result.extraction) printExtractionReport(result);
2142
+ printNextSteps(result.docsDir);
2143
+ } catch (error) {
2144
+ console.error(pc.red(`error: ${messageOf(error)}`));
2145
+ process.exit(1);
2146
+ }
2147
+ });
2148
+ }
2149
+ async function scaffoldDocsProject(dir = ".", options = {}, dependencies = {}) {
2150
+ const docsDir = resolve(dir);
2151
+ const docsJsonPath = join(docsDir, "docs.json");
2152
+ if (existsSync(docsJsonPath)) {
2153
+ throw new Error(`docs.json already exists in ${docsDir}`);
2154
+ }
2155
+ await mkdir(docsDir, { recursive: true });
2156
+ const extractBrand = dependencies.extractBrand ?? extractBrandFromUrl;
2157
+ const extraction = options.fromUrl ? await extractBrand(options.fromUrl) : void 0;
2158
+ if (extraction) await writeExtractedAssets(docsDir, extraction);
2159
+ const defaultName = basename(docsDir) || "docs";
2160
+ const name = cleanOption2(options.name) ?? extraction?.name ?? defaultName;
2161
+ const primary = cleanOption2(options.primary) ?? extraction?.primaryColor ?? DEFAULT_PRIMARY_COLOR;
2162
+ const config = deriveDocsJson(
2163
+ extraction ?? { primaryColor: DEFAULT_PRIMARY_COLOR },
2164
+ {
2165
+ name,
2166
+ primary
2167
+ }
2168
+ );
2169
+ config.fonts = extraction ? mergeExtractedFonts(extraction) : starterFonts();
2170
+ await writeScaffoldFiles(docsDir, config);
2171
+ return { docsDir, config: await loadDocsConfig(docsJsonPath), extraction };
2172
+ }
2173
+ function buildDocsConfig(input) {
2174
+ const config = deriveDocsJson(
2175
+ {
2176
+ name: input.name,
2177
+ description: input.description,
2178
+ primaryColor: input.primary
2179
+ },
2180
+ {
2181
+ name: input.name,
2182
+ primary: input.primary
2183
+ }
2184
+ );
2185
+ config.fonts = starterFonts();
2186
+ if (input.logo) config.logo = input.logo;
2187
+ if (input.favicon) config.favicon = input.favicon;
2188
+ return config;
2189
+ }
2190
+ function starterFonts() {
2191
+ return {
2192
+ body: { family: "Instrument Sans", weights: [400, 500, 600] },
2193
+ heading: { family: "Bricolage Grotesque", weights: [500, 600, 700] },
2194
+ mono: { family: "IBM Plex Mono", weights: [400, 500] }
2195
+ };
2196
+ }
2197
+ function mergeExtractedFonts(extraction) {
2198
+ const fonts = starterFonts();
2199
+ const body = extraction.fonts?.body;
2200
+ const heading = extraction.fonts?.heading;
2201
+ if (body) {
2202
+ fonts.body = extractedFont(body);
2203
+ fonts.heading = extractedFont(
2204
+ heading?.family === body.family ? heading : body
2205
+ );
2206
+ } else if (heading) {
2207
+ fonts.heading = extractedFont(heading);
2208
+ }
2209
+ if (extraction.fonts?.mono) fonts.mono = extractedFont(extraction.fonts.mono);
2210
+ return fonts;
2211
+ }
2212
+ function extractedFont(detected) {
2213
+ return {
2214
+ family: detected.family,
2215
+ source: detected.source,
2216
+ ...detected.weights ? { weights: detected.weights } : {}
2217
+ };
2218
+ }
2219
+ var THEME_SEED_FIELDS = [
2220
+ ["background", "light"],
2221
+ ["background", "dark"],
2222
+ ["text", "light"],
2223
+ ["text", "dark"],
2224
+ ["accent", "light"],
2225
+ ["accent", "dark"]
2226
+ ];
2227
+ var THEME_CONFIDENCE_FIELDS = [
2228
+ ...THEME_SEED_FIELDS.map(([field, mode]) => `seeds.${field}.${mode}`),
2229
+ "radius"
2230
+ ];
2231
+ function cleanOption2(value) {
2232
+ const cleaned = value?.trim();
2233
+ return cleaned && cleaned.length > 0 ? cleaned : void 0;
2234
+ }
2235
+ async function writeScaffoldFiles(docsDir, config) {
2236
+ const pagesDir = join(docsDir, "pages");
2237
+ await mkdir(pagesDir, { recursive: true });
2238
+ await writeFile(
2239
+ join(docsDir, "docs.json"),
2240
+ `${JSON.stringify(config, null, 2)}
2241
+ `,
2242
+ "utf8"
2243
+ );
2244
+ await writeFile(
2245
+ join(pagesDir, "index.mdx"),
2246
+ indexPageMdx(config.name, config.description ?? ""),
2247
+ "utf8"
2248
+ );
2249
+ await writeFile(
2250
+ join(pagesDir, "quickstart.mdx"),
2251
+ quickstartPageMdx(config.name),
2252
+ "utf8"
2253
+ );
2254
+ }
2255
+ async function writeExtractedAssets(docsDir, extraction) {
2256
+ const assets = [extraction.logo, extraction.favicon].filter(
2257
+ (asset) => asset !== void 0
2258
+ );
2259
+ if (assets.length === 0) return;
2260
+ const publicDir = join(docsDir, "public");
2261
+ await mkdir(publicDir, { recursive: true });
2262
+ for (const asset of assets) {
2263
+ await writeFile(join(publicDir, asset.suggestedFilename), asset.bytes);
2264
+ }
2265
+ }
2266
+ function indexPageMdx(name, description) {
2267
+ return `---
2268
+ title: ${yamlString(`Welcome to ${name}`)}
2269
+ description: ${yamlString(description)}
2270
+ ---
2271
+
2272
+ <Note>
2273
+ This site was scaffolded with SaturnDocs. Edit \`docs.json\` to update site metadata and navigation.
2274
+ </Note>
2275
+
2276
+ ## Start here
2277
+
2278
+ <Columns cols={2}>
2279
+ <Card title="Write pages" icon="file-text">
2280
+ Add MDX files under \`pages/\`, then list them in \`docs.json\`.
2281
+ </Card>
2282
+ <Card title="Preview locally" icon="monitor">
2283
+ Run \`saturndocs dev\` to open the docs site while you edit.
2284
+ </Card>
2285
+ </Columns>
2286
+ `;
2287
+ }
2288
+ function quickstartPageMdx(name) {
2289
+ return `---
2290
+ title: Quickstart
2291
+ description: ${yamlString(`Start editing ${name}.`)}
2292
+ ---
2293
+
2294
+ ## Run the site
2295
+
2296
+ \`\`\`sh
2297
+ saturndocs dev
2298
+ \`\`\`
2299
+
2300
+ ## Edit content
2301
+
2302
+ Update \`pages/index.mdx\`, then add more files under \`pages/\`. Every page that should appear in the sidebar must be listed in \`docs.json\`.
2303
+
2304
+ \`\`\`json title="docs.json"
2305
+ {
2306
+ "navigation": {
2307
+ "groups": [
2308
+ { "group": "Getting started", "pages": ["index", "quickstart"] }
2309
+ ]
2310
+ }
2311
+ }
2312
+ \`\`\`
2313
+
2314
+ ## Use built-in components
2315
+
2316
+ \`\`\`mdx
2317
+ <Note>
2318
+ Use notes for short callouts.
2319
+ </Note>
2320
+
2321
+ <Steps>
2322
+ <Step title="Create a page">
2323
+ Save a new MDX file in the pages directory.
2324
+ </Step>
2325
+ <Step title="Add it to navigation">
2326
+ Add the page slug to docs.json.
2327
+ </Step>
2328
+ </Steps>
2329
+ \`\`\`
2330
+ `;
2331
+ }
2332
+ function yamlString(value) {
2333
+ return JSON.stringify(value);
2334
+ }
2335
+ function printExtractionReport(result) {
2336
+ const extraction = result.extraction;
2337
+ if (!extraction) return;
2338
+ console.log(pc.bold("\nBrand extraction"));
2339
+ console.log(` Name: ${result.config.name}`);
2340
+ console.log(
2341
+ ` Description: ${truncate(result.config.description ?? "not found", 120)}`
2342
+ );
2343
+ console.log(
2344
+ ` Primary: ${result.config.colors.primary} ${colorSwatch(
2345
+ result.config.colors.primary
2346
+ )}`.trimEnd()
2347
+ );
2348
+ console.log(` Logo: ${result.config.logo ?? "not found"}`);
2349
+ console.log(` Favicon: ${result.config.favicon ?? "not found"}`);
2350
+ const detectedFonts = extraction.fonts;
2351
+ if (!detectedFonts || Object.keys(detectedFonts).length === 0) {
2352
+ console.log(" Fonts: not detected");
2353
+ } else {
2354
+ for (const slot of ["body", "heading", "mono"]) {
2355
+ const font = detectedFonts[slot];
2356
+ if (!font) continue;
2357
+ console.log(` Fonts: ${slot}: ${font.family} (${font.source})`);
2358
+ }
2359
+ }
2360
+ printThemeReport(extraction.themeCandidate, result.config.theme);
2361
+ if (extraction.warnings.length > 0) {
2362
+ console.log(pc.yellow(" Warnings:"));
2363
+ for (const warning of extraction.warnings) {
2364
+ console.log(pc.yellow(` - ${warning}`));
2365
+ }
2366
+ }
2367
+ console.log(" Review docs.json \u2014 extraction is a best guess.");
2368
+ }
2369
+ function printThemeReport(candidate, theme) {
2370
+ if (!candidate || !theme) {
2371
+ console.log(" Theme: not detected");
2372
+ return;
2373
+ }
2374
+ console.log(" Theme:");
2375
+ if (typeof theme !== "string") {
2376
+ for (const field of ["background", "surface"]) {
2377
+ for (const mode of ["light", "dark"]) {
2378
+ const value = theme.seeds?.[field]?.[mode];
2379
+ if (value) console.log(` generated seeds.${field}.${mode}: ${value}`);
2380
+ }
2381
+ }
2382
+ }
2383
+ for (const [field, mode] of THEME_SEED_FIELDS) {
2384
+ const path = `seeds.${field}.${mode}`;
2385
+ const value = candidate.seeds[field]?.[mode];
2386
+ const confidence = candidate.confidence[path];
2387
+ if (!value || confidence?.level !== "high") continue;
2388
+ console.log(` evidence ${path}: ${value} (${confidence.level})`);
2389
+ console.log(` reason: ${confidence.reason}`);
2390
+ }
2391
+ const radiusConfidence = candidate.confidence.radius;
2392
+ if (candidate.radius && radiusConfidence?.level === "high") {
2393
+ console.log(` evidence radius: ${candidate.radius} (${radiusConfidence.level})`);
2394
+ console.log(` reason: ${radiusConfidence.reason}`);
2395
+ }
2396
+ const omitted = THEME_CONFIDENCE_FIELDS.filter(
2397
+ (field) => {
2398
+ const level = candidate.confidence[field]?.level;
2399
+ return level === "low" || level === "medium";
2400
+ }
2401
+ );
2402
+ if (omitted.length > 0) {
2403
+ console.log(` omitted (not high confidence): ${omitted.join(", ")}`);
2404
+ }
2405
+ }
2406
+ function printNextSteps(docsDir) {
2407
+ console.log(pc.green(`
2408
+ Created SaturnDocs docs in ${docsDir}`));
2409
+ console.log(pc.bold("Next steps"));
2410
+ const rel = relative(process.cwd(), docsDir);
2411
+ const cdTarget = !rel || rel === "." ? "" : rel.startsWith("..") ? docsDir : rel;
2412
+ if (cdTarget) {
2413
+ console.log(` ${pc.cyan(`cd ${cdTarget}`)}`);
2414
+ }
2415
+ console.log(` ${pc.cyan("saturndocs dev")}`);
2416
+ console.log(" Edit docs.json to adjust navigation and site metadata.");
2417
+ console.log(" Edit pages/index.mdx and pages/quickstart.mdx to start writing.");
2418
+ }
2419
+ function truncate(value, maxLength) {
2420
+ if (value.length <= maxLength) return value;
2421
+ return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
2422
+ }
2423
+ function colorSwatch(value) {
2424
+ const color = parseCssColor(value);
2425
+ if (!color) return "";
2426
+ return `\x1B[48;2;${color.r};${color.g};${color.b}m \x1B[0m`;
2427
+ }
2428
+
2429
+ export {
2430
+ BrandFetcher,
2431
+ normalizeHttpUrl,
2432
+ parseCssColor,
2433
+ colorToHex,
2434
+ normalizeHexColor,
2435
+ scoreColorCandidates,
2436
+ pickPrimaryColor,
2437
+ extractCssColorCandidates,
2438
+ extractBrandText,
2439
+ extractThemeColorMetas,
2440
+ extractManifestUrl,
2441
+ extractStylesheetUrls,
2442
+ selectLogoAssets,
2443
+ fetchBrandAsset,
2444
+ extensionForContentType,
2445
+ DEFAULT_PRIMARY_COLOR,
2446
+ extractBrandFromUrl,
2447
+ deriveDocsJson,
2448
+ registerInit,
2449
+ scaffoldDocsProject,
2450
+ buildDocsConfig,
2451
+ printExtractionReport
2452
+ };