fontfetch 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -1
- package/README.md +87 -19
- package/dist/cli.js +995 -206
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -126892,7 +126892,7 @@ var require_decompress = __commonJS({
|
|
|
126892
126892
|
|
|
126893
126893
|
// ../core/src/pipeline/pull.ts
|
|
126894
126894
|
import fs4 from "fs/promises";
|
|
126895
|
-
import
|
|
126895
|
+
import path6 from "path";
|
|
126896
126896
|
|
|
126897
126897
|
// ../core/src/lib/utils.ts
|
|
126898
126898
|
import path from "path";
|
|
@@ -127103,6 +127103,67 @@ function buildReadme(host, faces, totalFiles, orphans = []) {
|
|
|
127103
127103
|
lines.push("");
|
|
127104
127104
|
return lines.join("\n");
|
|
127105
127105
|
}
|
|
127106
|
+
function buildProvenanceJson(host, sourceUrl, classified, orphans, fileSizes) {
|
|
127107
|
+
const faces = classified.map((c) => ({
|
|
127108
|
+
family: c.face.family,
|
|
127109
|
+
weight: c.face.weight,
|
|
127110
|
+
style: c.face.style,
|
|
127111
|
+
unicodeRange: c.face.unicodeRange,
|
|
127112
|
+
classification: {
|
|
127113
|
+
status: c.classification.status,
|
|
127114
|
+
reason: c.classification.reason,
|
|
127115
|
+
...c.classification.hasRFN ? { hasRFN: true } : {}
|
|
127116
|
+
},
|
|
127117
|
+
files: c.face.sources.filter((s) => Boolean(s.localFile)).map((s) => {
|
|
127118
|
+
const filePath = `files/${s.localFile}`;
|
|
127119
|
+
const bucket = s.localFile.includes("/") ? s.localFile.split("/")[0] : "self-hosted";
|
|
127120
|
+
return {
|
|
127121
|
+
file: filePath,
|
|
127122
|
+
bucket,
|
|
127123
|
+
url: s.url,
|
|
127124
|
+
format: s.format,
|
|
127125
|
+
bytes: fileSizes.get(s.localFile) ?? null
|
|
127126
|
+
};
|
|
127127
|
+
})
|
|
127128
|
+
}));
|
|
127129
|
+
const familiesWithRFN = [
|
|
127130
|
+
...new Set(classified.filter((c) => c.classification.hasRFN).map((c) => c.face.family))
|
|
127131
|
+
];
|
|
127132
|
+
const byStatus = { open: 0, commercial: 0, unknown: 0 };
|
|
127133
|
+
for (const c of classified) byStatus[c.classification.status]++;
|
|
127134
|
+
const byBucket = {};
|
|
127135
|
+
let totalBytes = 0;
|
|
127136
|
+
let totalFiles = 0;
|
|
127137
|
+
for (const f of faces) {
|
|
127138
|
+
for (const file of f.files) {
|
|
127139
|
+
byBucket[file.bucket] = (byBucket[file.bucket] ?? 0) + 1;
|
|
127140
|
+
totalFiles++;
|
|
127141
|
+
if (file.bytes) totalBytes += file.bytes;
|
|
127142
|
+
}
|
|
127143
|
+
}
|
|
127144
|
+
const report = {
|
|
127145
|
+
schemaVersion: "1.0",
|
|
127146
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
127147
|
+
host,
|
|
127148
|
+
sourceUrl,
|
|
127149
|
+
summary: {
|
|
127150
|
+
families: new Set(classified.map((c) => c.face.family)).size,
|
|
127151
|
+
faces: classified.length,
|
|
127152
|
+
files: totalFiles,
|
|
127153
|
+
bytes: totalBytes,
|
|
127154
|
+
byStatus,
|
|
127155
|
+
byBucket,
|
|
127156
|
+
familiesWithRFN
|
|
127157
|
+
},
|
|
127158
|
+
faces,
|
|
127159
|
+
orphans: orphans.map((o) => ({
|
|
127160
|
+
url: o.url,
|
|
127161
|
+
file: `files/${o.file}`,
|
|
127162
|
+
bytes: fileSizes.get(o.file) ?? null
|
|
127163
|
+
}))
|
|
127164
|
+
};
|
|
127165
|
+
return JSON.stringify(report, null, 2);
|
|
127166
|
+
}
|
|
127106
127167
|
function buildLicenseReview(host, classified, summary) {
|
|
127107
127168
|
const lines = [
|
|
127108
127169
|
`# License review for ${host}`,
|
|
@@ -127167,6 +127228,178 @@ function buildLicenseReview(host, classified, summary) {
|
|
|
127167
127228
|
return lines.join("\n");
|
|
127168
127229
|
}
|
|
127169
127230
|
|
|
127231
|
+
// ../core/src/license/provenance.ts
|
|
127232
|
+
var RULES = [
|
|
127233
|
+
{
|
|
127234
|
+
bucket: "google",
|
|
127235
|
+
patterns: ["fonts.gstatic.com", "fonts.googleapis.com", "cdn.jsdelivr.net/gh/google/fonts"]
|
|
127236
|
+
},
|
|
127237
|
+
{
|
|
127238
|
+
bucket: "adobe-typekit",
|
|
127239
|
+
patterns: ["use.typekit.net", "p.typekit.net", "fonts.adobe.com"]
|
|
127240
|
+
},
|
|
127241
|
+
{
|
|
127242
|
+
bucket: "commercial",
|
|
127243
|
+
patterns: [
|
|
127244
|
+
"fast.fonts.net",
|
|
127245
|
+
"cloud.typography.com",
|
|
127246
|
+
"cloud.typenetwork.com",
|
|
127247
|
+
"use.fontawesome.com",
|
|
127248
|
+
"fontstand.com"
|
|
127249
|
+
]
|
|
127250
|
+
},
|
|
127251
|
+
{
|
|
127252
|
+
bucket: "open-cdn",
|
|
127253
|
+
patterns: ["cdn.jsdelivr.net/npm/@fontsource/", "rsms.me"]
|
|
127254
|
+
}
|
|
127255
|
+
];
|
|
127256
|
+
function strip(host) {
|
|
127257
|
+
return host.replace(/^www\./i, "").toLowerCase();
|
|
127258
|
+
}
|
|
127259
|
+
function sameOrigin(urlHost, pageHost) {
|
|
127260
|
+
const a = strip(urlHost);
|
|
127261
|
+
const b = strip(pageHost);
|
|
127262
|
+
if (a === b) return true;
|
|
127263
|
+
return a.endsWith("." + b) || b.endsWith("." + a);
|
|
127264
|
+
}
|
|
127265
|
+
function bucketForUrl(url, pageHost) {
|
|
127266
|
+
for (const rule of RULES) {
|
|
127267
|
+
for (const pattern of rule.patterns) {
|
|
127268
|
+
if (url.includes(pattern)) return rule.bucket;
|
|
127269
|
+
}
|
|
127270
|
+
}
|
|
127271
|
+
try {
|
|
127272
|
+
const u = new URL(url);
|
|
127273
|
+
if (sameOrigin(u.hostname, pageHost)) return "self-hosted";
|
|
127274
|
+
} catch {
|
|
127275
|
+
}
|
|
127276
|
+
return "self-hosted";
|
|
127277
|
+
}
|
|
127278
|
+
|
|
127279
|
+
// ../core/src/emit/gdpr.ts
|
|
127280
|
+
var REMEDIATION_BY_BUCKET = {
|
|
127281
|
+
google: {
|
|
127282
|
+
severity: "high",
|
|
127283
|
+
hint: "Self-host the Google Fonts CDN binaries. Use fontfetch to extract them, then serve from your own origin. Avoid bunny.net Fonts only if your DPA also covers Bunny."
|
|
127284
|
+
},
|
|
127285
|
+
"adobe-typekit": {
|
|
127286
|
+
severity: "high",
|
|
127287
|
+
hint: "Adobe Fonts requires an active licence. You cannot self-host the binaries under the standard agreement \u2014 you must remove the family OR negotiate a bespoke licence that allows self-hosting."
|
|
127288
|
+
},
|
|
127289
|
+
commercial: {
|
|
127290
|
+
severity: "high",
|
|
127291
|
+
hint: "Commercial foundry CDN. Check the licence for self-host rights, then download the binaries directly from the foundry portal (not via this CDN)."
|
|
127292
|
+
},
|
|
127293
|
+
"open-cdn": {
|
|
127294
|
+
severity: "medium",
|
|
127295
|
+
hint: "Open CDN (Fontsource, rsms.me, jsdelivr). Self-host the binaries OR use Bunny Fonts as a GDPR-compliant proxy. Fontsource also publishes per-family npm packages."
|
|
127296
|
+
},
|
|
127297
|
+
"self-hosted": {
|
|
127298
|
+
severity: "low",
|
|
127299
|
+
hint: "Already on the same origin. No third-party request; nothing to remediate."
|
|
127300
|
+
}
|
|
127301
|
+
};
|
|
127302
|
+
function buildGdprReport(host, sourceUrl, faces) {
|
|
127303
|
+
const findings = [];
|
|
127304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
127305
|
+
const pageHost = (() => {
|
|
127306
|
+
try {
|
|
127307
|
+
return new URL(sourceUrl).hostname;
|
|
127308
|
+
} catch {
|
|
127309
|
+
return host;
|
|
127310
|
+
}
|
|
127311
|
+
})();
|
|
127312
|
+
for (const f of faces) {
|
|
127313
|
+
for (const s of f.sources) {
|
|
127314
|
+
const bucket = bucketForUrl(s.url, pageHost);
|
|
127315
|
+
const key = `${f.family}|${bucket}|${s.url}`;
|
|
127316
|
+
if (seen.has(key)) continue;
|
|
127317
|
+
seen.add(key);
|
|
127318
|
+
const meta = REMEDIATION_BY_BUCKET[bucket];
|
|
127319
|
+
findings.push({
|
|
127320
|
+
family: f.family,
|
|
127321
|
+
bucket,
|
|
127322
|
+
url: s.url,
|
|
127323
|
+
severity: meta.severity,
|
|
127324
|
+
remediation: meta.hint
|
|
127325
|
+
});
|
|
127326
|
+
}
|
|
127327
|
+
}
|
|
127328
|
+
const thirdParty = findings.filter((f) => f.bucket !== "self-hosted").length;
|
|
127329
|
+
const selfHosted = findings.filter((f) => f.bucket === "self-hosted").length;
|
|
127330
|
+
const highSeverity = findings.filter((f) => f.severity === "high").length;
|
|
127331
|
+
return {
|
|
127332
|
+
schemaVersion: "1.0",
|
|
127333
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
127334
|
+
host,
|
|
127335
|
+
sourceUrl,
|
|
127336
|
+
findings,
|
|
127337
|
+
summary: { thirdParty, selfHosted, highSeverity }
|
|
127338
|
+
};
|
|
127339
|
+
}
|
|
127340
|
+
function formatGdprMarkdown(report) {
|
|
127341
|
+
const lines = [
|
|
127342
|
+
`# GDPR review for ${report.host}`,
|
|
127343
|
+
"",
|
|
127344
|
+
"> Heuristic-only. Not legal advice. Built on the post-LG M\xFCnchen I 20 O 1393/21 understanding that cross-origin font loading from a third-party CDN exposes the visitor's IP address to a controller the site operator has no DPA with.",
|
|
127345
|
+
"",
|
|
127346
|
+
"## Summary",
|
|
127347
|
+
"",
|
|
127348
|
+
`- ${report.summary.thirdParty} third-party font request(s)`,
|
|
127349
|
+
`- ${report.summary.selfHosted} self-hosted request(s)`,
|
|
127350
|
+
`- ${report.summary.highSeverity} high-severity finding(s)`,
|
|
127351
|
+
""
|
|
127352
|
+
];
|
|
127353
|
+
if (report.summary.thirdParty === 0) {
|
|
127354
|
+
lines.push("\u2713 No third-party font requests detected. Fonts are served from the same origin.");
|
|
127355
|
+
return lines.join("\n");
|
|
127356
|
+
}
|
|
127357
|
+
lines.push("## Findings");
|
|
127358
|
+
lines.push("");
|
|
127359
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
127360
|
+
for (const f of report.findings) {
|
|
127361
|
+
if (f.bucket === "self-hosted") continue;
|
|
127362
|
+
const list = byBucket.get(f.bucket) ?? [];
|
|
127363
|
+
list.push(f);
|
|
127364
|
+
byBucket.set(f.bucket, list);
|
|
127365
|
+
}
|
|
127366
|
+
const severityIcon = {
|
|
127367
|
+
high: "\u{1F534}",
|
|
127368
|
+
medium: "\u{1F7E1}",
|
|
127369
|
+
low: "\u{1F7E2}"
|
|
127370
|
+
};
|
|
127371
|
+
for (const [bucket, list] of byBucket) {
|
|
127372
|
+
lines.push(`### ${bucketLabel(bucket)}`);
|
|
127373
|
+
lines.push("");
|
|
127374
|
+
const families = /* @__PURE__ */ new Map();
|
|
127375
|
+
for (const f of list) {
|
|
127376
|
+
const arr = families.get(f.family) ?? [];
|
|
127377
|
+
arr.push(f);
|
|
127378
|
+
families.set(f.family, arr);
|
|
127379
|
+
}
|
|
127380
|
+
for (const [family, occurrences] of families) {
|
|
127381
|
+
const sev = occurrences[0].severity;
|
|
127382
|
+
lines.push(`- ${severityIcon[sev]} **${family}** \u2014 ${occurrences[0].remediation}`);
|
|
127383
|
+
}
|
|
127384
|
+
lines.push("");
|
|
127385
|
+
}
|
|
127386
|
+
return lines.join("\n");
|
|
127387
|
+
}
|
|
127388
|
+
function bucketLabel(b) {
|
|
127389
|
+
switch (b) {
|
|
127390
|
+
case "google":
|
|
127391
|
+
return "Google Fonts CDN (high risk \u2014 German court precedent)";
|
|
127392
|
+
case "adobe-typekit":
|
|
127393
|
+
return "Adobe Fonts / Typekit";
|
|
127394
|
+
case "commercial":
|
|
127395
|
+
return "Commercial foundry CDN";
|
|
127396
|
+
case "open-cdn":
|
|
127397
|
+
return "Open-licence CDN";
|
|
127398
|
+
case "self-hosted":
|
|
127399
|
+
return "Self-hosted";
|
|
127400
|
+
}
|
|
127401
|
+
}
|
|
127402
|
+
|
|
127170
127403
|
// ../core/src/license/license-data.ts
|
|
127171
127404
|
var OPEN_HOSTS = [
|
|
127172
127405
|
{ host: "fonts.gstatic.com", label: "Google Fonts CDN" },
|
|
@@ -137897,16 +138130,16 @@ var $f43aec954cdfdf21$export$2e2bcd8739ae039 = class _$f43aec954cdfdf21$export$2
|
|
|
137897
138130
|
* @return {Path}
|
|
137898
138131
|
*/
|
|
137899
138132
|
mapPoints(fn) {
|
|
137900
|
-
let
|
|
138133
|
+
let path8 = new _$f43aec954cdfdf21$export$2e2bcd8739ae039();
|
|
137901
138134
|
for (let c of this.commands) {
|
|
137902
138135
|
let args = [];
|
|
137903
138136
|
for (let i = 0; i < c.args.length; i += 2) {
|
|
137904
138137
|
let [x, y] = fn(c.args[i], c.args[i + 1]);
|
|
137905
138138
|
args.push(x, y);
|
|
137906
138139
|
}
|
|
137907
|
-
|
|
138140
|
+
path8[c.command](...args);
|
|
137908
138141
|
}
|
|
137909
|
-
return
|
|
138142
|
+
return path8;
|
|
137910
138143
|
}
|
|
137911
138144
|
/**
|
|
137912
138145
|
* Transforms the path by the given matrix.
|
|
@@ -138599,7 +138832,7 @@ var $69aac16029968692$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138599
138832
|
// Converts contours to a Path object that can be rendered
|
|
138600
138833
|
_getPath() {
|
|
138601
138834
|
let contours = this._getContours();
|
|
138602
|
-
let
|
|
138835
|
+
let path8 = new (0, $f43aec954cdfdf21$export$2e2bcd8739ae039)();
|
|
138603
138836
|
for (let i = 0; i < contours.length; i++) {
|
|
138604
138837
|
let contour = contours[i];
|
|
138605
138838
|
let firstPt = contour[0];
|
|
@@ -138615,26 +138848,26 @@ var $69aac16029968692$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138615
138848
|
firstPt = new $69aac16029968692$export$baf26146a414f24a(false, false, (firstPt.x + lastPt.x) / 2, (firstPt.y + lastPt.y) / 2);
|
|
138616
138849
|
var curvePt = firstPt;
|
|
138617
138850
|
}
|
|
138618
|
-
|
|
138851
|
+
path8.moveTo(firstPt.x, firstPt.y);
|
|
138619
138852
|
for (let j = start; j < contour.length; j++) {
|
|
138620
138853
|
let pt = contour[j];
|
|
138621
138854
|
let prevPt = j === 0 ? firstPt : contour[j - 1];
|
|
138622
|
-
if (prevPt.onCurve && pt.onCurve)
|
|
138855
|
+
if (prevPt.onCurve && pt.onCurve) path8.lineTo(pt.x, pt.y);
|
|
138623
138856
|
else if (prevPt.onCurve && !pt.onCurve) var curvePt = pt;
|
|
138624
138857
|
else if (!prevPt.onCurve && !pt.onCurve) {
|
|
138625
138858
|
let midX = (prevPt.x + pt.x) / 2;
|
|
138626
138859
|
let midY = (prevPt.y + pt.y) / 2;
|
|
138627
|
-
|
|
138860
|
+
path8.quadraticCurveTo(prevPt.x, prevPt.y, midX, midY);
|
|
138628
138861
|
var curvePt = pt;
|
|
138629
138862
|
} else if (!prevPt.onCurve && pt.onCurve) {
|
|
138630
|
-
|
|
138863
|
+
path8.quadraticCurveTo(curvePt.x, curvePt.y, pt.x, pt.y);
|
|
138631
138864
|
var curvePt = null;
|
|
138632
138865
|
} else throw new Error("Unknown TTF path state");
|
|
138633
138866
|
}
|
|
138634
|
-
if (curvePt)
|
|
138635
|
-
|
|
138867
|
+
if (curvePt) path8.quadraticCurveTo(curvePt.x, curvePt.y, firstPt.x, firstPt.y);
|
|
138868
|
+
path8.closePath();
|
|
138636
138869
|
}
|
|
138637
|
-
return
|
|
138870
|
+
return path8;
|
|
138638
138871
|
}
|
|
138639
138872
|
constructor(...args) {
|
|
138640
138873
|
super(...args);
|
|
@@ -138657,7 +138890,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138657
138890
|
let str = cff.topDict.CharStrings[this.id];
|
|
138658
138891
|
let end = str.offset + str.length;
|
|
138659
138892
|
stream.pos = str.offset;
|
|
138660
|
-
let
|
|
138893
|
+
let path8 = new (0, $f43aec954cdfdf21$export$2e2bcd8739ae039)();
|
|
138661
138894
|
let stack = [];
|
|
138662
138895
|
let trans = [];
|
|
138663
138896
|
let width = null;
|
|
@@ -138685,8 +138918,8 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138685
138918
|
return stack.length = 0;
|
|
138686
138919
|
}
|
|
138687
138920
|
function moveTo(x2, y2) {
|
|
138688
|
-
if (open)
|
|
138689
|
-
|
|
138921
|
+
if (open) path8.closePath();
|
|
138922
|
+
path8.moveTo(x2, y2);
|
|
138690
138923
|
open = true;
|
|
138691
138924
|
}
|
|
138692
138925
|
let parse = function() {
|
|
@@ -138713,7 +138946,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138713
138946
|
while (stack.length >= 2) {
|
|
138714
138947
|
x += stack.shift();
|
|
138715
138948
|
y += stack.shift();
|
|
138716
|
-
|
|
138949
|
+
path8.lineTo(x, y);
|
|
138717
138950
|
}
|
|
138718
138951
|
break;
|
|
138719
138952
|
case 6:
|
|
@@ -138722,7 +138955,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138722
138955
|
while (stack.length >= 1) {
|
|
138723
138956
|
if (phase) x += stack.shift();
|
|
138724
138957
|
else y += stack.shift();
|
|
138725
|
-
|
|
138958
|
+
path8.lineTo(x, y);
|
|
138726
138959
|
phase = !phase;
|
|
138727
138960
|
}
|
|
138728
138961
|
break;
|
|
@@ -138734,7 +138967,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138734
138967
|
c2y = c1y + stack.shift();
|
|
138735
138968
|
x = c2x + stack.shift();
|
|
138736
138969
|
y = c2y + stack.shift();
|
|
138737
|
-
|
|
138970
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
138738
138971
|
}
|
|
138739
138972
|
break;
|
|
138740
138973
|
case 10:
|
|
@@ -138758,7 +138991,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138758
138991
|
if (cff.version >= 2) break;
|
|
138759
138992
|
if (stack.length > 0) checkWidth();
|
|
138760
138993
|
if (open) {
|
|
138761
|
-
|
|
138994
|
+
path8.closePath();
|
|
138762
138995
|
open = false;
|
|
138763
138996
|
}
|
|
138764
138997
|
break;
|
|
@@ -138806,17 +139039,17 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138806
139039
|
c2y = c1y + stack.shift();
|
|
138807
139040
|
x = c2x + stack.shift();
|
|
138808
139041
|
y = c2y + stack.shift();
|
|
138809
|
-
|
|
139042
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
138810
139043
|
}
|
|
138811
139044
|
x += stack.shift();
|
|
138812
139045
|
y += stack.shift();
|
|
138813
|
-
|
|
139046
|
+
path8.lineTo(x, y);
|
|
138814
139047
|
break;
|
|
138815
139048
|
case 25:
|
|
138816
139049
|
while (stack.length >= 8) {
|
|
138817
139050
|
x += stack.shift();
|
|
138818
139051
|
y += stack.shift();
|
|
138819
|
-
|
|
139052
|
+
path8.lineTo(x, y);
|
|
138820
139053
|
}
|
|
138821
139054
|
c1x = x + stack.shift();
|
|
138822
139055
|
c1y = y + stack.shift();
|
|
@@ -138824,7 +139057,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138824
139057
|
c2y = c1y + stack.shift();
|
|
138825
139058
|
x = c2x + stack.shift();
|
|
138826
139059
|
y = c2y + stack.shift();
|
|
138827
|
-
|
|
139060
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
138828
139061
|
break;
|
|
138829
139062
|
case 26:
|
|
138830
139063
|
if (stack.length % 2) x += stack.shift();
|
|
@@ -138835,7 +139068,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138835
139068
|
c2y = c1y + stack.shift();
|
|
138836
139069
|
x = c2x;
|
|
138837
139070
|
y = c2y + stack.shift();
|
|
138838
|
-
|
|
139071
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
138839
139072
|
}
|
|
138840
139073
|
break;
|
|
138841
139074
|
case 27:
|
|
@@ -138847,7 +139080,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138847
139080
|
c2y = c1y + stack.shift();
|
|
138848
139081
|
x = c2x + stack.shift();
|
|
138849
139082
|
y = c2y;
|
|
138850
|
-
|
|
139083
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
138851
139084
|
}
|
|
138852
139085
|
break;
|
|
138853
139086
|
case 28:
|
|
@@ -138886,7 +139119,7 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
138886
139119
|
x = c2x + stack.shift();
|
|
138887
139120
|
y = c2y + (stack.length === 1 ? stack.shift() : 0);
|
|
138888
139121
|
}
|
|
138889
|
-
|
|
139122
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
138890
139123
|
phase = !phase;
|
|
138891
139124
|
}
|
|
138892
139125
|
break;
|
|
@@ -139012,8 +139245,8 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
139012
139245
|
c6y = c5y;
|
|
139013
139246
|
x = c6x;
|
|
139014
139247
|
y = c6y;
|
|
139015
|
-
|
|
139016
|
-
|
|
139248
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
|
|
139249
|
+
path8.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
|
|
139017
139250
|
break;
|
|
139018
139251
|
case 35:
|
|
139019
139252
|
pts = [];
|
|
@@ -139022,8 +139255,8 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
139022
139255
|
y += stack.shift();
|
|
139023
139256
|
pts.push(x, y);
|
|
139024
139257
|
}
|
|
139025
|
-
|
|
139026
|
-
|
|
139258
|
+
path8.bezierCurveTo(...pts.slice(0, 6));
|
|
139259
|
+
path8.bezierCurveTo(...pts.slice(6));
|
|
139027
139260
|
stack.shift();
|
|
139028
139261
|
break;
|
|
139029
139262
|
case 36:
|
|
@@ -139041,8 +139274,8 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
139041
139274
|
c6y = c5y;
|
|
139042
139275
|
x = c6x;
|
|
139043
139276
|
y = c6y;
|
|
139044
|
-
|
|
139045
|
-
|
|
139277
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
|
|
139278
|
+
path8.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
|
|
139046
139279
|
break;
|
|
139047
139280
|
case 37:
|
|
139048
139281
|
let startx = x;
|
|
@@ -139061,8 +139294,8 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
139061
139294
|
y += stack.shift();
|
|
139062
139295
|
}
|
|
139063
139296
|
pts.push(x, y);
|
|
139064
|
-
|
|
139065
|
-
|
|
139297
|
+
path8.bezierCurveTo(...pts.slice(0, 6));
|
|
139298
|
+
path8.bezierCurveTo(...pts.slice(6));
|
|
139066
139299
|
break;
|
|
139067
139300
|
default:
|
|
139068
139301
|
throw new Error(`Unknown op: 12 ${op}`);
|
|
@@ -139082,8 +139315,8 @@ var $62cc5109c6101893$export$2e2bcd8739ae039 = class extends (0, $f92906be28e617
|
|
|
139082
139315
|
}
|
|
139083
139316
|
};
|
|
139084
139317
|
parse();
|
|
139085
|
-
if (open)
|
|
139086
|
-
return
|
|
139318
|
+
if (open) path8.closePath();
|
|
139319
|
+
return path8;
|
|
139087
139320
|
}
|
|
139088
139321
|
constructor(...args) {
|
|
139089
139322
|
super(...args);
|
|
@@ -139543,7 +139776,7 @@ var $807e58506be70005$var$Glyf = new Struct({
|
|
|
139543
139776
|
yPoints: new ArrayT($807e58506be70005$var$Point, 0)
|
|
139544
139777
|
});
|
|
139545
139778
|
var $807e58506be70005$export$2e2bcd8739ae039 = class {
|
|
139546
|
-
encodeSimple(
|
|
139779
|
+
encodeSimple(path8, instructions = []) {
|
|
139547
139780
|
let endPtsOfContours = [];
|
|
139548
139781
|
let xPoints = [];
|
|
139549
139782
|
let yPoints = [];
|
|
@@ -139551,14 +139784,14 @@ var $807e58506be70005$export$2e2bcd8739ae039 = class {
|
|
|
139551
139784
|
let same = 0;
|
|
139552
139785
|
let lastX = 0, lastY = 0, lastFlag = 0;
|
|
139553
139786
|
let pointCount = 0;
|
|
139554
|
-
for (let i = 0; i <
|
|
139555
|
-
let c =
|
|
139787
|
+
for (let i = 0; i < path8.commands.length; i++) {
|
|
139788
|
+
let c = path8.commands[i];
|
|
139556
139789
|
for (let j = 0; j < c.args.length; j += 2) {
|
|
139557
139790
|
let x = c.args[j];
|
|
139558
139791
|
let y = c.args[j + 1];
|
|
139559
139792
|
let flag = 0;
|
|
139560
139793
|
if (c.command === "quadraticCurveTo" && j === 2) {
|
|
139561
|
-
let next =
|
|
139794
|
+
let next = path8.commands[i + 1];
|
|
139562
139795
|
if (next && next.command === "quadraticCurveTo") {
|
|
139563
139796
|
let midX = (lastX + next.args[0]) / 2;
|
|
139564
139797
|
let midY = (lastY + next.args[1]) / 2;
|
|
@@ -139585,8 +139818,8 @@ var $807e58506be70005$export$2e2bcd8739ae039 = class {
|
|
|
139585
139818
|
}
|
|
139586
139819
|
if (c.command === "closePath") endPtsOfContours.push(pointCount - 1);
|
|
139587
139820
|
}
|
|
139588
|
-
if (
|
|
139589
|
-
let bbox =
|
|
139821
|
+
if (path8.commands.length > 1 && path8.commands[path8.commands.length - 1].command !== "closePath") endPtsOfContours.push(pointCount - 1);
|
|
139822
|
+
let bbox = path8.bbox;
|
|
139590
139823
|
let glyf = {
|
|
139591
139824
|
numberOfContours: endPtsOfContours.length,
|
|
139592
139825
|
xMin: bbox.minX,
|
|
@@ -139693,7 +139926,7 @@ var $001d739428a71d5a$export$2e2bcd8739ae039 = class extends (0, $5cc7476da92df3
|
|
|
139693
139926
|
for (let gid of this.glyphs) {
|
|
139694
139927
|
this.charstrings.push(this.cff.getCharString(gid));
|
|
139695
139928
|
let glyph = this.font.getGlyph(gid);
|
|
139696
|
-
let
|
|
139929
|
+
let path8 = glyph.path;
|
|
139697
139930
|
for (let subr in glyph._usedGsubrs) gsubrs[subr] = true;
|
|
139698
139931
|
}
|
|
139699
139932
|
this.gsubrs = this.subsetSubrs(this.cff.globalSubrIndex, gsubrs);
|
|
@@ -139731,7 +139964,7 @@ var $001d739428a71d5a$export$2e2bcd8739ae039 = class extends (0, $5cc7476da92df3
|
|
|
139731
139964
|
used_fds[fd] = true;
|
|
139732
139965
|
topDict.FDSelect.fds.push(fd_select[fd]);
|
|
139733
139966
|
let glyph = this.font.getGlyph(gid);
|
|
139734
|
-
let
|
|
139967
|
+
let path8 = glyph.path;
|
|
139735
139968
|
for (let subr in glyph._usedSubrs) used_subrs[fd_select[fd]][subr] = true;
|
|
139736
139969
|
}
|
|
139737
139970
|
for (let i = 0; i < topDict.FDArray.length; i++) {
|
|
@@ -139748,7 +139981,7 @@ var $001d739428a71d5a$export$2e2bcd8739ae039 = class extends (0, $5cc7476da92df3
|
|
|
139748
139981
|
let used_subrs = {};
|
|
139749
139982
|
for (let gid of this.glyphs) {
|
|
139750
139983
|
let glyph = this.font.getGlyph(gid);
|
|
139751
|
-
let
|
|
139984
|
+
let path8 = glyph.path;
|
|
139752
139985
|
for (let subr in glyph._usedSubrs) used_subrs[subr] = true;
|
|
139753
139986
|
}
|
|
139754
139987
|
let privateDict = Object.assign({}, this.cff.topDict.Private);
|
|
@@ -141165,8 +141398,71 @@ var viteEmitter = (faces, ctx) => {
|
|
|
141165
141398
|
return { filename: "vite.fonts.md", content: lines.join("\n") };
|
|
141166
141399
|
};
|
|
141167
141400
|
|
|
141401
|
+
// ../core/src/emit/emitters/tokens.ts
|
|
141402
|
+
var tokensEmitter = (faces) => {
|
|
141403
|
+
const byFamily = groupByFamily(faces);
|
|
141404
|
+
if (byFamily.size === 0) return null;
|
|
141405
|
+
const familyTokens = {};
|
|
141406
|
+
const weightTokens = {};
|
|
141407
|
+
const seenWeights = /* @__PURE__ */ new Set();
|
|
141408
|
+
for (const [family, list] of byFamily) {
|
|
141409
|
+
const key = familyToKebab(family);
|
|
141410
|
+
familyTokens[key] = {
|
|
141411
|
+
$type: "fontFamily",
|
|
141412
|
+
$value: [family, `${family} Fallback`, "system-ui", "sans-serif"]
|
|
141413
|
+
};
|
|
141414
|
+
for (const f of list) {
|
|
141415
|
+
const w = parseInt(f.weight, 10);
|
|
141416
|
+
if (Number.isFinite(w) && !seenWeights.has(w)) {
|
|
141417
|
+
seenWeights.add(w);
|
|
141418
|
+
weightTokens[weightLabel(w)] = { $type: "fontWeight", $value: w };
|
|
141419
|
+
}
|
|
141420
|
+
}
|
|
141421
|
+
}
|
|
141422
|
+
const sizeTokens = {
|
|
141423
|
+
xs: { $type: "dimension", $value: "0.75rem" },
|
|
141424
|
+
sm: { $type: "dimension", $value: "0.875rem" },
|
|
141425
|
+
base: { $type: "dimension", $value: "1rem" },
|
|
141426
|
+
lg: { $type: "dimension", $value: "1.125rem" },
|
|
141427
|
+
xl: { $type: "dimension", $value: "1.25rem" },
|
|
141428
|
+
"2xl": { $type: "dimension", $value: "1.5rem" },
|
|
141429
|
+
"3xl": { $type: "dimension", $value: "1.875rem" },
|
|
141430
|
+
"4xl": { $type: "dimension", $value: "2.25rem" }
|
|
141431
|
+
};
|
|
141432
|
+
const lineHeightTokens = {
|
|
141433
|
+
tight: { $type: "number", $value: 1.2 },
|
|
141434
|
+
snug: { $type: "number", $value: 1.3 },
|
|
141435
|
+
normal: { $type: "number", $value: 1.5 },
|
|
141436
|
+
relaxed: { $type: "number", $value: 1.7 },
|
|
141437
|
+
loose: { $type: "number", $value: 2 }
|
|
141438
|
+
};
|
|
141439
|
+
const out = {
|
|
141440
|
+
$schema: "https://design-tokens.github.io/community-group/format/tokens.schema.json",
|
|
141441
|
+
$description: "Generated by fontfetch \u2014 W3C DTCG-compatible design tokens.",
|
|
141442
|
+
font: {
|
|
141443
|
+
family: familyTokens,
|
|
141444
|
+
weight: weightTokens,
|
|
141445
|
+
size: sizeTokens,
|
|
141446
|
+
lineHeight: lineHeightTokens
|
|
141447
|
+
}
|
|
141448
|
+
};
|
|
141449
|
+
return { filename: "fonts.tokens.json", content: `${JSON.stringify(out, null, 2)}
|
|
141450
|
+
` };
|
|
141451
|
+
};
|
|
141452
|
+
function weightLabel(weight) {
|
|
141453
|
+
if (weight <= 100) return "thin";
|
|
141454
|
+
if (weight <= 200) return "extralight";
|
|
141455
|
+
if (weight <= 300) return "light";
|
|
141456
|
+
if (weight <= 400) return "regular";
|
|
141457
|
+
if (weight <= 500) return "medium";
|
|
141458
|
+
if (weight <= 600) return "semibold";
|
|
141459
|
+
if (weight <= 700) return "bold";
|
|
141460
|
+
if (weight <= 800) return "extrabold";
|
|
141461
|
+
return "black";
|
|
141462
|
+
}
|
|
141463
|
+
|
|
141168
141464
|
// ../core/src/emit/emitters/types.ts
|
|
141169
|
-
var EMIT_TARGETS = ["css", "next", "tailwind", "vite"];
|
|
141465
|
+
var EMIT_TARGETS = ["css", "next", "tailwind", "vite", "tokens"];
|
|
141170
141466
|
function isEmitTarget(s) {
|
|
141171
141467
|
return EMIT_TARGETS.includes(s);
|
|
141172
141468
|
}
|
|
@@ -141175,57 +141471,10 @@ function isEmitTarget(s) {
|
|
|
141175
141471
|
var EMITTERS = {
|
|
141176
141472
|
next: nextEmitter,
|
|
141177
141473
|
tailwind: tailwindEmitter,
|
|
141178
|
-
vite: viteEmitter
|
|
141474
|
+
vite: viteEmitter,
|
|
141475
|
+
tokens: tokensEmitter
|
|
141179
141476
|
};
|
|
141180
141477
|
|
|
141181
|
-
// ../core/src/license/provenance.ts
|
|
141182
|
-
var RULES = [
|
|
141183
|
-
{
|
|
141184
|
-
bucket: "google",
|
|
141185
|
-
patterns: ["fonts.gstatic.com", "fonts.googleapis.com", "cdn.jsdelivr.net/gh/google/fonts"]
|
|
141186
|
-
},
|
|
141187
|
-
{
|
|
141188
|
-
bucket: "adobe-typekit",
|
|
141189
|
-
patterns: ["use.typekit.net", "p.typekit.net", "fonts.adobe.com"]
|
|
141190
|
-
},
|
|
141191
|
-
{
|
|
141192
|
-
bucket: "commercial",
|
|
141193
|
-
patterns: [
|
|
141194
|
-
"fast.fonts.net",
|
|
141195
|
-
"cloud.typography.com",
|
|
141196
|
-
"cloud.typenetwork.com",
|
|
141197
|
-
"use.fontawesome.com",
|
|
141198
|
-
"fontstand.com"
|
|
141199
|
-
]
|
|
141200
|
-
},
|
|
141201
|
-
{
|
|
141202
|
-
bucket: "open-cdn",
|
|
141203
|
-
patterns: ["cdn.jsdelivr.net/npm/@fontsource/", "rsms.me"]
|
|
141204
|
-
}
|
|
141205
|
-
];
|
|
141206
|
-
function strip(host) {
|
|
141207
|
-
return host.replace(/^www\./i, "").toLowerCase();
|
|
141208
|
-
}
|
|
141209
|
-
function sameOrigin(urlHost, pageHost) {
|
|
141210
|
-
const a = strip(urlHost);
|
|
141211
|
-
const b = strip(pageHost);
|
|
141212
|
-
if (a === b) return true;
|
|
141213
|
-
return a.endsWith("." + b) || b.endsWith("." + a);
|
|
141214
|
-
}
|
|
141215
|
-
function bucketForUrl(url, pageHost) {
|
|
141216
|
-
for (const rule of RULES) {
|
|
141217
|
-
for (const pattern of rule.patterns) {
|
|
141218
|
-
if (url.includes(pattern)) return rule.bucket;
|
|
141219
|
-
}
|
|
141220
|
-
}
|
|
141221
|
-
try {
|
|
141222
|
-
const u = new URL(url);
|
|
141223
|
-
if (sameOrigin(u.hostname, pageHost)) return "self-hosted";
|
|
141224
|
-
} catch {
|
|
141225
|
-
}
|
|
141226
|
-
return "self-hosted";
|
|
141227
|
-
}
|
|
141228
|
-
|
|
141229
141478
|
// ../core/src/inspect/fallback.ts
|
|
141230
141479
|
import fs3 from "fs/promises";
|
|
141231
141480
|
import path4 from "path";
|
|
@@ -145341,16 +145590,16 @@ var Path = class Path2 {
|
|
|
145341
145590
|
* Applies a mapping function to each point in the path.
|
|
145342
145591
|
*/
|
|
145343
145592
|
mapPoints(fn) {
|
|
145344
|
-
const
|
|
145593
|
+
const path8 = new Path2();
|
|
145345
145594
|
for (const c of this.commands) {
|
|
145346
145595
|
const args = [];
|
|
145347
145596
|
for (let i = 0; i < c.args.length; i += 2) {
|
|
145348
145597
|
const [x, y] = fn(c.args[i], c.args[i + 1]);
|
|
145349
145598
|
args.push(x, y);
|
|
145350
145599
|
}
|
|
145351
|
-
|
|
145600
|
+
path8[c.command](...args);
|
|
145352
145601
|
}
|
|
145353
|
-
return
|
|
145602
|
+
return path8;
|
|
145354
145603
|
}
|
|
145355
145604
|
/**
|
|
145356
145605
|
* Transforms the path by the given matrix.
|
|
@@ -145992,7 +146241,7 @@ var TTFGlyph = class extends Glyph {
|
|
|
145992
146241
|
}
|
|
145993
146242
|
_getPath() {
|
|
145994
146243
|
const contours = this._getContours();
|
|
145995
|
-
const
|
|
146244
|
+
const path8 = new Path();
|
|
145996
146245
|
let curvePt;
|
|
145997
146246
|
for (const contour of contours) {
|
|
145998
146247
|
let firstPt = contour[0];
|
|
@@ -146006,26 +146255,26 @@ var TTFGlyph = class extends Glyph {
|
|
|
146006
146255
|
else firstPt = new Point(false, false, (firstPt.x + lastPt.x) / 2, (firstPt.y + lastPt.y) / 2);
|
|
146007
146256
|
curvePt = firstPt;
|
|
146008
146257
|
}
|
|
146009
|
-
|
|
146258
|
+
path8.moveTo(firstPt.x, firstPt.y);
|
|
146010
146259
|
for (let j = start; j < contour.length; j++) {
|
|
146011
146260
|
const pt = contour[j];
|
|
146012
146261
|
const prevPt = j === 0 ? firstPt : contour[j - 1];
|
|
146013
|
-
if (prevPt.onCurve && pt.onCurve)
|
|
146262
|
+
if (prevPt.onCurve && pt.onCurve) path8.lineTo(pt.x, pt.y);
|
|
146014
146263
|
else if (prevPt.onCurve && !pt.onCurve) curvePt = pt;
|
|
146015
146264
|
else if (!prevPt.onCurve && !pt.onCurve) {
|
|
146016
146265
|
const midX = (prevPt.x + pt.x) / 2;
|
|
146017
146266
|
const midY = (prevPt.y + pt.y) / 2;
|
|
146018
|
-
|
|
146267
|
+
path8.quadraticCurveTo(prevPt.x, prevPt.y, midX, midY);
|
|
146019
146268
|
curvePt = pt;
|
|
146020
146269
|
} else if (!prevPt.onCurve && pt.onCurve) {
|
|
146021
|
-
|
|
146270
|
+
path8.quadraticCurveTo(curvePt.x, curvePt.y, pt.x, pt.y);
|
|
146022
146271
|
curvePt = null;
|
|
146023
146272
|
} else throw new Error("Unknown TTF path state");
|
|
146024
146273
|
}
|
|
146025
|
-
if (curvePt)
|
|
146026
|
-
|
|
146274
|
+
if (curvePt) path8.quadraticCurveTo(curvePt.x, curvePt.y, firstPt.x, firstPt.y);
|
|
146275
|
+
path8.closePath();
|
|
146027
146276
|
}
|
|
146028
|
-
return
|
|
146277
|
+
return path8;
|
|
146029
146278
|
}
|
|
146030
146279
|
};
|
|
146031
146280
|
var CFFGlyph = class extends Glyph {
|
|
@@ -146043,7 +146292,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146043
146292
|
const str = cff.topDict.CharStrings[this.id];
|
|
146044
146293
|
let end = str.offset + str.length;
|
|
146045
146294
|
stream.pos = str.offset;
|
|
146046
|
-
const
|
|
146295
|
+
const path8 = new Path();
|
|
146047
146296
|
const stack = [];
|
|
146048
146297
|
const trans = [];
|
|
146049
146298
|
let width = null;
|
|
@@ -146071,8 +146320,8 @@ var CFFGlyph = class extends Glyph {
|
|
|
146071
146320
|
return stack.length = 0;
|
|
146072
146321
|
};
|
|
146073
146322
|
const moveTo = (x2, y2) => {
|
|
146074
|
-
if (open)
|
|
146075
|
-
|
|
146323
|
+
if (open) path8.closePath();
|
|
146324
|
+
path8.moveTo(x2, y2);
|
|
146076
146325
|
open = true;
|
|
146077
146326
|
};
|
|
146078
146327
|
const parse = () => {
|
|
@@ -146099,7 +146348,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146099
146348
|
while (stack.length >= 2) {
|
|
146100
146349
|
x += stack.shift();
|
|
146101
146350
|
y += stack.shift();
|
|
146102
|
-
|
|
146351
|
+
path8.lineTo(x, y);
|
|
146103
146352
|
}
|
|
146104
146353
|
break;
|
|
146105
146354
|
case 6:
|
|
@@ -146108,7 +146357,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146108
146357
|
while (stack.length >= 1) {
|
|
146109
146358
|
if (phase) x += stack.shift();
|
|
146110
146359
|
else y += stack.shift();
|
|
146111
|
-
|
|
146360
|
+
path8.lineTo(x, y);
|
|
146112
146361
|
phase = !phase;
|
|
146113
146362
|
}
|
|
146114
146363
|
break;
|
|
@@ -146120,7 +146369,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146120
146369
|
c2y = c1y + stack.shift();
|
|
146121
146370
|
x = c2x + stack.shift();
|
|
146122
146371
|
y = c2y + stack.shift();
|
|
146123
|
-
|
|
146372
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
146124
146373
|
}
|
|
146125
146374
|
break;
|
|
146126
146375
|
case 10:
|
|
@@ -146144,7 +146393,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146144
146393
|
if (cff.version >= 2) break;
|
|
146145
146394
|
if (stack.length > 0) checkWidth();
|
|
146146
146395
|
if (open) {
|
|
146147
|
-
|
|
146396
|
+
path8.closePath();
|
|
146148
146397
|
open = false;
|
|
146149
146398
|
}
|
|
146150
146399
|
break;
|
|
@@ -146192,17 +146441,17 @@ var CFFGlyph = class extends Glyph {
|
|
|
146192
146441
|
c2y = c1y + stack.shift();
|
|
146193
146442
|
x = c2x + stack.shift();
|
|
146194
146443
|
y = c2y + stack.shift();
|
|
146195
|
-
|
|
146444
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
146196
146445
|
}
|
|
146197
146446
|
x += stack.shift();
|
|
146198
146447
|
y += stack.shift();
|
|
146199
|
-
|
|
146448
|
+
path8.lineTo(x, y);
|
|
146200
146449
|
break;
|
|
146201
146450
|
case 25:
|
|
146202
146451
|
while (stack.length >= 8) {
|
|
146203
146452
|
x += stack.shift();
|
|
146204
146453
|
y += stack.shift();
|
|
146205
|
-
|
|
146454
|
+
path8.lineTo(x, y);
|
|
146206
146455
|
}
|
|
146207
146456
|
c1x = x + stack.shift();
|
|
146208
146457
|
c1y = y + stack.shift();
|
|
@@ -146210,7 +146459,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146210
146459
|
c2y = c1y + stack.shift();
|
|
146211
146460
|
x = c2x + stack.shift();
|
|
146212
146461
|
y = c2y + stack.shift();
|
|
146213
|
-
|
|
146462
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
146214
146463
|
break;
|
|
146215
146464
|
case 26:
|
|
146216
146465
|
if (stack.length % 2) x += stack.shift();
|
|
@@ -146221,7 +146470,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146221
146470
|
c2y = c1y + stack.shift();
|
|
146222
146471
|
x = c2x;
|
|
146223
146472
|
y = c2y + stack.shift();
|
|
146224
|
-
|
|
146473
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
146225
146474
|
}
|
|
146226
146475
|
break;
|
|
146227
146476
|
case 27:
|
|
@@ -146233,7 +146482,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146233
146482
|
c2y = c1y + stack.shift();
|
|
146234
146483
|
x = c2x + stack.shift();
|
|
146235
146484
|
y = c2y;
|
|
146236
|
-
|
|
146485
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
146237
146486
|
}
|
|
146238
146487
|
break;
|
|
146239
146488
|
case 28:
|
|
@@ -146272,7 +146521,7 @@ var CFFGlyph = class extends Glyph {
|
|
|
146272
146521
|
x = c2x + stack.shift();
|
|
146273
146522
|
y = c2y + (stack.length === 1 ? stack.shift() : 0);
|
|
146274
146523
|
}
|
|
146275
|
-
|
|
146524
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
|
|
146276
146525
|
phase = !phase;
|
|
146277
146526
|
}
|
|
146278
146527
|
break;
|
|
@@ -146398,8 +146647,8 @@ var CFFGlyph = class extends Glyph {
|
|
|
146398
146647
|
c6y = c5y;
|
|
146399
146648
|
x = c6x;
|
|
146400
146649
|
y = c6y;
|
|
146401
|
-
|
|
146402
|
-
|
|
146650
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
|
|
146651
|
+
path8.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
|
|
146403
146652
|
break;
|
|
146404
146653
|
case 35:
|
|
146405
146654
|
pts = [];
|
|
@@ -146408,8 +146657,8 @@ var CFFGlyph = class extends Glyph {
|
|
|
146408
146657
|
y += stack.shift();
|
|
146409
146658
|
pts.push(x, y);
|
|
146410
146659
|
}
|
|
146411
|
-
|
|
146412
|
-
|
|
146660
|
+
path8.bezierCurveTo(...pts.slice(0, 6));
|
|
146661
|
+
path8.bezierCurveTo(...pts.slice(6));
|
|
146413
146662
|
stack.shift();
|
|
146414
146663
|
break;
|
|
146415
146664
|
case 36:
|
|
@@ -146427,8 +146676,8 @@ var CFFGlyph = class extends Glyph {
|
|
|
146427
146676
|
c6y = c5y;
|
|
146428
146677
|
x = c6x;
|
|
146429
146678
|
y = c6y;
|
|
146430
|
-
|
|
146431
|
-
|
|
146679
|
+
path8.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
|
|
146680
|
+
path8.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
|
|
146432
146681
|
break;
|
|
146433
146682
|
case 37:
|
|
146434
146683
|
const startx = x;
|
|
@@ -146447,8 +146696,8 @@ var CFFGlyph = class extends Glyph {
|
|
|
146447
146696
|
y += stack.shift();
|
|
146448
146697
|
}
|
|
146449
146698
|
pts.push(x, y);
|
|
146450
|
-
|
|
146451
|
-
|
|
146699
|
+
path8.bezierCurveTo(...pts.slice(0, 6));
|
|
146700
|
+
path8.bezierCurveTo(...pts.slice(6));
|
|
146452
146701
|
break;
|
|
146453
146702
|
default:
|
|
146454
146703
|
throw new Error(`Unknown op: 12 ${op}`);
|
|
@@ -146468,8 +146717,8 @@ var CFFGlyph = class extends Glyph {
|
|
|
146468
146717
|
}
|
|
146469
146718
|
};
|
|
146470
146719
|
parse();
|
|
146471
|
-
if (open)
|
|
146472
|
-
return
|
|
146720
|
+
if (open) path8.closePath();
|
|
146721
|
+
return path8;
|
|
146473
146722
|
}
|
|
146474
146723
|
};
|
|
146475
146724
|
var SBIXImage = new Struct2({
|
|
@@ -149582,57 +149831,57 @@ async function computeFallback(filePath, options = {}) {
|
|
|
149582
149831
|
};
|
|
149583
149832
|
}
|
|
149584
149833
|
function formatFallbackCss(metrics) {
|
|
149834
|
+
const variantTag = metrics.fontWeight || metrics.fontStyle ? ` (${metrics.fontWeight ?? "400"}${metrics.fontStyle && metrics.fontStyle !== "normal" ? " " + metrics.fontStyle : ""})` : "";
|
|
149585
149835
|
const lines = [
|
|
149586
|
-
`/* CLS-killing fallback for ${metrics.familyName} \u2014 generated by fontfetch */`,
|
|
149836
|
+
`/* CLS-killing fallback for ${metrics.familyName}${variantTag} \u2014 generated by fontfetch */`,
|
|
149587
149837
|
`@font-face {`,
|
|
149588
|
-
` font-family: '${metrics.fallbackFamily}'
|
|
149838
|
+
` font-family: '${metrics.fallbackFamily}';`
|
|
149839
|
+
];
|
|
149840
|
+
if (metrics.fontWeight) lines.push(` font-weight: ${metrics.fontWeight};`);
|
|
149841
|
+
if (metrics.fontStyle) lines.push(` font-style: ${metrics.fontStyle};`);
|
|
149842
|
+
lines.push(
|
|
149589
149843
|
` src: local('${SYSTEM_FALLBACKS[metrics.generic].familyName}');`,
|
|
149590
149844
|
` size-adjust: ${metrics.sizeAdjust};`,
|
|
149591
149845
|
` ascent-override: ${metrics.ascentOverride};`,
|
|
149592
149846
|
` descent-override: ${metrics.descentOverride};`,
|
|
149593
149847
|
` line-gap-override: ${metrics.lineGapOverride};`,
|
|
149594
149848
|
`}`
|
|
149595
|
-
|
|
149849
|
+
);
|
|
149596
149850
|
return lines.join("\n");
|
|
149597
149851
|
}
|
|
149598
|
-
async function
|
|
149599
|
-
let entries;
|
|
149600
|
-
try {
|
|
149601
|
-
entries = await collectFontFiles2(filesDir);
|
|
149602
|
-
} catch (e) {
|
|
149603
|
-
return { css: "", count: 0, errors: [{ file: filesDir, reason: e.message }] };
|
|
149604
|
-
}
|
|
149605
|
-
const seen = /* @__PURE__ */ new Set();
|
|
149852
|
+
async function buildPerFaceFallbacks(filesDir, faces) {
|
|
149606
149853
|
const blocks = [];
|
|
149607
149854
|
const errors = [];
|
|
149608
|
-
|
|
149855
|
+
const seen = /* @__PURE__ */ new Set();
|
|
149856
|
+
const metricsByFile = /* @__PURE__ */ new Map();
|
|
149857
|
+
for (const face of faces) {
|
|
149858
|
+
const primary = face.sources.find((s) => s.localFile);
|
|
149859
|
+
if (!primary?.localFile) continue;
|
|
149860
|
+
const key = `${face.family}|${face.weight}|${face.style}`;
|
|
149861
|
+
if (seen.has(key)) continue;
|
|
149862
|
+
seen.add(key);
|
|
149863
|
+
const abs2 = path4.join(filesDir, primary.localFile);
|
|
149609
149864
|
try {
|
|
149610
|
-
|
|
149611
|
-
if (
|
|
149612
|
-
|
|
149613
|
-
|
|
149865
|
+
let base = metricsByFile.get(abs2);
|
|
149866
|
+
if (!base) {
|
|
149867
|
+
base = await computeFallback(abs2);
|
|
149868
|
+
metricsByFile.set(abs2, base);
|
|
149869
|
+
}
|
|
149870
|
+
blocks.push(
|
|
149871
|
+
formatFallbackCss({
|
|
149872
|
+
...base,
|
|
149873
|
+
familyName: face.family,
|
|
149874
|
+
fallbackFamily: `${face.family} Fallback`,
|
|
149875
|
+
fontWeight: face.weight || "400",
|
|
149876
|
+
fontStyle: face.style || "normal"
|
|
149877
|
+
})
|
|
149878
|
+
);
|
|
149614
149879
|
} catch (e) {
|
|
149615
|
-
errors.push({ file, reason: e.message });
|
|
149880
|
+
errors.push({ file: primary.localFile, reason: e.message });
|
|
149616
149881
|
}
|
|
149617
149882
|
}
|
|
149618
149883
|
return { css: blocks.join("\n\n"), count: blocks.length, errors };
|
|
149619
149884
|
}
|
|
149620
|
-
async function collectFontFiles2(root) {
|
|
149621
|
-
const out = [];
|
|
149622
|
-
async function walk(dir) {
|
|
149623
|
-
const items = await fs3.readdir(dir, { withFileTypes: true });
|
|
149624
|
-
for (const it of items) {
|
|
149625
|
-
const p = path4.join(dir, it.name);
|
|
149626
|
-
if (it.isDirectory()) {
|
|
149627
|
-
await walk(p);
|
|
149628
|
-
} else if (/\.(woff2|woff|ttf|otf)$/i.test(it.name)) {
|
|
149629
|
-
out.push(p);
|
|
149630
|
-
}
|
|
149631
|
-
}
|
|
149632
|
-
}
|
|
149633
|
-
await walk(root);
|
|
149634
|
-
return out;
|
|
149635
|
-
}
|
|
149636
149885
|
|
|
149637
149886
|
// ../core/src/parse/crawl.ts
|
|
149638
149887
|
var CRAWL_PAGE_CAP = 50;
|
|
@@ -149679,6 +149928,88 @@ function stripHash(u) {
|
|
|
149679
149928
|
return clone.toString();
|
|
149680
149929
|
}
|
|
149681
149930
|
|
|
149931
|
+
// ../core/src/parse/consistency.ts
|
|
149932
|
+
function computeConsistency(perPage) {
|
|
149933
|
+
if (perPage.length === 0) {
|
|
149934
|
+
return { shared: [], perPage: [], divergent: [] };
|
|
149935
|
+
}
|
|
149936
|
+
const sortedPerPage = perPage.map((p) => ({ url: p.url, families: [...p.families].sort() }));
|
|
149937
|
+
const allFamilySets = sortedPerPage.map((p) => new Set(p.families));
|
|
149938
|
+
const shared = [...allFamilySets[0]].filter(
|
|
149939
|
+
(fam) => allFamilySets.every((s) => s.has(fam))
|
|
149940
|
+
);
|
|
149941
|
+
shared.sort();
|
|
149942
|
+
const entry = allFamilySets[0];
|
|
149943
|
+
const divergent = [];
|
|
149944
|
+
for (let i = 1; i < sortedPerPage.length; i++) {
|
|
149945
|
+
const here = allFamilySets[i];
|
|
149946
|
+
const onlyHere = [...here].filter((f) => !entry.has(f)).sort();
|
|
149947
|
+
const missingHere = [...entry].filter((f) => !here.has(f)).sort();
|
|
149948
|
+
if (onlyHere.length > 0 || missingHere.length > 0) {
|
|
149949
|
+
divergent.push({ url: sortedPerPage[i].url, onlyHere, missingHere });
|
|
149950
|
+
}
|
|
149951
|
+
}
|
|
149952
|
+
return { shared, perPage: sortedPerPage, divergent };
|
|
149953
|
+
}
|
|
149954
|
+
function buildPageFaceMap(pageUrls, sourceToPage, facesPerSource) {
|
|
149955
|
+
const pageFamilies = /* @__PURE__ */ new Map();
|
|
149956
|
+
for (const url of pageUrls) pageFamilies.set(url, /* @__PURE__ */ new Set());
|
|
149957
|
+
for (let i = 0; i < facesPerSource.length; i++) {
|
|
149958
|
+
const pageIdx = sourceToPage[i];
|
|
149959
|
+
const pageUrl = pageUrls[pageIdx];
|
|
149960
|
+
if (!pageUrl) continue;
|
|
149961
|
+
const set = pageFamilies.get(pageUrl);
|
|
149962
|
+
for (const f of facesPerSource[i]) set.add(f.family);
|
|
149963
|
+
}
|
|
149964
|
+
return pageUrls.map((url) => ({ url, families: [...pageFamilies.get(url) ?? []] }));
|
|
149965
|
+
}
|
|
149966
|
+
function buildConsistencyReport(report, host) {
|
|
149967
|
+
const lines = [
|
|
149968
|
+
`# Cross-page font consistency for ${host}`,
|
|
149969
|
+
"",
|
|
149970
|
+
`Crawled ${report.perPage.length} page(s). ${report.shared.length} family/families used everywhere; ${report.divergent.length} page(s) diverge from the entry page.`,
|
|
149971
|
+
""
|
|
149972
|
+
];
|
|
149973
|
+
lines.push("## Shared across every page");
|
|
149974
|
+
lines.push("");
|
|
149975
|
+
if (report.shared.length === 0) {
|
|
149976
|
+
lines.push("_No families appeared on every page._");
|
|
149977
|
+
} else {
|
|
149978
|
+
for (const fam of report.shared) lines.push(`- ${fam}`);
|
|
149979
|
+
}
|
|
149980
|
+
lines.push("");
|
|
149981
|
+
lines.push("## Per-page families");
|
|
149982
|
+
lines.push("");
|
|
149983
|
+
for (const page of report.perPage) {
|
|
149984
|
+
lines.push(`### ${page.url}`);
|
|
149985
|
+
lines.push("");
|
|
149986
|
+
if (page.families.length === 0) {
|
|
149987
|
+
lines.push("_No @font-face declarations found on this page._");
|
|
149988
|
+
} else {
|
|
149989
|
+
for (const fam of page.families) lines.push(`- ${fam}`);
|
|
149990
|
+
}
|
|
149991
|
+
lines.push("");
|
|
149992
|
+
}
|
|
149993
|
+
if (report.divergent.length > 0) {
|
|
149994
|
+
lines.push("## Divergence from the entry page");
|
|
149995
|
+
lines.push("");
|
|
149996
|
+
lines.push("_The entry page (first URL crawled) is the baseline. Each row below is a page whose typography differs from the entry._");
|
|
149997
|
+
lines.push("");
|
|
149998
|
+
for (const d of report.divergent) {
|
|
149999
|
+
lines.push(`### ${d.url}`);
|
|
150000
|
+
lines.push("");
|
|
150001
|
+
if (d.onlyHere.length > 0) {
|
|
150002
|
+
lines.push(`- **Only here**: ${d.onlyHere.join(", ")}`);
|
|
150003
|
+
}
|
|
150004
|
+
if (d.missingHere.length > 0) {
|
|
150005
|
+
lines.push(`- **Missing here** (but on entry): ${d.missingHere.join(", ")}`);
|
|
150006
|
+
}
|
|
150007
|
+
lines.push("");
|
|
150008
|
+
}
|
|
150009
|
+
}
|
|
150010
|
+
return lines.join("\n");
|
|
150011
|
+
}
|
|
150012
|
+
|
|
149682
150013
|
// ../core/src/platforms/nextjs.ts
|
|
149683
150014
|
var NEXT_SUBSET_RE = /^(.+\/_next\/static\/media\/[^/?#]+-s\.)([a-z0-9]+)(\.(?:woff2|woff|ttf|otf))(\?[^#]*)?(#.*)?$/i;
|
|
149684
150015
|
var SIBLING_LETTERS = "abcdefghijklmnopqrstuvwxyz".split("");
|
|
@@ -149716,6 +150047,63 @@ async function probeNextjsSiblings(url, headers = {}) {
|
|
|
149716
150047
|
return found.sort();
|
|
149717
150048
|
}
|
|
149718
150049
|
|
|
150050
|
+
// ../core/src/inspect/collapse.ts
|
|
150051
|
+
import path5 from "path";
|
|
150052
|
+
function detectCollapseOpportunities(variableFonts, faces, fileSizes) {
|
|
150053
|
+
const out = [];
|
|
150054
|
+
for (const vf of variableFonts) {
|
|
150055
|
+
const hasWeightAxis = vf.axes.some((a) => a.tag === "wght" && a.min !== a.max);
|
|
150056
|
+
if (!hasWeightAxis) continue;
|
|
150057
|
+
const variableFileName = path5.basename(vf.filePath);
|
|
150058
|
+
const staticFiles = /* @__PURE__ */ new Map();
|
|
150059
|
+
for (const f of faces) {
|
|
150060
|
+
if (f.family !== vf.family) continue;
|
|
150061
|
+
for (const s of f.sources) {
|
|
150062
|
+
if (!s.localFile) continue;
|
|
150063
|
+
const baseName = path5.basename(s.localFile);
|
|
150064
|
+
if (baseName === variableFileName) continue;
|
|
150065
|
+
if (/\.subset\./i.test(baseName)) continue;
|
|
150066
|
+
staticFiles.set(s.localFile, { weight: f.weight, style: f.style });
|
|
150067
|
+
}
|
|
150068
|
+
}
|
|
150069
|
+
if (staticFiles.size < 2) continue;
|
|
150070
|
+
const variableLocalKey = findLocalKeyForFile(faces, variableFileName);
|
|
150071
|
+
const variableBytes = variableLocalKey ? fileSizes.get(variableLocalKey) ?? 0 : 0;
|
|
150072
|
+
const staticEntries = [];
|
|
150073
|
+
let staticBytes = 0;
|
|
150074
|
+
for (const [file, meta] of staticFiles) {
|
|
150075
|
+
const bytes = fileSizes.get(file) ?? 0;
|
|
150076
|
+
staticBytes += bytes;
|
|
150077
|
+
staticEntries.push({ file, bytes, weight: meta.weight, style: meta.style });
|
|
150078
|
+
}
|
|
150079
|
+
const savedBytes = staticBytes - variableBytes;
|
|
150080
|
+
if (savedBytes <= 0) continue;
|
|
150081
|
+
out.push({
|
|
150082
|
+
family: vf.family,
|
|
150083
|
+
variableFile: vf.filePath,
|
|
150084
|
+
variableBytes,
|
|
150085
|
+
staticFiles: staticEntries,
|
|
150086
|
+
staticBytes,
|
|
150087
|
+
savedBytes
|
|
150088
|
+
});
|
|
150089
|
+
}
|
|
150090
|
+
return out;
|
|
150091
|
+
}
|
|
150092
|
+
function findLocalKeyForFile(faces, baseName) {
|
|
150093
|
+
for (const f of faces) {
|
|
150094
|
+
for (const s of f.sources) {
|
|
150095
|
+
if (s.localFile && path5.basename(s.localFile) === baseName) {
|
|
150096
|
+
return s.localFile;
|
|
150097
|
+
}
|
|
150098
|
+
}
|
|
150099
|
+
}
|
|
150100
|
+
return void 0;
|
|
150101
|
+
}
|
|
150102
|
+
function formatCollapseHint(op) {
|
|
150103
|
+
const savedKb = (op.savedBytes / 1024).toFixed(1);
|
|
150104
|
+
return ` \u2139 ${op.family}: drop ${op.staticFiles.length} static weight file(s) \u2014 variable font already covers them. Saves ~${savedKb} KB.`;
|
|
150105
|
+
}
|
|
150106
|
+
|
|
149719
150107
|
// ../core/src/formats/formats.ts
|
|
149720
150108
|
var FONT_FORMATS = ["woff2", "woff", "ttf", "otf", "eot"];
|
|
149721
150109
|
function isFontFormat(s) {
|
|
@@ -149764,11 +150152,12 @@ async function pull({
|
|
|
149764
150152
|
onProgress,
|
|
149765
150153
|
fallback = false,
|
|
149766
150154
|
pages = 1,
|
|
149767
|
-
formats: formats2
|
|
150155
|
+
formats: formats2,
|
|
150156
|
+
gdprReport = false
|
|
149768
150157
|
}) {
|
|
149769
150158
|
const host = siteSlug(url);
|
|
149770
|
-
const outDir =
|
|
149771
|
-
const filesDir =
|
|
150159
|
+
const outDir = path6.join(path6.resolve(baseDir), host);
|
|
150160
|
+
const filesDir = path6.join(outDir, "files");
|
|
149772
150161
|
await fs4.mkdir(filesDir, { recursive: true });
|
|
149773
150162
|
const cappedPages = Math.max(1, Math.min(Math.floor(pages), CRAWL_PAGE_CAP));
|
|
149774
150163
|
onProgress?.({ type: "phase", phase: "fetch_html" });
|
|
@@ -149796,6 +150185,7 @@ async function pull({
|
|
|
149796
150185
|
}
|
|
149797
150186
|
onProgress?.({ type: "phase", phase: "parse_css" });
|
|
149798
150187
|
const cssSources = [];
|
|
150188
|
+
const sourceToPage = [];
|
|
149799
150189
|
const cssLinkSet = /* @__PURE__ */ new Set();
|
|
149800
150190
|
const perPage = [];
|
|
149801
150191
|
let totalInline = 0;
|
|
@@ -149812,12 +150202,17 @@ async function pull({
|
|
|
149812
150202
|
perPage.push({ page, uniqueLinks, inline });
|
|
149813
150203
|
}
|
|
149814
150204
|
log.info(` ${cssLinkSet.size} external stylesheet(s), ${totalInline} inline <style> block(s)`);
|
|
149815
|
-
for (
|
|
149816
|
-
|
|
150205
|
+
for (let pageIdx = 0; pageIdx < perPage.length; pageIdx++) {
|
|
150206
|
+
const { page, uniqueLinks, inline } = perPage[pageIdx];
|
|
150207
|
+
for (const text of inline) {
|
|
150208
|
+
cssSources.push({ text, base: page.url });
|
|
150209
|
+
sourceToPage.push(pageIdx);
|
|
150210
|
+
}
|
|
149817
150211
|
for (const link of uniqueLinks) {
|
|
149818
150212
|
try {
|
|
149819
150213
|
log.info(`\u2192 Fetching CSS: ${link}`);
|
|
149820
150214
|
cssSources.push({ text: await fetchText(link, { Referer: page.url }), base: link });
|
|
150215
|
+
sourceToPage.push(pageIdx);
|
|
149821
150216
|
onProgress?.({ type: "css_fetched", url: link });
|
|
149822
150217
|
} catch (e) {
|
|
149823
150218
|
const reason = e.message;
|
|
@@ -149831,7 +150226,10 @@ async function pull({
|
|
|
149831
150226
|
log.info("\u2192 Running headless mode (Playwright)...");
|
|
149832
150227
|
try {
|
|
149833
150228
|
const result = await fetchHeadless(url);
|
|
149834
|
-
|
|
150229
|
+
for (const src of result.cssSources) {
|
|
150230
|
+
cssSources.push(src);
|
|
150231
|
+
sourceToPage.push(0);
|
|
150232
|
+
}
|
|
149835
150233
|
networkFontUrls = result.networkFontUrls;
|
|
149836
150234
|
log.info(` + ${result.cssSources.length} stylesheet block(s) from headless`);
|
|
149837
150235
|
if (networkFontUrls.length > 0) {
|
|
@@ -149842,7 +150240,8 @@ async function pull({
|
|
|
149842
150240
|
log.warn(" Continuing with static results.");
|
|
149843
150241
|
}
|
|
149844
150242
|
}
|
|
149845
|
-
const
|
|
150243
|
+
const facesPerSource = cssSources.map(({ text, base }) => extractFontFaces(text, base));
|
|
150244
|
+
const allFaces = facesPerSource.flat();
|
|
149846
150245
|
const seen = /* @__PURE__ */ new Set();
|
|
149847
150246
|
const dedupedFaces = allFaces.filter((f) => {
|
|
149848
150247
|
const sig = `${f.family}|${f.weight}|${f.style}|${f.sources.map((s) => s.url).sort().join(",")}`;
|
|
@@ -149974,13 +150373,13 @@ async function pull({
|
|
|
149974
150373
|
});
|
|
149975
150374
|
if (licenseSummary.allCommercial && !force) {
|
|
149976
150375
|
await fs4.writeFile(
|
|
149977
|
-
|
|
150376
|
+
path6.join(outDir, "LICENSE_REVIEW.md"),
|
|
149978
150377
|
buildLicenseReview(host, classified, licenseSummary)
|
|
149979
150378
|
);
|
|
149980
150379
|
log.warn("");
|
|
149981
150380
|
log.warn(`\u2717 All ${licenseSummary.commercial} detected font(s) are served from known commercial CDNs.`);
|
|
149982
150381
|
log.warn(" Downloading and shipping these without a license violates foundry EULAs.");
|
|
149983
|
-
log.warn(` Wrote ${
|
|
150382
|
+
log.warn(` Wrote ${path6.join(outDir, "LICENSE_REVIEW.md")} with the breakdown.`);
|
|
149984
150383
|
log.warn(" To download anyway (e.g. for a local mockup you have rights to), re-run with --force.");
|
|
149985
150384
|
log.warn("");
|
|
149986
150385
|
onProgress?.({ type: "aborted_all_commercial", count: licenseSummary.commercial });
|
|
@@ -150001,10 +150400,11 @@ async function pull({
|
|
|
150001
150400
|
let downloaded = 0;
|
|
150002
150401
|
let index = 0;
|
|
150003
150402
|
const createdBuckets = /* @__PURE__ */ new Set();
|
|
150403
|
+
const fileSizes = /* @__PURE__ */ new Map();
|
|
150004
150404
|
for (const [fontUrl, name] of urlToLocal) {
|
|
150005
150405
|
index++;
|
|
150006
|
-
const dest =
|
|
150007
|
-
const bucketDir =
|
|
150406
|
+
const dest = path6.join(filesDir, name);
|
|
150407
|
+
const bucketDir = path6.dirname(dest);
|
|
150008
150408
|
if (!createdBuckets.has(bucketDir)) {
|
|
150009
150409
|
await fs4.mkdir(bucketDir, { recursive: true });
|
|
150010
150410
|
createdBuckets.add(bucketDir);
|
|
@@ -150012,6 +150412,7 @@ async function pull({
|
|
|
150012
150412
|
try {
|
|
150013
150413
|
const buf = await fetchBuffer(fontUrl, { Referer: url });
|
|
150014
150414
|
await fs4.writeFile(dest, buf);
|
|
150415
|
+
fileSizes.set(name, buf.length);
|
|
150015
150416
|
log.info(` \u2713 ${name} (${buf.length.toLocaleString()} bytes)`);
|
|
150016
150417
|
const bucket = name.includes("/") ? name.split("/")[0] : "self-hosted";
|
|
150017
150418
|
onProgress?.({
|
|
@@ -150052,35 +150453,82 @@ async function pull({
|
|
|
150052
150453
|
}
|
|
150053
150454
|
onProgress?.({ type: "variable_fonts", fonts: variableFonts });
|
|
150054
150455
|
}
|
|
150456
|
+
const collapseOpportunities = detectCollapseOpportunities(variableFonts, faces, fileSizes);
|
|
150457
|
+
if (collapseOpportunities.length > 0) {
|
|
150458
|
+
log.info(" \u2139 Variable-font collapse opportunities:");
|
|
150459
|
+
for (const op of collapseOpportunities) log.info(formatCollapseHint(op));
|
|
150460
|
+
}
|
|
150055
150461
|
let fallbackBlocks = [];
|
|
150056
150462
|
if (fallback) {
|
|
150057
|
-
log.info("\u2192 Computing CLS-killing fallback metrics (capsize)");
|
|
150058
|
-
const { css, count, errors } = await
|
|
150463
|
+
log.info("\u2192 Computing CLS-killing fallback metrics (capsize, per weight)");
|
|
150464
|
+
const { css, count, errors } = await buildPerFaceFallbacks(filesDir, faces);
|
|
150059
150465
|
if (count > 0) {
|
|
150060
150466
|
fallbackBlocks = [css];
|
|
150061
|
-
log.info(` + ${count} fallback @font-face block(s) generated`);
|
|
150467
|
+
log.info(` + ${count} fallback @font-face block(s) generated (one per weight/style)`);
|
|
150062
150468
|
}
|
|
150063
150469
|
for (const err of errors) {
|
|
150064
|
-
log.warn(` ! fallback skipped for ${
|
|
150470
|
+
log.warn(` ! fallback skipped for ${path6.basename(err.file)}: ${err.reason}`);
|
|
150065
150471
|
}
|
|
150066
150472
|
}
|
|
150067
150473
|
const preloadHints = buildPreloadHints(faces);
|
|
150068
150474
|
await fs4.writeFile(
|
|
150069
|
-
|
|
150475
|
+
path6.join(outDir, "fonts.css"),
|
|
150070
150476
|
buildFontsCss(faces, { preloadHints, extraBlocks: fallbackBlocks })
|
|
150071
150477
|
);
|
|
150072
|
-
await fs4.writeFile(
|
|
150073
|
-
await fs4.writeFile(
|
|
150478
|
+
await fs4.writeFile(path6.join(outDir, "fonts.json"), buildFontsJson(faces, orphans));
|
|
150479
|
+
await fs4.writeFile(path6.join(outDir, "README.md"), buildReadme(host, faces, downloaded, orphans));
|
|
150074
150480
|
await fs4.writeFile(
|
|
150075
|
-
|
|
150481
|
+
path6.join(outDir, "LICENSE_REVIEW.md"),
|
|
150076
150482
|
buildLicenseReview(host, refined, refinedSummary)
|
|
150077
150483
|
);
|
|
150484
|
+
await fs4.writeFile(
|
|
150485
|
+
path6.join(outDir, "provenance.json"),
|
|
150486
|
+
buildProvenanceJson(host, url, refined, orphans, fileSizes)
|
|
150487
|
+
);
|
|
150488
|
+
if (gdprReport) {
|
|
150489
|
+
const report = buildGdprReport(host, url, faces);
|
|
150490
|
+
await fs4.writeFile(path6.join(outDir, "GDPR.md"), formatGdprMarkdown(report));
|
|
150491
|
+
await fs4.writeFile(
|
|
150492
|
+
path6.join(outDir, "gdpr.json"),
|
|
150493
|
+
JSON.stringify(report, null, 2)
|
|
150494
|
+
);
|
|
150495
|
+
if (report.summary.highSeverity > 0) {
|
|
150496
|
+
log.info(
|
|
150497
|
+
`\u2192 GDPR review: ${report.summary.thirdParty} third-party request(s), ${report.summary.highSeverity} high-severity \u2014 see GDPR.md`
|
|
150498
|
+
);
|
|
150499
|
+
} else if (report.summary.thirdParty > 0) {
|
|
150500
|
+
log.info(
|
|
150501
|
+
`\u2192 GDPR review: ${report.summary.thirdParty} third-party request(s) \u2014 see GDPR.md`
|
|
150502
|
+
);
|
|
150503
|
+
} else {
|
|
150504
|
+
log.info("\u2192 GDPR review: no third-party font requests detected");
|
|
150505
|
+
}
|
|
150506
|
+
}
|
|
150507
|
+
let consistencyReport;
|
|
150508
|
+
if (fetchedPages.length > 1) {
|
|
150509
|
+
const pageUrls = fetchedPages.map((p) => p.url);
|
|
150510
|
+
const pageFaceMap = buildPageFaceMap(pageUrls, sourceToPage, facesPerSource);
|
|
150511
|
+
consistencyReport = computeConsistency(pageFaceMap);
|
|
150512
|
+
await fs4.writeFile(
|
|
150513
|
+
path6.join(outDir, "CONSISTENCY.md"),
|
|
150514
|
+
buildConsistencyReport(consistencyReport, host)
|
|
150515
|
+
);
|
|
150516
|
+
if (consistencyReport.divergent.length > 0) {
|
|
150517
|
+
log.info(
|
|
150518
|
+
`\u2192 Cross-page consistency: ${consistencyReport.shared.length} shared / ${consistencyReport.divergent.length} divergent page(s) \u2014 see CONSISTENCY.md`
|
|
150519
|
+
);
|
|
150520
|
+
} else {
|
|
150521
|
+
log.info(
|
|
150522
|
+
`\u2192 Cross-page consistency: ${consistencyReport.shared.length} family/families used on every crawled page`
|
|
150523
|
+
);
|
|
150524
|
+
}
|
|
150525
|
+
}
|
|
150078
150526
|
for (const target of emit) {
|
|
150079
150527
|
const emitter = EMITTERS[target];
|
|
150080
150528
|
if (!emitter) continue;
|
|
150081
150529
|
const output = emitter(faces, { siteSlug: host, filesDir: "files" });
|
|
150082
150530
|
if (!output) continue;
|
|
150083
|
-
await fs4.writeFile(
|
|
150531
|
+
await fs4.writeFile(path6.join(outDir, output.filename), output.content);
|
|
150084
150532
|
log.info(` + emitted ${output.filename} (--emit ${target})`);
|
|
150085
150533
|
}
|
|
150086
150534
|
onProgress?.({ type: "phase", phase: "done" });
|
|
@@ -150093,13 +150541,16 @@ async function pull({
|
|
|
150093
150541
|
total: urlToLocal.size,
|
|
150094
150542
|
variableFonts,
|
|
150095
150543
|
pagesCrawled: fetchedPages.length,
|
|
150096
|
-
discoveredNextjsSiblings
|
|
150544
|
+
discoveredNextjsSiblings,
|
|
150545
|
+
...consistencyReport ? { consistency: consistencyReport } : {},
|
|
150546
|
+
fileSizes: Object.fromEntries(fileSizes),
|
|
150547
|
+
collapseOpportunities
|
|
150097
150548
|
};
|
|
150098
150549
|
}
|
|
150099
150550
|
|
|
150100
150551
|
// ../core/src/pipeline/subset.ts
|
|
150101
150552
|
import fs5 from "fs/promises";
|
|
150102
|
-
import
|
|
150553
|
+
import path7 from "path";
|
|
150103
150554
|
|
|
150104
150555
|
// ../core/src/formats/codepoints.ts
|
|
150105
150556
|
var ENTRY_RE = /^U\+([0-9A-Fa-f]{1,6})(?:-([0-9A-Fa-f]{1,6}))?$/;
|
|
@@ -150239,7 +150690,7 @@ async function subset(options) {
|
|
|
150239
150690
|
if (options.skipPull) {
|
|
150240
150691
|
const host = new URL(options.url).hostname.replace(/^www\./, "").replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
150241
150692
|
pullResult = {
|
|
150242
|
-
outDir:
|
|
150693
|
+
outDir: path7.join(path7.resolve(options.baseDir), host),
|
|
150243
150694
|
faces: [],
|
|
150244
150695
|
orphans: [],
|
|
150245
150696
|
downloaded: 0,
|
|
@@ -150267,8 +150718,8 @@ async function subset(options) {
|
|
|
150267
150718
|
if (options.whitelist) extras.push(...options.whitelist);
|
|
150268
150719
|
const codepoints = uniqueCodepoints(renderedText, extras);
|
|
150269
150720
|
log.info(` ${codepoints.length.toLocaleString()} unique codepoint(s) observed`);
|
|
150270
|
-
const filesDir =
|
|
150271
|
-
const fontFiles = await
|
|
150721
|
+
const filesDir = path7.join(pullResult.outDir, "files");
|
|
150722
|
+
const fontFiles = await collectFontFiles2(filesDir);
|
|
150272
150723
|
const perFile = [];
|
|
150273
150724
|
const errors = [];
|
|
150274
150725
|
let totalOriginal = 0;
|
|
@@ -150283,10 +150734,10 @@ async function subset(options) {
|
|
|
150283
150734
|
const savedBytes = original.length - subsetBuffer.length;
|
|
150284
150735
|
const savedPct = original.length > 0 ? savedBytes / original.length * 100 : 0;
|
|
150285
150736
|
log.info(
|
|
150286
|
-
` \u2713 ${
|
|
150737
|
+
` \u2713 ${path7.relative(filesDir, outFile)} ${(subsetBuffer.length / 1024).toFixed(1)} KB (was ${(original.length / 1024).toFixed(1)} KB, \u2212${savedPct.toFixed(0)}%)`
|
|
150287
150738
|
);
|
|
150288
150739
|
perFile.push({
|
|
150289
|
-
file:
|
|
150740
|
+
file: path7.relative(filesDir, outFile),
|
|
150290
150741
|
originalBytes: original.length,
|
|
150291
150742
|
subsetBytes: subsetBuffer.length,
|
|
150292
150743
|
saved: savedBytes,
|
|
@@ -150296,8 +150747,8 @@ async function subset(options) {
|
|
|
150296
150747
|
totalSubset += subsetBuffer.length;
|
|
150297
150748
|
} catch (e) {
|
|
150298
150749
|
const reason = e.message;
|
|
150299
|
-
log.warn(` \u2717 ${
|
|
150300
|
-
errors.push({ file:
|
|
150750
|
+
log.warn(` \u2717 ${path7.relative(filesDir, file)} \u2014 ${reason}`);
|
|
150751
|
+
errors.push({ file: path7.relative(filesDir, file), reason });
|
|
150301
150752
|
}
|
|
150302
150753
|
}
|
|
150303
150754
|
const totalSaved = totalOriginal - totalSubset;
|
|
@@ -150335,8 +150786,8 @@ async function runSplit(pullResult, options, subsetFont) {
|
|
|
150335
150786
|
bucket: b,
|
|
150336
150787
|
codepoints: expandBucket(b)
|
|
150337
150788
|
}));
|
|
150338
|
-
const filesDir =
|
|
150339
|
-
const fontFiles = await
|
|
150789
|
+
const filesDir = path7.join(pullResult.outDir, "files");
|
|
150790
|
+
const fontFiles = await collectFontFiles2(filesDir);
|
|
150340
150791
|
const perFile = [];
|
|
150341
150792
|
const splits = [];
|
|
150342
150793
|
const errors = [];
|
|
@@ -150356,19 +150807,19 @@ async function runSplit(pullResult, options, subsetFont) {
|
|
|
150356
150807
|
try {
|
|
150357
150808
|
original = await fs5.readFile(file);
|
|
150358
150809
|
} catch (e) {
|
|
150359
|
-
errors.push({ file:
|
|
150810
|
+
errors.push({ file: path7.relative(filesDir, file), reason: e.message });
|
|
150360
150811
|
continue;
|
|
150361
150812
|
}
|
|
150362
150813
|
let charsetInfo;
|
|
150363
150814
|
try {
|
|
150364
150815
|
charsetInfo = readFontCharset(original);
|
|
150365
150816
|
} catch (e) {
|
|
150366
|
-
errors.push({ file:
|
|
150817
|
+
errors.push({ file: path7.relative(filesDir, file), reason: `fontkit: ${e.message}` });
|
|
150367
150818
|
continue;
|
|
150368
150819
|
}
|
|
150369
|
-
const localRel =
|
|
150820
|
+
const localRel = path7.relative(filesDir, file);
|
|
150370
150821
|
const faceMeta = faceByLocal.get(localRel);
|
|
150371
|
-
const family = faceMeta?.family ?? charsetInfo.family ??
|
|
150822
|
+
const family = faceMeta?.family ?? charsetInfo.family ?? path7.basename(file, path7.extname(file));
|
|
150372
150823
|
const familyReport = {
|
|
150373
150824
|
family,
|
|
150374
150825
|
sourceFile: localRel,
|
|
@@ -150390,7 +150841,7 @@ async function runSplit(pullResult, options, subsetFont) {
|
|
|
150390
150841
|
`.${bucket.name}.subset.woff2`
|
|
150391
150842
|
);
|
|
150392
150843
|
await fs5.writeFile(outFile, subsetBuffer);
|
|
150393
|
-
const outRel =
|
|
150844
|
+
const outRel = path7.relative(filesDir, outFile);
|
|
150394
150845
|
const savedBytes = original.length - subsetBuffer.length;
|
|
150395
150846
|
const savedPct = original.length > 0 ? savedBytes / original.length * 100 : 0;
|
|
150396
150847
|
log.info(
|
|
@@ -150424,7 +150875,7 @@ async function runSplit(pullResult, options, subsetFont) {
|
|
|
150424
150875
|
splits.push(familyReport);
|
|
150425
150876
|
}
|
|
150426
150877
|
}
|
|
150427
|
-
const splitCssPath =
|
|
150878
|
+
const splitCssPath = path7.join(pullResult.outDir, "fonts.subset.css");
|
|
150428
150879
|
await fs5.writeFile(splitCssPath, buildSplitCss(splits, expandedBuckets, faceByLocal));
|
|
150429
150880
|
const totalSaved = totalOriginal - totalSubset;
|
|
150430
150881
|
const totalSavedPct = totalOriginal > 0 ? totalSaved / totalOriginal * 100 : 0;
|
|
@@ -150442,7 +150893,7 @@ async function runSplit(pullResult, options, subsetFont) {
|
|
|
150442
150893
|
perFile,
|
|
150443
150894
|
errors,
|
|
150444
150895
|
splits,
|
|
150445
|
-
splitCss:
|
|
150896
|
+
splitCss: path7.relative(pullResult.outDir, splitCssPath)
|
|
150446
150897
|
};
|
|
150447
150898
|
}
|
|
150448
150899
|
function buildSplitCss(splits, expandedBuckets, faceByLocal) {
|
|
@@ -150477,7 +150928,7 @@ function buildSplitCss(splits, expandedBuckets, faceByLocal) {
|
|
|
150477
150928
|
}
|
|
150478
150929
|
return lines.join("\n");
|
|
150479
150930
|
}
|
|
150480
|
-
async function
|
|
150931
|
+
async function collectFontFiles2(root) {
|
|
150481
150932
|
const out = [];
|
|
150482
150933
|
async function walk(dir) {
|
|
150483
150934
|
let items;
|
|
@@ -150487,7 +150938,7 @@ async function collectFontFiles3(root) {
|
|
|
150487
150938
|
return;
|
|
150488
150939
|
}
|
|
150489
150940
|
for (const it of items) {
|
|
150490
|
-
const p =
|
|
150941
|
+
const p = path7.join(dir, it.name);
|
|
150491
150942
|
if (it.isDirectory()) await walk(p);
|
|
150492
150943
|
else if (/\.(woff2|woff|ttf|otf)$/i.test(it.name)) out.push(p);
|
|
150493
150944
|
}
|
|
@@ -150511,8 +150962,204 @@ function emptyReport(outDir, url) {
|
|
|
150511
150962
|
};
|
|
150512
150963
|
}
|
|
150513
150964
|
|
|
150965
|
+
// ../core/src/pipeline/diff.ts
|
|
150966
|
+
async function diffPulls(urlA, urlB, baseDir, options = {}) {
|
|
150967
|
+
const [resultA, resultB] = await Promise.all([
|
|
150968
|
+
pull({ url: urlA, baseDir, ...options }),
|
|
150969
|
+
pull({ url: urlB, baseDir, ...options })
|
|
150970
|
+
]);
|
|
150971
|
+
const sideA = summarise(urlA, resultA.faces, resultA.fileSizes);
|
|
150972
|
+
const sideB = summarise(urlB, resultB.faces, resultB.fileSizes);
|
|
150973
|
+
const famSetA = new Set(sideA.families);
|
|
150974
|
+
const famSetB = new Set(sideB.families);
|
|
150975
|
+
const added = [...famSetB].filter((f) => !famSetA.has(f)).sort();
|
|
150976
|
+
const removed = [...famSetA].filter((f) => !famSetB.has(f)).sort();
|
|
150977
|
+
const shared = [...famSetA].filter((f) => famSetB.has(f)).sort();
|
|
150978
|
+
return {
|
|
150979
|
+
schemaVersion: "1.0",
|
|
150980
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
150981
|
+
a: sideA,
|
|
150982
|
+
b: sideB,
|
|
150983
|
+
added,
|
|
150984
|
+
removed,
|
|
150985
|
+
shared,
|
|
150986
|
+
byteDelta: sideB.totalBytes - sideA.totalBytes,
|
|
150987
|
+
commercialDelta: sideB.commercialCount - sideA.commercialCount
|
|
150988
|
+
};
|
|
150989
|
+
}
|
|
150990
|
+
function summarise(url, faces, fileSizes) {
|
|
150991
|
+
const families = [...new Set(faces.map((f) => f.family))].sort();
|
|
150992
|
+
const sizes = fileSizes ?? {};
|
|
150993
|
+
let totalBytes = 0;
|
|
150994
|
+
let fileCount = 0;
|
|
150995
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
150996
|
+
for (const f of faces) {
|
|
150997
|
+
for (const s of f.sources) {
|
|
150998
|
+
if (!s.localFile) continue;
|
|
150999
|
+
if (seenFiles.has(s.localFile)) continue;
|
|
151000
|
+
seenFiles.add(s.localFile);
|
|
151001
|
+
fileCount++;
|
|
151002
|
+
totalBytes += sizes[s.localFile] ?? 0;
|
|
151003
|
+
}
|
|
151004
|
+
}
|
|
151005
|
+
const classified = classifyFaces(faces);
|
|
151006
|
+
const commercialCount = classified.filter((c) => c.classification.status === "commercial").length;
|
|
151007
|
+
return { url, families, totalBytes, commercialCount, fileCount };
|
|
151008
|
+
}
|
|
151009
|
+
function formatFontDiff(diff) {
|
|
151010
|
+
const lines = [
|
|
151011
|
+
`# Font diff`,
|
|
151012
|
+
"",
|
|
151013
|
+
`- **A** ${diff.a.url}`,
|
|
151014
|
+
`- **B** ${diff.b.url}`,
|
|
151015
|
+
"",
|
|
151016
|
+
`## Summary`,
|
|
151017
|
+
"",
|
|
151018
|
+
`- Added: ${diff.added.length} family/families`,
|
|
151019
|
+
`- Removed: ${diff.removed.length} family/families`,
|
|
151020
|
+
`- Shared: ${diff.shared.length} family/families`,
|
|
151021
|
+
`- Byte delta: ${formatBytes(diff.byteDelta)}`,
|
|
151022
|
+
`- Commercial delta: ${signed(diff.commercialDelta)} face(s)`,
|
|
151023
|
+
""
|
|
151024
|
+
];
|
|
151025
|
+
if (diff.added.length > 0) {
|
|
151026
|
+
lines.push("## Added (only in B)");
|
|
151027
|
+
lines.push("");
|
|
151028
|
+
for (const f of diff.added) lines.push(`- ${f}`);
|
|
151029
|
+
lines.push("");
|
|
151030
|
+
}
|
|
151031
|
+
if (diff.removed.length > 0) {
|
|
151032
|
+
lines.push("## Removed (only in A)");
|
|
151033
|
+
lines.push("");
|
|
151034
|
+
for (const f of diff.removed) lines.push(`- ${f}`);
|
|
151035
|
+
lines.push("");
|
|
151036
|
+
}
|
|
151037
|
+
if (diff.shared.length > 0) {
|
|
151038
|
+
lines.push("## Shared");
|
|
151039
|
+
lines.push("");
|
|
151040
|
+
for (const f of diff.shared) lines.push(`- ${f}`);
|
|
151041
|
+
lines.push("");
|
|
151042
|
+
}
|
|
151043
|
+
return lines.join("\n");
|
|
151044
|
+
}
|
|
151045
|
+
function formatBytes(bytes) {
|
|
151046
|
+
const sign = bytes >= 0 ? "+" : "\u2212";
|
|
151047
|
+
const abs2 = Math.abs(bytes);
|
|
151048
|
+
if (abs2 < 1024) return `${sign}${abs2} B`;
|
|
151049
|
+
if (abs2 < 1024 * 1024) return `${sign}${(abs2 / 1024).toFixed(1)} KB`;
|
|
151050
|
+
return `${sign}${(abs2 / (1024 * 1024)).toFixed(1)} MB`;
|
|
151051
|
+
}
|
|
151052
|
+
function signed(n) {
|
|
151053
|
+
if (n === 0) return "0";
|
|
151054
|
+
return n > 0 ? `+${n}` : `${n}`;
|
|
151055
|
+
}
|
|
151056
|
+
|
|
151057
|
+
// ../core/src/pipeline/audit.ts
|
|
151058
|
+
async function audit(url, baseDir, options = {}) {
|
|
151059
|
+
const result = await pull({ url, baseDir, ...options.pull });
|
|
151060
|
+
const classified = classifyFaces(result.faces);
|
|
151061
|
+
const sizes = result.fileSizes ?? {};
|
|
151062
|
+
const perFamilyBytes = perFamily(result.faces, sizes);
|
|
151063
|
+
const totalBytes = Object.values(perFamilyBytes).reduce((a, b) => a + b, 0);
|
|
151064
|
+
const files = countFiles(result.faces);
|
|
151065
|
+
const byStatus = { open: 0, commercial: 0, unknown: 0 };
|
|
151066
|
+
for (const c of classified) byStatus[c.classification.status]++;
|
|
151067
|
+
const violations = [];
|
|
151068
|
+
if (options.maxKb !== void 0 && totalBytes / 1024 > options.maxKb) {
|
|
151069
|
+
violations.push({
|
|
151070
|
+
type: "budget_exceeded",
|
|
151071
|
+
message: `Total font bundle is ${(totalBytes / 1024).toFixed(1)} KB, exceeds budget of ${options.maxKb} KB`,
|
|
151072
|
+
detail: { actualKb: Math.round(totalBytes / 1024), budgetKb: options.maxKb }
|
|
151073
|
+
});
|
|
151074
|
+
}
|
|
151075
|
+
if (options.perFamilyKb) {
|
|
151076
|
+
for (const [family, budget] of Object.entries(options.perFamilyKb)) {
|
|
151077
|
+
const actual = perFamilyBytes[family] ?? 0;
|
|
151078
|
+
if (actual / 1024 > budget) {
|
|
151079
|
+
violations.push({
|
|
151080
|
+
type: "family_budget_exceeded",
|
|
151081
|
+
message: `Family "${family}" is ${(actual / 1024).toFixed(1)} KB, exceeds budget of ${budget} KB`,
|
|
151082
|
+
detail: { family, actualKb: Math.round(actual / 1024), budgetKb: budget }
|
|
151083
|
+
});
|
|
151084
|
+
}
|
|
151085
|
+
}
|
|
151086
|
+
}
|
|
151087
|
+
if (options.noCommercial && byStatus.commercial > 0) {
|
|
151088
|
+
const offenders = classified.filter((c) => c.classification.status === "commercial").map((c) => c.face.family);
|
|
151089
|
+
violations.push({
|
|
151090
|
+
type: "commercial_present",
|
|
151091
|
+
message: `${byStatus.commercial} commercial face(s) detected: ${[...new Set(offenders)].join(", ")}`,
|
|
151092
|
+
detail: { count: byStatus.commercial, families: [...new Set(offenders)].join(", ") }
|
|
151093
|
+
});
|
|
151094
|
+
}
|
|
151095
|
+
return {
|
|
151096
|
+
schemaVersion: "1.0",
|
|
151097
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151098
|
+
url,
|
|
151099
|
+
passed: violations.length === 0,
|
|
151100
|
+
violations,
|
|
151101
|
+
summary: {
|
|
151102
|
+
families: new Set(result.faces.map((f) => f.family)).size,
|
|
151103
|
+
faces: result.faces.length,
|
|
151104
|
+
files,
|
|
151105
|
+
totalBytes,
|
|
151106
|
+
byStatus,
|
|
151107
|
+
perFamilyBytes
|
|
151108
|
+
}
|
|
151109
|
+
};
|
|
151110
|
+
}
|
|
151111
|
+
function perFamily(faces, sizes) {
|
|
151112
|
+
const out = {};
|
|
151113
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
151114
|
+
for (const f of faces) {
|
|
151115
|
+
let familyBytes = 0;
|
|
151116
|
+
for (const s of f.sources) {
|
|
151117
|
+
if (!s.localFile) continue;
|
|
151118
|
+
if (seenFiles.has(s.localFile)) continue;
|
|
151119
|
+
seenFiles.add(s.localFile);
|
|
151120
|
+
familyBytes += sizes[s.localFile] ?? 0;
|
|
151121
|
+
}
|
|
151122
|
+
out[f.family] = (out[f.family] ?? 0) + familyBytes;
|
|
151123
|
+
}
|
|
151124
|
+
return out;
|
|
151125
|
+
}
|
|
151126
|
+
function countFiles(faces) {
|
|
151127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
151128
|
+
for (const f of faces) {
|
|
151129
|
+
for (const s of f.sources) {
|
|
151130
|
+
if (s.localFile) seen.add(s.localFile);
|
|
151131
|
+
}
|
|
151132
|
+
}
|
|
151133
|
+
return seen.size;
|
|
151134
|
+
}
|
|
151135
|
+
function formatAuditReport(report) {
|
|
151136
|
+
const lines = [];
|
|
151137
|
+
lines.push(`# Audit: ${report.url}`);
|
|
151138
|
+
lines.push("");
|
|
151139
|
+
if (report.passed) {
|
|
151140
|
+
lines.push(`\u2713 Passed. ${report.summary.faces} face(s), ${(report.summary.totalBytes / 1024).toFixed(1)} KB total.`);
|
|
151141
|
+
} else {
|
|
151142
|
+
lines.push(`\u2717 Failed with ${report.violations.length} violation(s).`);
|
|
151143
|
+
lines.push("");
|
|
151144
|
+
for (const v of report.violations) {
|
|
151145
|
+
lines.push(`- **${v.type}**: ${v.message}`);
|
|
151146
|
+
}
|
|
151147
|
+
}
|
|
151148
|
+
lines.push("");
|
|
151149
|
+
lines.push("## Summary");
|
|
151150
|
+
lines.push("");
|
|
151151
|
+
lines.push(`- Families: ${report.summary.families}`);
|
|
151152
|
+
lines.push(`- Faces: ${report.summary.faces}`);
|
|
151153
|
+
lines.push(`- Files: ${report.summary.files}`);
|
|
151154
|
+
lines.push(`- Total bytes: ${(report.summary.totalBytes / 1024).toFixed(1)} KB`);
|
|
151155
|
+
lines.push(
|
|
151156
|
+
`- License: ${report.summary.byStatus.open} open / ${report.summary.byStatus.commercial} commercial / ${report.summary.byStatus.unknown} unknown`
|
|
151157
|
+
);
|
|
151158
|
+
return lines.join("\n");
|
|
151159
|
+
}
|
|
151160
|
+
|
|
150514
151161
|
// src/cli.ts
|
|
150515
|
-
var VERSION = "1.
|
|
151162
|
+
var VERSION = "1.4.0";
|
|
150516
151163
|
function printHelp() {
|
|
150517
151164
|
log.info(`fontfetch ${VERSION}
|
|
150518
151165
|
|
|
@@ -150520,6 +151167,9 @@ Usage:
|
|
|
150520
151167
|
fontfetch <url> [outDir] [flags] Extract every webfont from a URL
|
|
150521
151168
|
fontfetch inspect <file> Print a terminal report for a font file
|
|
150522
151169
|
fontfetch subset <url> [outDir] Extract + subset to glyphs actually rendered on the page
|
|
151170
|
+
fontfetch diff <urlA> <urlB> Diff the font set between two URLs (v1.4)
|
|
151171
|
+
fontfetch audit <url> [flags] CI-friendly checks: budget, no-commercial (v1.4, non-zero exit on fail)
|
|
151172
|
+
fontfetch budget <url> --max-kb N Convenience around audit for the bundle-size dimension (v1.4)
|
|
150523
151173
|
|
|
150524
151174
|
Arguments (default command):
|
|
150525
151175
|
<url> Page to download fonts from (https://example.com)
|
|
@@ -150547,6 +151197,11 @@ Flags (default command):
|
|
|
150547
151197
|
--emit tailwind Tailwind fontFamily snippet
|
|
150548
151198
|
--emit next,tailwind Both (pair for CSS variables)
|
|
150549
151199
|
--emit vite Vite integration guide
|
|
151200
|
+
--emit tokens W3C / Style Dictionary design tokens (v1.4)
|
|
151201
|
+
--gdpr-report Emit GDPR.md + gdpr.json listing every third-party font
|
|
151202
|
+
request with self-host remediation. Post-LG M\xFCnchen I
|
|
151203
|
+
20 O 1393/21 (2022) German court ruling on Google Fonts
|
|
151204
|
+
CDN. (v1.4)
|
|
150550
151205
|
--fallback Emit a CLS-killing 'Fallback' @font-face for every family,
|
|
150551
151206
|
with size-adjust / ascent-override / descent-override /
|
|
150552
151207
|
line-gap-override matched to a system fallback (Arial /
|
|
@@ -150592,6 +151247,19 @@ Examples:
|
|
|
150592
151247
|
fontfetch subset https://stripe.com --split-ranges=latin,latin-ext
|
|
150593
151248
|
npx fontfetch https://shinobidata.com
|
|
150594
151249
|
|
|
151250
|
+
Flags (audit / budget subcommand, v1.4):
|
|
151251
|
+
--max-kb <N> Total bundle size budget in KB; exceeds \u2192 exit 1
|
|
151252
|
+
--per-family-kb <list>
|
|
151253
|
+
Per-family budgets: "Inter:30,Geist:40"
|
|
151254
|
+
--no-commercial Exit 1 if any face is classified as commercial
|
|
151255
|
+
--json Emit machine-readable JSON instead of human-readable output
|
|
151256
|
+
|
|
151257
|
+
Examples (v1.4):
|
|
151258
|
+
fontfetch diff https://staging.acme.com https://acme.com
|
|
151259
|
+
fontfetch diff https://staging.acme.com https://acme.com --json
|
|
151260
|
+
fontfetch audit https://acme.com --max-kb 200 --no-commercial
|
|
151261
|
+
fontfetch budget https://acme.com --max-kb 100 --json
|
|
151262
|
+
|
|
150595
151263
|
Output (per site):
|
|
150596
151264
|
<outDir>/<hostname>/
|
|
150597
151265
|
files/ Raw font files (woff2/woff/ttf/otf/eot)
|
|
@@ -150600,6 +151268,9 @@ Output (per site):
|
|
|
150600
151268
|
fonts.json Manifest grouped by family/weight/style
|
|
150601
151269
|
README.md Human-readable summary
|
|
150602
151270
|
LICENSE_REVIEW.md Per-face license verdict (open / commercial / unknown)
|
|
151271
|
+
provenance.json Machine-readable license + provenance report (v1.4)
|
|
151272
|
+
CONSISTENCY.md Cross-page font consistency (when --pages > 1, v1.4)
|
|
151273
|
+
fonts.tokens.json W3C design tokens (when --emit tokens, v1.4)
|
|
150603
151274
|
|
|
150604
151275
|
For local design exploration. You're responsible for licensing the fonts you use.
|
|
150605
151276
|
`);
|
|
@@ -150696,6 +151367,7 @@ async function runPull(args) {
|
|
|
150696
151367
|
const headless = args.includes("--headless");
|
|
150697
151368
|
const force = args.includes("--force");
|
|
150698
151369
|
const fallback = args.includes("--fallback");
|
|
151370
|
+
const gdprReport = args.includes("--gdpr-report");
|
|
150699
151371
|
const emit = [];
|
|
150700
151372
|
const emitIdx = args.findIndex((a) => a === "--emit" || a.startsWith("--emit="));
|
|
150701
151373
|
if (emitIdx !== -1) {
|
|
@@ -150756,7 +151428,8 @@ async function runPull(args) {
|
|
|
150756
151428
|
"--force",
|
|
150757
151429
|
"--fallback",
|
|
150758
151430
|
"--pages",
|
|
150759
|
-
"--formats"
|
|
151431
|
+
"--formats",
|
|
151432
|
+
"--gdpr-report"
|
|
150760
151433
|
]);
|
|
150761
151434
|
const positional = args.filter((a, i) => {
|
|
150762
151435
|
if (a.startsWith("--")) return false;
|
|
@@ -150784,7 +151457,8 @@ async function runPull(args) {
|
|
|
150784
151457
|
force,
|
|
150785
151458
|
fallback,
|
|
150786
151459
|
pages,
|
|
150787
|
-
formats: formats2
|
|
151460
|
+
formats: formats2,
|
|
151461
|
+
gdprReport
|
|
150788
151462
|
});
|
|
150789
151463
|
log.info("");
|
|
150790
151464
|
if (result.total === 0) {
|
|
@@ -150793,6 +151467,109 @@ async function runPull(args) {
|
|
|
150793
151467
|
}
|
|
150794
151468
|
log.info(`Done. ${result.downloaded}/${result.total} files saved to ${result.outDir}`);
|
|
150795
151469
|
}
|
|
151470
|
+
async function runDiff(args) {
|
|
151471
|
+
const wantJson = args.includes("--json");
|
|
151472
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
151473
|
+
const [urlA, urlB, outDir = "./downloaded-fonts"] = positional;
|
|
151474
|
+
if (!urlA || !urlB) {
|
|
151475
|
+
log.err("Usage: fontfetch diff <urlA> <urlB> [outDir] [--json]");
|
|
151476
|
+
process.exit(1);
|
|
151477
|
+
}
|
|
151478
|
+
try {
|
|
151479
|
+
new URL(urlA);
|
|
151480
|
+
new URL(urlB);
|
|
151481
|
+
} catch {
|
|
151482
|
+
log.err(`Invalid URL passed to diff`);
|
|
151483
|
+
process.exit(1);
|
|
151484
|
+
}
|
|
151485
|
+
try {
|
|
151486
|
+
const diff = await diffPulls(urlA, urlB, outDir);
|
|
151487
|
+
if (wantJson) {
|
|
151488
|
+
process.stdout.write(JSON.stringify(diff, null, 2) + "\n");
|
|
151489
|
+
} else {
|
|
151490
|
+
log.info("");
|
|
151491
|
+
log.info(formatFontDiff(diff));
|
|
151492
|
+
}
|
|
151493
|
+
process.exit(0);
|
|
151494
|
+
} catch (e) {
|
|
151495
|
+
log.err(`diff failed: ${e.message}`);
|
|
151496
|
+
process.exit(1);
|
|
151497
|
+
}
|
|
151498
|
+
}
|
|
151499
|
+
async function runAudit(args) {
|
|
151500
|
+
const wantJson = args.includes("--json");
|
|
151501
|
+
const noCommercial = args.includes("--no-commercial");
|
|
151502
|
+
let maxKb;
|
|
151503
|
+
const maxKbIdx = args.findIndex((a) => a === "--max-kb" || a.startsWith("--max-kb="));
|
|
151504
|
+
if (maxKbIdx !== -1) {
|
|
151505
|
+
const raw = args[maxKbIdx] === "--max-kb" ? args[maxKbIdx + 1] : args[maxKbIdx].slice("--max-kb=".length);
|
|
151506
|
+
const n = Number.parseInt(raw, 10);
|
|
151507
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
151508
|
+
log.err(`--max-kb must be a positive integer, got '${raw}'`);
|
|
151509
|
+
process.exit(1);
|
|
151510
|
+
}
|
|
151511
|
+
maxKb = n;
|
|
151512
|
+
}
|
|
151513
|
+
let perFamilyKb;
|
|
151514
|
+
const pfIdx = args.findIndex(
|
|
151515
|
+
(a) => a === "--per-family-kb" || a.startsWith("--per-family-kb=")
|
|
151516
|
+
);
|
|
151517
|
+
if (pfIdx !== -1) {
|
|
151518
|
+
const raw = args[pfIdx] === "--per-family-kb" ? args[pfIdx + 1] : args[pfIdx].slice("--per-family-kb=".length);
|
|
151519
|
+
perFamilyKb = {};
|
|
151520
|
+
for (const part of raw.split(",")) {
|
|
151521
|
+
const [family, kb] = part.split(":");
|
|
151522
|
+
if (!family || !kb) {
|
|
151523
|
+
log.err(`--per-family-kb expects 'Family:KB' pairs; got '${part}'`);
|
|
151524
|
+
process.exit(1);
|
|
151525
|
+
}
|
|
151526
|
+
const n = Number.parseInt(kb, 10);
|
|
151527
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
151528
|
+
log.err(`--per-family-kb value must be positive integer; got '${kb}'`);
|
|
151529
|
+
process.exit(1);
|
|
151530
|
+
}
|
|
151531
|
+
perFamilyKb[family.trim()] = n;
|
|
151532
|
+
}
|
|
151533
|
+
}
|
|
151534
|
+
const reserved = /* @__PURE__ */ new Set(["--json", "--no-commercial", "--max-kb", "--per-family-kb"]);
|
|
151535
|
+
const positional = args.filter((a, i) => {
|
|
151536
|
+
if (a.startsWith("--")) return false;
|
|
151537
|
+
if (i > 0 && (args[i - 1] === "--max-kb" || args[i - 1] === "--per-family-kb")) return false;
|
|
151538
|
+
if (reserved.has(a)) return false;
|
|
151539
|
+
return true;
|
|
151540
|
+
});
|
|
151541
|
+
const [url, outDir = "./downloaded-fonts"] = positional;
|
|
151542
|
+
if (!url) {
|
|
151543
|
+
log.err("Usage: fontfetch audit <url> [outDir] [--max-kb N] [--no-commercial] [--per-family-kb F:N,...] [--json]");
|
|
151544
|
+
process.exit(1);
|
|
151545
|
+
}
|
|
151546
|
+
try {
|
|
151547
|
+
new URL(url);
|
|
151548
|
+
} catch {
|
|
151549
|
+
log.err(`Invalid URL: ${url}`);
|
|
151550
|
+
process.exit(1);
|
|
151551
|
+
}
|
|
151552
|
+
try {
|
|
151553
|
+
const report = await audit(url, outDir, { maxKb, perFamilyKb, noCommercial });
|
|
151554
|
+
if (wantJson) {
|
|
151555
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
151556
|
+
} else {
|
|
151557
|
+
log.info("");
|
|
151558
|
+
log.info(formatAuditReport(report));
|
|
151559
|
+
}
|
|
151560
|
+
process.exit(report.passed ? 0 : 1);
|
|
151561
|
+
} catch (e) {
|
|
151562
|
+
log.err(`audit failed: ${e.message}`);
|
|
151563
|
+
process.exit(1);
|
|
151564
|
+
}
|
|
151565
|
+
}
|
|
151566
|
+
async function runBudget(args) {
|
|
151567
|
+
if (!args.some((a) => a === "--max-kb" || a.startsWith("--max-kb="))) {
|
|
151568
|
+
log.err("Usage: fontfetch budget <url> --max-kb <N> [outDir] [--json]");
|
|
151569
|
+
process.exit(1);
|
|
151570
|
+
}
|
|
151571
|
+
await runAudit(args);
|
|
151572
|
+
}
|
|
150796
151573
|
async function main() {
|
|
150797
151574
|
const args = process.argv.slice(2);
|
|
150798
151575
|
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
|
|
@@ -150812,6 +151589,18 @@ async function main() {
|
|
|
150812
151589
|
await runSubset(rest);
|
|
150813
151590
|
return;
|
|
150814
151591
|
}
|
|
151592
|
+
if (command === "diff") {
|
|
151593
|
+
await runDiff(rest);
|
|
151594
|
+
return;
|
|
151595
|
+
}
|
|
151596
|
+
if (command === "audit") {
|
|
151597
|
+
await runAudit(rest);
|
|
151598
|
+
return;
|
|
151599
|
+
}
|
|
151600
|
+
if (command === "budget") {
|
|
151601
|
+
await runBudget(rest);
|
|
151602
|
+
return;
|
|
151603
|
+
}
|
|
150815
151604
|
await runPull(args);
|
|
150816
151605
|
}
|
|
150817
151606
|
main().catch((e) => {
|