@aglyn/shared-ui-theme 1.0.0-beta.230 → 1.0.0-beta.232

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 (33) hide show
  1. package/package.json +4 -4
  2. package/src/lib/console-app.theme.d.ts +25 -0
  3. package/src/lib/console-app.theme.js +142 -0
  4. package/src/lib/console-app.theme.js.map +1 -0
  5. package/src/lib/console.theme.js +8 -2
  6. package/src/lib/console.theme.js.map +1 -1
  7. package/src/lib/constants.d.ts +2 -0
  8. package/src/lib/constants.js +1 -0
  9. package/src/lib/constants.js.map +1 -1
  10. package/src/lib/platform-brand.d.ts +25 -0
  11. package/src/lib/platform-brand.js +40 -0
  12. package/src/lib/platform-brand.js.map +1 -0
  13. package/src/lib/site-base-fonts.d.ts +54 -0
  14. package/src/lib/site-base-fonts.js +125 -0
  15. package/src/lib/site-base-fonts.js.map +1 -0
  16. package/src/lib/tenant.theme.d.ts +1 -2
  17. package/src/lib/tenant.theme.js +8 -30
  18. package/src/lib/tenant.theme.js.map +1 -1
  19. package/src/lib/util/font-stack.d.ts +44 -0
  20. package/src/lib/util/font-stack.js +79 -0
  21. package/src/lib/util/font-stack.js.map +1 -0
  22. package/src/lib/util/roboto-flex.d.ts +65 -0
  23. package/src/lib/util/roboto-flex.js +115 -0
  24. package/src/lib/util/roboto-flex.js.map +1 -0
  25. package/src/lib/util/self-hosted-fonts.d.ts +111 -1
  26. package/src/lib/util/self-hosted-fonts.js +349 -2
  27. package/src/lib/util/self-hosted-fonts.js.map +1 -1
  28. package/src/lib/util/theme-editor-edits.d.ts +105 -0
  29. package/src/lib/util/theme-editor-edits.js +168 -0
  30. package/src/lib/util/theme-editor-edits.js.map +1 -0
  31. package/src/lib/util/theme-editor-fields.d.ts +53 -2
  32. package/src/lib/util/theme-editor-fields.js +171 -20
  33. package/src/lib/util/theme-editor-fields.js.map +1 -1
@@ -14,7 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { HostTheme } from '@aglyn/shared-data-types';
17
+ import type { HostTheme, HostThemeFont, HostThemeFontCategory, HostThemeFontMetrics } from '@aglyn/shared-data-types';
18
+ export { metricFallbackFamily, themeWebFontFamilies, withMetricFallbacks } from './font-stack';
18
19
  /**
19
20
  * A theme's Google fonts, served from the site's own origin (AGL-3485).
20
21
  *
@@ -94,3 +95,112 @@ export declare function selfHostedFontFaceCss(faces: readonly GoogleFontFace[]):
94
95
  * from one file, which is then preloaded once.
95
96
  */
96
97
  export declare function selfHostedFontPreloads(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, faces: readonly GoogleFontFace[]): string[];
98
+ /**
99
+ * One face a page declares, wherever its file lives: a Google face rewritten
100
+ * onto {@link SELF_HOSTED_FONT_ROUTE}, or a site's own upload on its media
101
+ * route. Both are on the site's own origin.
102
+ */
103
+ export interface SiteFontFace {
104
+ family: string;
105
+ style: 'normal' | 'italic';
106
+ /** A weight (`400`) or, for a variable file, a range (`300 900`). */
107
+ weight: string;
108
+ /** Site-relative URL of the WOFF2 file. */
109
+ url: string;
110
+ /** Google's subset name (`latin`, …); absent for a file with no split. */
111
+ subset?: string;
112
+ unicodeRange?: string;
113
+ }
114
+ /** A Google face as a {@link SiteFontFace} on the site's own route. */
115
+ export declare function siteFontFaceFromGoogle(face: GoogleFontFace): SiteFontFace;
116
+ /**
117
+ * The faces with every set of rules that share one file folded into one rule
118
+ * over the weights it covers.
119
+ *
120
+ * Google answers a request for several weights of a variable family with the
121
+ * SAME file once per weight, each rule naming one weight. A browser treats
122
+ * those as separate faces: it matches 500 to nothing between the 400 and 600
123
+ * rules a theme listed and renders it as 400, though the file holds every
124
+ * weight in between. One rule declaring the range (`400 700`) is what the
125
+ * file is.
126
+ */
127
+ export declare function mergeSharedFiles<T extends {
128
+ url: string;
129
+ weight: string;
130
+ family: string;
131
+ style: string;
132
+ unicodeRange?: string;
133
+ }>(faces: readonly T[]): T[];
134
+ /** The rules for a page's faces, every one `font-display: swap`. */
135
+ export declare function siteFontFaceCss(faces: readonly SiteFontFace[]): string;
136
+ /**
137
+ * Whether a URL is one a rule may name: site-relative, on the font route or
138
+ * the media route, with nothing that could close the `url()` it is written
139
+ * into. A face whose URL fails is dropped rather than escaped.
140
+ */
141
+ export declare function isSiteFontUrl(url: string): boolean;
142
+ /**
143
+ * The `@font-face` rule for a family's metric-matched fallback: the local face
144
+ * of its category, scaled so its average character is as wide as the web
145
+ * font's (`size-adjust`) and given the web font's ascent, descent and line gap
146
+ * at that scale. Text set in it occupies the box the web font will, so the
147
+ * swap moves nothing — the CLS a `font-display: swap` page otherwise pays.
148
+ *
149
+ * The overrides are divided by the size adjustment because the browser
150
+ * applies them to the ADJUSTED size: an ascent of 0.97em on a face scaled to
151
+ * 1.07 has to be written as 0.97/1.07 to land at 0.97em.
152
+ *
153
+ * Empty for metrics that are missing or not numbers.
154
+ */
155
+ export declare function metricFallbackFontFaceCss(family: string, metrics: HostThemeFontMetrics | undefined, category?: HostThemeFontCategory): string;
156
+ /** What one family of a theme needs loaded: the weights, and which as italics. */
157
+ export interface ThemeFontNeed {
158
+ family: string;
159
+ /** Upright weights the theme's text styles and named weights draw with. */
160
+ weights: number[];
161
+ /** The subset of `weights` text styles alone use, which a delivery choice is measured on. */
162
+ textWeights: number[];
163
+ italics: number[];
164
+ }
165
+ /**
166
+ * The faces each of a theme's web font families is drawn with (AGL-3656).
167
+ *
168
+ * A theme lists weights for its font (`fonts[].weights`), but the page draws
169
+ * with whatever its text styles say: the platform's ramp sets `h1` in 900 and
170
+ * `h2` in 800, so a theme that loaded Inter at 400, 500 and 700 drew its
171
+ * headlines in a synthesized or substituted weight. The weights here are the
172
+ * union of the listed ones, every text style's weight whose family resolves to
173
+ * this one, and the theme's named weights (`fontWeightLight` … `Black`), which
174
+ * the weight picker offers content.
175
+ *
176
+ * `baseTypography` is the typography the site's theme is layered onto
177
+ * (`siteBaseOptions(...).typography`): a text style the theme leaves alone
178
+ * keeps the base's weight and family.
179
+ *
180
+ * One italic is loaded per family, at the body text's weight, so emphasis in
181
+ * running text is a real italic rather than a slanted upright; a theme can
182
+ * list more under `italics`.
183
+ */
184
+ export declare function themeFontNeeds(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, baseTypography: Record<string, unknown> | undefined, extraFonts?: readonly HostThemeFont[]): ThemeFontNeed[];
185
+ /**
186
+ * The weights to load from what a family offers: each wanted weight as the
187
+ * nearest one offered, without repeats. Unknown availability keeps the
188
+ * wanted weights as they are.
189
+ */
190
+ export declare function offeredWeights(wanted: readonly number[], offered: readonly number[] | undefined): number[];
191
+ /**
192
+ * Google's CSS2 URL for some faces of one family: `ital,wght` tuples, sorted
193
+ * as the API requires. One weight asks for a static instance, which for a
194
+ * variable family is a file holding that weight alone — Inter's Latin 400 is
195
+ * 24 KB that way and 48 KB as the variable file.
196
+ */
197
+ export declare function googleFontSheetUrl(family: string, faces: ReadonlyArray<{
198
+ weight: number;
199
+ style: 'normal' | 'italic';
200
+ }>): string | undefined;
201
+ /**
202
+ * The files the first screen paints with, to preload: the body text's face
203
+ * and the headline's, Latin and upright, from whatever faces the page
204
+ * declares. A file covering both is preloaded once.
205
+ */
206
+ export declare function siteFontPreloads(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, faces: readonly SiteFontFace[], baseTypography?: Record<string, unknown>): string[];
@@ -14,6 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { metricFallbackFamily, stackFamilies } from "./font-stack.js";
18
+ export { metricFallbackFamily, themeWebFontFamilies, withMetricFallbacks } from "./font-stack.js";
17
19
  /**
18
20
  * A theme's Google fonts, served from the site's own origin (AGL-3485).
19
21
  *
@@ -45,8 +47,11 @@
45
47
  /**
46
48
  * A font file's path on {@link GOOGLE_FONT_FILE_ORIGIN} below `/s/`, as the
47
49
  * route accepts it: `{family}/v{n}/{file}.woff2`. Anything else is refused,
48
- * so the route can never be used to fetch an arbitrary URL.
49
- */ const FONT_FILE_PATH = /^[a-z0-9]+\/v\d{1,4}\/[A-Za-z0-9_-]{1,200}\.woff2$/;
50
+ * so the route can never be used to fetch an arbitrary URL. A file name runs
51
+ * to 400 characters: a variable family with many axes (Roboto Flex) is named
52
+ * by an id of about 250, which a 200 cap refused, dropping every face of the
53
+ * family (AGL-3656).
54
+ */ const FONT_FILE_PATH = /^[a-z0-9]+\/v\d{1,4}\/[A-Za-z0-9_-]{1,400}\.woff2$/;
50
55
  /** Whether a path is one {@link SELF_HOSTED_FONT_ROUTE} will serve. */ export function isSelfHostedFontPath(path) {
51
56
  return FONT_FILE_PATH.test(path);
52
57
  }
@@ -165,5 +170,347 @@ const RANGE = /unicode-range:\s*([U+0-9A-Fa-f?,\s-]{1,4000});/;
165
170
  ...new Set(paths)
166
171
  ];
167
172
  }
173
+ /** A Google face as a {@link SiteFontFace} on the site's own route. */ export function siteFontFaceFromGoogle(face) {
174
+ return _extends({
175
+ family: face.family,
176
+ style: face.style,
177
+ weight: face.weight,
178
+ url: selfHostedFontUrl(face.path)
179
+ }, face.subset ? {
180
+ subset: face.subset
181
+ } : {}, face.unicodeRange ? {
182
+ unicodeRange: face.unicodeRange
183
+ } : {});
184
+ }
185
+ /**
186
+ * The faces with every set of rules that share one file folded into one rule
187
+ * over the weights it covers.
188
+ *
189
+ * Google answers a request for several weights of a variable family with the
190
+ * SAME file once per weight, each rule naming one weight. A browser treats
191
+ * those as separate faces: it matches 500 to nothing between the 400 and 600
192
+ * rules a theme listed and renders it as 400, though the file holds every
193
+ * weight in between. One rule declaring the range (`400 700`) is what the
194
+ * file is.
195
+ */ export function mergeSharedFiles(faces) {
196
+ const groups = new Map();
197
+ for (const face of faces){
198
+ var _face_unicodeRange;
199
+ const key = [
200
+ face.family.toLowerCase(),
201
+ face.style,
202
+ face.url,
203
+ (_face_unicodeRange = face.unicodeRange) != null ? _face_unicodeRange : ''
204
+ ].join('|');
205
+ const group = groups.get(key);
206
+ if (group) group.push(face);
207
+ else groups.set(key, [
208
+ face
209
+ ]);
210
+ }
211
+ return [
212
+ ...groups.values()
213
+ ].map((group)=>{
214
+ if (group.length === 1) return group[0];
215
+ const bounds = group.flatMap((face)=>face.weight.split(' ').map(Number));
216
+ const low = Math.min(...bounds);
217
+ const high = Math.max(...bounds);
218
+ return _extends({}, group[0], {
219
+ weight: low === high ? String(low) : `${low} ${high}`
220
+ });
221
+ });
222
+ }
223
+ /** The rules for a page's faces, every one `font-display: swap`. */ export function siteFontFaceCss(faces) {
224
+ return faces.filter((face)=>isSiteFontUrl(face.url)).map((face)=>[
225
+ '@font-face{',
226
+ `font-family:${cssFamily(face.family)};`,
227
+ `font-style:${face.style};`,
228
+ `font-weight:${/^\d{1,4}( \d{1,4})?$/.test(face.weight) ? face.weight : '400'};`,
229
+ 'font-display:swap;',
230
+ `src:url(${face.url}) format('woff2');`,
231
+ face.unicodeRange && /^[U+0-9A-Fa-f?,\s-]{1,4000}$/.test(face.unicodeRange) ? `unicode-range:${face.unicodeRange};` : '',
232
+ '}'
233
+ ].join('')).join('');
234
+ }
235
+ /**
236
+ * Whether a URL is one a rule may name: site-relative, on the font route or
237
+ * the media route, with nothing that could close the `url()` it is written
238
+ * into. A face whose URL fails is dropped rather than escaped.
239
+ */ export function isSiteFontUrl(url) {
240
+ return /^\/api\/(fonts|media\/cdn)\/[A-Za-z0-9_\-./:%]{1,600}(\?v=[A-Za-z0-9_.-]{1,128})?$/.test(url);
241
+ }
242
+ /**
243
+ * The local faces fallbacks are drawn from, by category. Their metrics are
244
+ * read from the faces themselves with the fonts plugin's reader and its
245
+ * frequency-weighted average width (Arial 2048/1854/-434/67/901), so the ratio
246
+ * to a web font measured the same way is what a line of text changes by.
247
+ * Arial and Times New Roman ship with Windows and macOS; where neither is
248
+ * installed the `local()` source fails and the stack moves on unadjusted.
249
+ */ const LOCAL_FALLBACKS = {
250
+ 'sans-serif': {
251
+ local: [
252
+ 'Arial',
253
+ 'ArialMT'
254
+ ],
255
+ metrics: {
256
+ unitsPerEm: 2048,
257
+ ascent: 1854,
258
+ descent: -434,
259
+ lineGap: 67,
260
+ xWidthAvg: 901
261
+ }
262
+ },
263
+ serif: {
264
+ local: [
265
+ 'Times New Roman',
266
+ 'TimesNewRomanPSMT'
267
+ ],
268
+ metrics: {
269
+ unitsPerEm: 2048,
270
+ ascent: 1825,
271
+ descent: -443,
272
+ lineGap: 87,
273
+ xWidthAvg: 817
274
+ }
275
+ },
276
+ monospace: {
277
+ local: [
278
+ 'Courier New',
279
+ 'CourierNewPSMT'
280
+ ],
281
+ metrics: {
282
+ unitsPerEm: 2048,
283
+ ascent: 1705,
284
+ descent: -615,
285
+ lineGap: 0,
286
+ xWidthAvg: 1229
287
+ }
288
+ }
289
+ };
290
+ function localFallbackFor(category) {
291
+ if (category === 'serif') return LOCAL_FALLBACKS.serif;
292
+ if (category === 'monospace') return LOCAL_FALLBACKS.monospace;
293
+ return LOCAL_FALLBACKS['sans-serif'];
294
+ }
295
+ function percent(value) {
296
+ return `${(Math.round(value * 10000) / 100).toString()}%`;
297
+ }
298
+ /** Whether metrics are numbers a rule can be written from. */ function usableMetrics(metrics) {
299
+ return !!metrics && [
300
+ metrics.unitsPerEm,
301
+ metrics.ascent,
302
+ metrics.descent,
303
+ metrics.lineGap,
304
+ metrics.xWidthAvg
305
+ ].every((value)=>typeof value === 'number' && Number.isFinite(value)) && metrics.unitsPerEm > 0 && metrics.xWidthAvg > 0 && metrics.ascent > 0;
306
+ }
307
+ /**
308
+ * The `@font-face` rule for a family's metric-matched fallback: the local face
309
+ * of its category, scaled so its average character is as wide as the web
310
+ * font's (`size-adjust`) and given the web font's ascent, descent and line gap
311
+ * at that scale. Text set in it occupies the box the web font will, so the
312
+ * swap moves nothing — the CLS a `font-display: swap` page otherwise pays.
313
+ *
314
+ * The overrides are divided by the size adjustment because the browser
315
+ * applies them to the ADJUSTED size: an ascent of 0.97em on a face scaled to
316
+ * 1.07 has to be written as 0.97/1.07 to land at 0.97em.
317
+ *
318
+ * Empty for metrics that are missing or not numbers.
319
+ */ export function metricFallbackFontFaceCss(family, metrics, category) {
320
+ if (!usableMetrics(metrics)) return '';
321
+ const local = localFallbackFor(category);
322
+ const sizeAdjust = metrics.xWidthAvg / metrics.unitsPerEm / (local.metrics.xWidthAvg / local.metrics.unitsPerEm);
323
+ if (!Number.isFinite(sizeAdjust) || sizeAdjust <= 0) return '';
324
+ const over = (value)=>percent(Math.abs(value) / metrics.unitsPerEm / sizeAdjust);
325
+ return [
326
+ '@font-face{',
327
+ `font-family:${cssFamily(metricFallbackFamily(family))};`,
328
+ `src:${local.local.map((name)=>`local(${cssFamily(name)})`).join(',')};`,
329
+ `size-adjust:${percent(sizeAdjust)};`,
330
+ `ascent-override:${over(metrics.ascent)};`,
331
+ `descent-override:${over(metrics.descent)};`,
332
+ `line-gap-override:${over(metrics.lineGap)};`,
333
+ '}'
334
+ ].join('');
335
+ }
336
+ const NAMED_WEIGHTS = {
337
+ normal: 400,
338
+ bold: 700,
339
+ lighter: 300,
340
+ bolder: 700
341
+ };
342
+ function numericWeight(value) {
343
+ if (typeof value === 'number' && Number.isFinite(value)) return Math.round(value);
344
+ if (typeof value !== 'string') return undefined;
345
+ if (/^\d{3,4}$/.test(value.trim())) return Number(value.trim());
346
+ return NAMED_WEIGHTS[value.trim().toLowerCase()];
347
+ }
348
+ /** MUI's four named weights, before a theme adds its own. */ const MUI_WEIGHT_TOKENS = {
349
+ fontWeightLight: 300,
350
+ fontWeightRegular: 400,
351
+ fontWeightMedium: 500,
352
+ fontWeightBold: 700
353
+ };
354
+ /**
355
+ * The faces each of a theme's web font families is drawn with (AGL-3656).
356
+ *
357
+ * A theme lists weights for its font (`fonts[].weights`), but the page draws
358
+ * with whatever its text styles say: the platform's ramp sets `h1` in 900 and
359
+ * `h2` in 800, so a theme that loaded Inter at 400, 500 and 700 drew its
360
+ * headlines in a synthesized or substituted weight. The weights here are the
361
+ * union of the listed ones, every text style's weight whose family resolves to
362
+ * this one, and the theme's named weights (`fontWeightLight` … `Black`), which
363
+ * the weight picker offers content.
364
+ *
365
+ * `baseTypography` is the typography the site's theme is layered onto
366
+ * (`siteBaseOptions(...).typography`): a text style the theme leaves alone
367
+ * keeps the base's weight and family.
368
+ *
369
+ * One italic is loaded per family, at the body text's weight, so emphasis in
370
+ * running text is a real italic rather than a slanted upright; a theme can
371
+ * list more under `italics`.
372
+ */ export function themeFontNeeds(theme, baseTypography, extraFonts = []) {
373
+ var _ref, _ref1;
374
+ var _theme_typography, _theme_typography1;
375
+ const fonts = [
376
+ ...(_ref = theme == null ? void 0 : theme.fonts) != null ? _ref : [],
377
+ ...extraFonts
378
+ ].filter((font)=>{
379
+ var _font_source;
380
+ return font.family && ((_font_source = font.source) != null ? _font_source : 'google') !== 'system';
381
+ });
382
+ if (!fonts.length) return [];
383
+ const base = baseTypography != null ? baseTypography : {};
384
+ const variants = (_ref1 = theme == null ? void 0 : (_theme_typography = theme.typography) == null ? void 0 : _theme_typography.variants) != null ? _ref1 : {};
385
+ const baseStack = typeof (theme == null ? void 0 : (_theme_typography1 = theme.typography) == null ? void 0 : _theme_typography1.fontFamily) === 'string' && theme.typography.fontFamily || (typeof base['fontFamily'] === 'string' ? base['fontFamily'] : '');
386
+ const needs = new Map();
387
+ for (const font of fonts){
388
+ var _needs_get, _font_weights, _font_italics;
389
+ const key = font.family.trim().toLowerCase();
390
+ const need = (_needs_get = needs.get(key)) != null ? _needs_get : {
391
+ family: font.family.trim(),
392
+ weights: new Set(),
393
+ text: new Set(),
394
+ italics: new Set()
395
+ };
396
+ for (const weight of (_font_weights = font.weights) != null ? _font_weights : [])if (numericWeight(weight)) need.weights.add(numericWeight(weight));
397
+ for (const weight of (_font_italics = font.italics) != null ? _font_italics : [])if (numericWeight(weight)) need.italics.add(numericWeight(weight));
398
+ needs.set(key, need);
399
+ }
400
+ const needFor = (stack)=>{
401
+ const first = typeof stack === 'string' ? stackFamilies(stack)[0] : undefined;
402
+ return first ? needs.get(first.toLowerCase()) : undefined;
403
+ };
404
+ const variantKeys = new Set([
405
+ ...Object.keys(variants),
406
+ ...Object.keys(base).filter((key)=>{
407
+ const value = base[key];
408
+ return !!value && typeof value === 'object' && !Array.isArray(value);
409
+ })
410
+ ]);
411
+ for (const key of variantKeys){
412
+ var _variants_key, _base_key, _ref2, _own_fontFamily, _numericWeight, _own_fontWeight;
413
+ const own = (_variants_key = variants[key]) != null ? _variants_key : {};
414
+ const inherited = (_base_key = base[key]) != null ? _base_key : {};
415
+ const need = needFor((_ref2 = (_own_fontFamily = own['fontFamily']) != null ? _own_fontFamily : inherited['fontFamily']) != null ? _ref2 : baseStack);
416
+ if (!need) continue;
417
+ const weight = (_numericWeight = numericWeight((_own_fontWeight = own['fontWeight']) != null ? _own_fontWeight : inherited['fontWeight'])) != null ? _numericWeight : 400;
418
+ need.weights.add(weight);
419
+ need.text.add(weight);
420
+ }
421
+ const bodyNeed = needFor(baseStack);
422
+ if (bodyNeed) {
423
+ var _variants_body1, _numericWeight1, _body_fontWeight;
424
+ var _base_body1;
425
+ const tokens = _extends({}, MUI_WEIGHT_TOKENS);
426
+ for (const [key, value] of Object.entries(base))if (/^fontWeight[A-Z]/.test(key)) tokens[key] = value;
427
+ for (const [key, value] of Object.entries(tokens)){
428
+ const weight = numericWeight(value);
429
+ if (!weight) continue;
430
+ bodyNeed.weights.add(weight);
431
+ if (key !== 'fontWeightLight' && MUI_WEIGHT_TOKENS[key]) bodyNeed.text.add(weight);
432
+ }
433
+ const body = (_variants_body1 = variants['body1']) != null ? _variants_body1 : {};
434
+ const bodyWeight = (_numericWeight1 = numericWeight((_body_fontWeight = body['fontWeight']) != null ? _body_fontWeight : (_base_body1 = base['body1']) == null ? void 0 : _base_body1['fontWeight'])) != null ? _numericWeight1 : 400;
435
+ bodyNeed.italics.add(bodyWeight);
436
+ }
437
+ return [
438
+ ...needs.values()
439
+ ].map((need)=>({
440
+ family: need.family,
441
+ weights: [
442
+ ...need.weights
443
+ ].sort((a, b)=>a - b),
444
+ textWeights: [
445
+ ...need.text
446
+ ].sort((a, b)=>a - b),
447
+ italics: [
448
+ ...need.italics
449
+ ].sort((a, b)=>a - b)
450
+ }));
451
+ }
452
+ /**
453
+ * The weights to load from what a family offers: each wanted weight as the
454
+ * nearest one offered, without repeats. Unknown availability keeps the
455
+ * wanted weights as they are.
456
+ */ export function offeredWeights(wanted, offered) {
457
+ if (!(offered == null ? void 0 : offered.length)) return [
458
+ ...new Set(wanted)
459
+ ].sort((a, b)=>a - b);
460
+ const picked = wanted.map((weight)=>offered.reduce((best, candidate)=>Math.abs(candidate - weight) < Math.abs(best - weight) ? candidate : best));
461
+ return [
462
+ ...new Set(picked)
463
+ ].sort((a, b)=>a - b);
464
+ }
465
+ /**
466
+ * Google's CSS2 URL for some faces of one family: `ital,wght` tuples, sorted
467
+ * as the API requires. One weight asks for a static instance, which for a
468
+ * variable family is a file holding that weight alone — Inter's Latin 400 is
469
+ * 24 KB that way and 48 KB as the variable file.
470
+ */ export function googleFontSheetUrl(family, faces) {
471
+ const name = family.trim();
472
+ if (!name || !/^[A-Za-z0-9 ]{1,80}$/.test(name) || !faces.length) return undefined;
473
+ const tuples = [
474
+ ...new Set(faces.map((face)=>`${face.style === 'italic' ? 1 : 0},${face.weight}`))
475
+ ].sort((a, b)=>{
476
+ const [ai, aw] = a.split(',').map(Number);
477
+ const [bi, bw] = b.split(',').map(Number);
478
+ return ai - bi || aw - bw;
479
+ });
480
+ const axis = tuples.some((tuple)=>tuple.startsWith('1,')) ? `ital,wght@${tuples.join(';')}` : `wght@${tuples.map((tuple)=>tuple.slice(2)).join(';')}`;
481
+ return `https://fonts.googleapis.com/css2?family=${name.replace(/ /g, '+')}:${axis}&display=swap`;
482
+ }
483
+ /**
484
+ * The files the first screen paints with, to preload: the body text's face
485
+ * and the headline's, Latin and upright, from whatever faces the page
486
+ * declares. A file covering both is preloaded once.
487
+ */ export function siteFontPreloads(theme, faces, baseTypography) {
488
+ var _base_h1, _base_body1, _ref, _firstFamily, _numericWeight, _ref1, _ref2, _firstFamily1, _numericWeight1, _ref3;
489
+ var _theme_fonts_find, _theme_fonts, _variants_body1, _variants_h1, _variants_h11;
490
+ const typography = theme == null ? void 0 : theme.typography;
491
+ const variants = typography == null ? void 0 : typography.variants;
492
+ const base = baseTypography != null ? baseTypography : {};
493
+ const baseH1 = (_base_h1 = base['h1']) != null ? _base_h1 : {};
494
+ const baseBody = (_base_body1 = base['body1']) != null ? _base_body1 : {};
495
+ const fallback = theme == null ? void 0 : (_theme_fonts = theme.fonts) == null ? void 0 : (_theme_fonts_find = _theme_fonts.find((font)=>{
496
+ var _font_source;
497
+ return ((_font_source = font.source) != null ? _font_source : 'google') !== 'system';
498
+ })) == null ? void 0 : _theme_fonts_find.family;
499
+ const bodyFamily = (_ref = (_firstFamily = firstFamily(typography == null ? void 0 : typography.fontFamily)) != null ? _firstFamily : firstFamily(base['fontFamily'])) != null ? _ref : fallback;
500
+ const pick = (family, weight)=>{
501
+ if (!family) return undefined;
502
+ const wanted = family.toLowerCase();
503
+ const candidates = faces.filter((face)=>face.family.toLowerCase() === wanted && face.style === 'normal' && (face.subset === undefined || face.subset === 'latin'));
504
+ return candidates.sort((a, b)=>weightDistance(a, weight) - weightDistance(b, weight))[0];
505
+ };
506
+ const body = pick(bodyFamily, (_numericWeight = numericWeight((_ref1 = variants == null ? void 0 : (_variants_body1 = variants.body1) == null ? void 0 : _variants_body1.fontWeight) != null ? _ref1 : baseBody['fontWeight'])) != null ? _numericWeight : 400);
507
+ const heading = pick((_ref2 = (_firstFamily1 = firstFamily(variants == null ? void 0 : (_variants_h1 = variants.h1) == null ? void 0 : _variants_h1.fontFamily)) != null ? _firstFamily1 : firstFamily(baseH1['fontFamily'])) != null ? _ref2 : bodyFamily, (_numericWeight1 = numericWeight((_ref3 = variants == null ? void 0 : (_variants_h11 = variants.h1) == null ? void 0 : _variants_h11.fontWeight) != null ? _ref3 : baseH1['fontWeight'])) != null ? _numericWeight1 : 700);
508
+ return [
509
+ ...new Set([
510
+ body,
511
+ heading
512
+ ].filter((face)=>!!face).map((face)=>face.url))
513
+ ];
514
+ }
168
515
 
169
516
  //# sourceMappingURL=self-hosted-fonts.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/theme/src/lib/util/self-hosted-fonts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostTheme } from '@aglyn/shared-data-types'\n\n/**\n * A theme's Google fonts, served from the site's own origin (AGL-3485).\n *\n * A published page loaded its theme font as a `<link rel=\"stylesheet\">` to\n * `fonts.googleapis.com`: a render-blocking request to a third origin, then a\n * second connection to `fonts.gstatic.com` for the files it names — about\n * 800 ms before first paint on the page Lighthouse measured. Nothing about a\n * theme font needs either round trip at view time. The stylesheet is a short\n * list of `@font-face` rules that changes only when the theme does, and the\n * files are versioned in their own paths (`/s/inter/v18/…woff2`), so neither\n * changes under a URL.\n *\n * So the server fetches the stylesheet once, keeps the rules, and inlines them\n * in the page with each `src` pointing at {@link SELF_HOSTED_FONT_ROUTE} on the\n * site's own origin, which serves the file with a year-long `immutable`\n * policy. The one or two faces the first screen paints with are preloaded.\n * Nothing on the page asks Google for anything, which also means a visitor's\n * address is no longer handed to Google by every page view.\n *\n * This module is the pure half: reading Google's stylesheet, writing the\n * rules back, and choosing the faces to preload. The fetching and the file\n * route live with the tenant (`libs/tenant/runtime/.../self-hosted-fonts.ts`).\n *\n * The rules are REBUILT from validated fields rather than echoed, so the\n * inlined CSS can only ever contain a family name the theme chose, a style, a\n * weight, a unicode range and a path on our own route.\n */\n\n/** Where a site serves its theme's font files from. */\nexport const SELF_HOSTED_FONT_ROUTE = '/api/fonts'\n\n/** The origin Google serves font files from. */\nexport const GOOGLE_FONT_FILE_ORIGIN = 'https://fonts.gstatic.com'\n\n/**\n * A font file's path on {@link GOOGLE_FONT_FILE_ORIGIN} below `/s/`, as the\n * route accepts it: `{family}/v{n}/{file}.woff2`. Anything else is refused,\n * so the route can never be used to fetch an arbitrary URL.\n */\nconst FONT_FILE_PATH = /^[a-z0-9]+\\/v\\d{1,4}\\/[A-Za-z0-9_-]{1,200}\\.woff2$/\n\n/** One `@font-face` rule from Google's stylesheet. */\nexport interface GoogleFontFace {\n family: string\n style: 'normal' | 'italic'\n /** A weight (`400`) or, for a variable font, a range (`100 900`). */\n weight: string\n /** Google's subset comment above the rule (`latin`, `latin-ext`, …). */\n subset?: string\n /** The file's path below `/s/` on Google's origin. */\n path: string\n unicodeRange?: string\n}\n\n/** Whether a path is one {@link SELF_HOSTED_FONT_ROUTE} will serve. */\nexport function isSelfHostedFontPath(path: string): boolean {\n return FONT_FILE_PATH.test(path)\n}\n\n/** The site-relative URL a face's file is served from. */\nexport function selfHostedFontUrl(path: string): string {\n return `${SELF_HOSTED_FONT_ROUTE}/${path}`\n}\n\n/** Google's own URL for a self-hosted path. */\nexport function googleFontFileUrl(path: string): string {\n return `${GOOGLE_FONT_FILE_ORIGIN}/s/${path}`\n}\n\nconst FACE_BLOCK = /(?:\\/\\*\\s*([a-z0-9-]+)\\s*\\*\\/\\s*)?@font-face\\s*\\{([^}]*)\\}/g\nconst FAMILY = /font-family:\\s*'([^']{1,80})'/\nconst STYLE = /font-style:\\s*(normal|italic)/\nconst WEIGHT = /font-weight:\\s*(\\d{3}(?:\\s+\\d{3})?)/\nconst SRC = /src:\\s*url\\((https:\\/\\/fonts\\.gstatic\\.com\\/s\\/[^)\\s]+)\\)\\s*format\\('woff2'\\)/\nconst RANGE = /unicode-range:\\s*([U+0-9A-Fa-f?,\\s-]{1,4000});/\n\n/**\n * Reads the `@font-face` rules out of a Google Fonts CSS2 stylesheet fetched\n * with a browser that accepts WOFF2. A rule that is not exactly the shape\n * Google writes — a missing field, a file off Google's origin, a path the\n * route would refuse — is dropped, never repaired.\n */\nexport function parseGoogleFontFaces(css: string): GoogleFontFace[] {\n const faces: GoogleFontFace[] = []\n for (const match of css.matchAll(FACE_BLOCK)) {\n const [, subset, body] = match\n const family = FAMILY.exec(body)?.[1]\n const style = STYLE.exec(body)?.[1] as GoogleFontFace['style'] | undefined\n const weight = WEIGHT.exec(body)?.[1]\n const url = SRC.exec(body)?.[1]\n if (!family || !style || !weight || !url) continue\n const path = url.slice(`${GOOGLE_FONT_FILE_ORIGIN}/s/`.length)\n if (!isSelfHostedFontPath(path)) continue\n const unicodeRange = RANGE.exec(body)?.[1]?.trim()\n faces.push({\n family,\n style,\n weight: weight.replace(/\\s+/g, ' '),\n ...(subset ? { subset } : {}),\n path,\n ...(unicodeRange ? { unicodeRange } : {}),\n })\n }\n return faces\n}\n\n/** A family name as it may appear quoted in CSS. */\nfunction cssFamily(family: string): string {\n return `'${family.replace(/['\\\\\\n\\r]/g, '')}'`\n}\n\n/**\n * The faces as `@font-face` rules on the site's own route, every one\n * `font-display: swap` so text paints in the fallback while a file loads.\n *\n * Every subset Google declared is kept, each with its `unicode-range`: a\n * browser downloads a subset only when the page uses a character in it, so\n * the Cyrillic file costs an English page nothing, and a Polish or Greek page\n * keeps its typeface. Only the Latin faces are preloaded.\n */\nexport function selfHostedFontFaceCss(faces: readonly GoogleFontFace[]): string {\n return faces\n .map((face) =>\n [\n '@font-face{',\n `font-family:${cssFamily(face.family)};`,\n `font-style:${face.style};`,\n `font-weight:${face.weight};`,\n 'font-display:swap;',\n `src:url(${selfHostedFontUrl(face.path)}) format('woff2');`,\n face.unicodeRange ? `unicode-range:${face.unicodeRange};` : '',\n '}',\n ].join(''),\n )\n .join('')\n}\n\n/** The first family named in a CSS `font-family` stack. */\nfunction firstFamily(stack: unknown): string | undefined {\n if (typeof stack !== 'string') return undefined\n const first = stack.split(',')[0]?.trim().replace(/^['\"]|['\"]$/g, '')\n return first || undefined\n}\n\n/** How far a face is from a wanted weight; 0 when its range covers it. */\nfunction weightDistance(face: GoogleFontFace, wanted: number): number {\n const [low, high = low] = face.weight.split(' ').map(Number)\n if (wanted >= low && wanted <= high) return 0\n return Math.min(Math.abs(wanted - low), Math.abs(wanted - high))\n}\n\n/** The Latin, upright face of `family` closest to `weight`. */\nfunction latinFace(\n faces: readonly GoogleFontFace[],\n family: string | undefined,\n weight: number,\n): GoogleFontFace | undefined {\n if (!family) return undefined\n const wanted = family.toLowerCase()\n const candidates = faces.filter(\n (face) =>\n face.family.toLowerCase() === wanted &&\n face.style === 'normal' &&\n (face.subset === undefined || face.subset === 'latin'),\n )\n return candidates.sort(\n (a, b) => weightDistance(a, weight) - weightDistance(b, weight),\n )[0]\n}\n\n/**\n * The paths of the one or two faces the first screen paints with: the body\n * text's face, and the headline's when it is a different file. Those are the\n * fonts the largest text on the page — usually the LCP element — waits for,\n * so they are worth starting with the document; every other face is fetched\n * when the page first uses it.\n *\n * The body face is the theme's `fontFamily` (else the first Google font it\n * loads) at `body1`'s weight, 400 by default; the headline face is `h1`'s\n * family and weight, 700 by default. A variable font serves every weight\n * from one file, which is then preloaded once.\n */\nexport function selfHostedFontPreloads(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n faces: readonly GoogleFontFace[],\n): string[] {\n const typography = theme?.typography\n const variants = typography?.variants\n const fallback = theme?.fonts?.find(\n (font) => (font.source ?? 'google') === 'google',\n )?.family\n const bodyFamily = firstFamily(typography?.fontFamily) ?? fallback\n const body = latinFace(faces, bodyFamily, variants?.body1?.fontWeight ?? 400)\n const heading = latinFace(\n faces,\n firstFamily(variants?.h1?.fontFamily) ?? bodyFamily,\n variants?.h1?.fontWeight ?? 700,\n )\n const paths = [body, heading]\n .filter((face): face is GoogleFontFace => Boolean(face))\n .map((face) => selfHostedFontUrl(face.path))\n return [...new Set(paths)]\n}\n"],"names":["SELF_HOSTED_FONT_ROUTE","GOOGLE_FONT_FILE_ORIGIN","FONT_FILE_PATH","isSelfHostedFontPath","path","test","selfHostedFontUrl","googleFontFileUrl","FACE_BLOCK","FAMILY","STYLE","WEIGHT","SRC","RANGE","parseGoogleFontFaces","css","faces","match","matchAll","subset","body","family","exec","style","weight","url","slice","length","unicodeRange","trim","push","replace","cssFamily","selfHostedFontFaceCss","map","face","join","firstFamily","stack","undefined","first","split","weightDistance","wanted","low","high","Number","Math","min","abs","latinFace","toLowerCase","candidates","filter","sort","a","b","selfHostedFontPreloads","theme","variants","typography","fallback","fonts","find","font","source","bodyFamily","fontFamily","body1","fontWeight","heading","h1","paths","Boolean","Set"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/theme/src/lib/util/self-hosted-fonts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n HostTheme,\n HostThemeFont,\n HostThemeFontCategory,\n HostThemeFontMetrics,\n} from '@aglyn/shared-data-types'\nimport { metricFallbackFamily, stackFamilies } from './font-stack'\n\nexport { metricFallbackFamily, themeWebFontFamilies, withMetricFallbacks } from './font-stack'\n\n/**\n * A theme's Google fonts, served from the site's own origin (AGL-3485).\n *\n * A published page loaded its theme font as a `<link rel=\"stylesheet\">` to\n * `fonts.googleapis.com`: a render-blocking request to a third origin, then a\n * second connection to `fonts.gstatic.com` for the files it names — about\n * 800 ms before first paint on the page Lighthouse measured. Nothing about a\n * theme font needs either round trip at view time. The stylesheet is a short\n * list of `@font-face` rules that changes only when the theme does, and the\n * files are versioned in their own paths (`/s/inter/v18/…woff2`), so neither\n * changes under a URL.\n *\n * So the server fetches the stylesheet once, keeps the rules, and inlines them\n * in the page with each `src` pointing at {@link SELF_HOSTED_FONT_ROUTE} on the\n * site's own origin, which serves the file with a year-long `immutable`\n * policy. The one or two faces the first screen paints with are preloaded.\n * Nothing on the page asks Google for anything, which also means a visitor's\n * address is no longer handed to Google by every page view.\n *\n * This module is the pure half: reading Google's stylesheet, writing the\n * rules back, and choosing the faces to preload. The fetching and the file\n * route live with the tenant (`libs/tenant/runtime/.../self-hosted-fonts.ts`).\n *\n * The rules are REBUILT from validated fields rather than echoed, so the\n * inlined CSS can only ever contain a family name the theme chose, a style, a\n * weight, a unicode range and a path on our own route.\n */\n\n/** Where a site serves its theme's font files from. */\nexport const SELF_HOSTED_FONT_ROUTE = '/api/fonts'\n\n/** The origin Google serves font files from. */\nexport const GOOGLE_FONT_FILE_ORIGIN = 'https://fonts.gstatic.com'\n\n/**\n * A font file's path on {@link GOOGLE_FONT_FILE_ORIGIN} below `/s/`, as the\n * route accepts it: `{family}/v{n}/{file}.woff2`. Anything else is refused,\n * so the route can never be used to fetch an arbitrary URL. A file name runs\n * to 400 characters: a variable family with many axes (Roboto Flex) is named\n * by an id of about 250, which a 200 cap refused, dropping every face of the\n * family (AGL-3656).\n */\nconst FONT_FILE_PATH = /^[a-z0-9]+\\/v\\d{1,4}\\/[A-Za-z0-9_-]{1,400}\\.woff2$/\n\n/** One `@font-face` rule from Google's stylesheet. */\nexport interface GoogleFontFace {\n family: string\n style: 'normal' | 'italic'\n /** A weight (`400`) or, for a variable font, a range (`100 900`). */\n weight: string\n /** Google's subset comment above the rule (`latin`, `latin-ext`, …). */\n subset?: string\n /** The file's path below `/s/` on Google's origin. */\n path: string\n unicodeRange?: string\n}\n\n/** Whether a path is one {@link SELF_HOSTED_FONT_ROUTE} will serve. */\nexport function isSelfHostedFontPath(path: string): boolean {\n return FONT_FILE_PATH.test(path)\n}\n\n/** The site-relative URL a face's file is served from. */\nexport function selfHostedFontUrl(path: string): string {\n return `${SELF_HOSTED_FONT_ROUTE}/${path}`\n}\n\n/** Google's own URL for a self-hosted path. */\nexport function googleFontFileUrl(path: string): string {\n return `${GOOGLE_FONT_FILE_ORIGIN}/s/${path}`\n}\n\nconst FACE_BLOCK = /(?:\\/\\*\\s*([a-z0-9-]+)\\s*\\*\\/\\s*)?@font-face\\s*\\{([^}]*)\\}/g\nconst FAMILY = /font-family:\\s*'([^']{1,80})'/\nconst STYLE = /font-style:\\s*(normal|italic)/\nconst WEIGHT = /font-weight:\\s*(\\d{3}(?:\\s+\\d{3})?)/\nconst SRC = /src:\\s*url\\((https:\\/\\/fonts\\.gstatic\\.com\\/s\\/[^)\\s]+)\\)\\s*format\\('woff2'\\)/\nconst RANGE = /unicode-range:\\s*([U+0-9A-Fa-f?,\\s-]{1,4000});/\n\n/**\n * Reads the `@font-face` rules out of a Google Fonts CSS2 stylesheet fetched\n * with a browser that accepts WOFF2. A rule that is not exactly the shape\n * Google writes — a missing field, a file off Google's origin, a path the\n * route would refuse — is dropped, never repaired.\n */\nexport function parseGoogleFontFaces(css: string): GoogleFontFace[] {\n const faces: GoogleFontFace[] = []\n for (const match of css.matchAll(FACE_BLOCK)) {\n const [, subset, body] = match\n const family = FAMILY.exec(body)?.[1]\n const style = STYLE.exec(body)?.[1] as GoogleFontFace['style'] | undefined\n const weight = WEIGHT.exec(body)?.[1]\n const url = SRC.exec(body)?.[1]\n if (!family || !style || !weight || !url) continue\n const path = url.slice(`${GOOGLE_FONT_FILE_ORIGIN}/s/`.length)\n if (!isSelfHostedFontPath(path)) continue\n const unicodeRange = RANGE.exec(body)?.[1]?.trim()\n faces.push({\n family,\n style,\n weight: weight.replace(/\\s+/g, ' '),\n ...(subset ? { subset } : {}),\n path,\n ...(unicodeRange ? { unicodeRange } : {}),\n })\n }\n return faces\n}\n\n/** A family name as it may appear quoted in CSS. */\nfunction cssFamily(family: string): string {\n return `'${family.replace(/['\\\\\\n\\r]/g, '')}'`\n}\n\n/**\n * The faces as `@font-face` rules on the site's own route, every one\n * `font-display: swap` so text paints in the fallback while a file loads.\n *\n * Every subset Google declared is kept, each with its `unicode-range`: a\n * browser downloads a subset only when the page uses a character in it, so\n * the Cyrillic file costs an English page nothing, and a Polish or Greek page\n * keeps its typeface. Only the Latin faces are preloaded.\n */\nexport function selfHostedFontFaceCss(faces: readonly GoogleFontFace[]): string {\n return faces\n .map((face) =>\n [\n '@font-face{',\n `font-family:${cssFamily(face.family)};`,\n `font-style:${face.style};`,\n `font-weight:${face.weight};`,\n 'font-display:swap;',\n `src:url(${selfHostedFontUrl(face.path)}) format('woff2');`,\n face.unicodeRange ? `unicode-range:${face.unicodeRange};` : '',\n '}',\n ].join(''),\n )\n .join('')\n}\n\n/** The first family named in a CSS `font-family` stack. */\nfunction firstFamily(stack: unknown): string | undefined {\n if (typeof stack !== 'string') return undefined\n const first = stack.split(',')[0]?.trim().replace(/^['\"]|['\"]$/g, '')\n return first || undefined\n}\n\n/** How far a face is from a wanted weight; 0 when its range covers it. */\nfunction weightDistance(face: { weight: string }, wanted: number): number {\n const [low, high = low] = face.weight.split(' ').map(Number)\n if (wanted >= low && wanted <= high) return 0\n return Math.min(Math.abs(wanted - low), Math.abs(wanted - high))\n}\n\n/** The Latin, upright face of `family` closest to `weight`. */\nfunction latinFace(\n faces: readonly GoogleFontFace[],\n family: string | undefined,\n weight: number,\n): GoogleFontFace | undefined {\n if (!family) return undefined\n const wanted = family.toLowerCase()\n const candidates = faces.filter(\n (face) =>\n face.family.toLowerCase() === wanted &&\n face.style === 'normal' &&\n (face.subset === undefined || face.subset === 'latin'),\n )\n return candidates.sort(\n (a, b) => weightDistance(a, weight) - weightDistance(b, weight),\n )[0]\n}\n\n/**\n * The paths of the one or two faces the first screen paints with: the body\n * text's face, and the headline's when it is a different file. Those are the\n * fonts the largest text on the page — usually the LCP element — waits for,\n * so they are worth starting with the document; every other face is fetched\n * when the page first uses it.\n *\n * The body face is the theme's `fontFamily` (else the first Google font it\n * loads) at `body1`'s weight, 400 by default; the headline face is `h1`'s\n * family and weight, 700 by default. A variable font serves every weight\n * from one file, which is then preloaded once.\n */\nexport function selfHostedFontPreloads(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n faces: readonly GoogleFontFace[],\n): string[] {\n const typography = theme?.typography\n const variants = typography?.variants\n const fallback = theme?.fonts?.find(\n (font) => (font.source ?? 'google') === 'google',\n )?.family\n const bodyFamily = firstFamily(typography?.fontFamily) ?? fallback\n const body = latinFace(faces, bodyFamily, variants?.body1?.fontWeight ?? 400)\n const heading = latinFace(\n faces,\n firstFamily(variants?.h1?.fontFamily) ?? bodyFamily,\n variants?.h1?.fontWeight ?? 700,\n )\n const paths = [body, heading]\n .filter((face): face is GoogleFontFace => Boolean(face))\n .map((face) => selfHostedFontUrl(face.path))\n return [...new Set(paths)]\n}\n\n/* ------------------------------------------------------------------------ *\n * The loader (AGL-3656): faces per weight, metric-matched fallbacks, and the\n * site's own uploaded fonts.\n * ------------------------------------------------------------------------ */\n\n/**\n * One face a page declares, wherever its file lives: a Google face rewritten\n * onto {@link SELF_HOSTED_FONT_ROUTE}, or a site's own upload on its media\n * route. Both are on the site's own origin.\n */\nexport interface SiteFontFace {\n family: string\n style: 'normal' | 'italic'\n /** A weight (`400`) or, for a variable file, a range (`300 900`). */\n weight: string\n /** Site-relative URL of the WOFF2 file. */\n url: string\n /** Google's subset name (`latin`, …); absent for a file with no split. */\n subset?: string\n unicodeRange?: string\n}\n\n/** A Google face as a {@link SiteFontFace} on the site's own route. */\nexport function siteFontFaceFromGoogle(face: GoogleFontFace): SiteFontFace {\n return {\n family: face.family,\n style: face.style,\n weight: face.weight,\n url: selfHostedFontUrl(face.path),\n ...(face.subset ? { subset: face.subset } : {}),\n ...(face.unicodeRange ? { unicodeRange: face.unicodeRange } : {}),\n }\n}\n\n/**\n * The faces with every set of rules that share one file folded into one rule\n * over the weights it covers.\n *\n * Google answers a request for several weights of a variable family with the\n * SAME file once per weight, each rule naming one weight. A browser treats\n * those as separate faces: it matches 500 to nothing between the 400 and 600\n * rules a theme listed and renders it as 400, though the file holds every\n * weight in between. One rule declaring the range (`400 700`) is what the\n * file is.\n */\nexport function mergeSharedFiles<T extends { url: string; weight: string; family: string; style: string; unicodeRange?: string }>(\n faces: readonly T[],\n): T[] {\n const groups = new Map<string, T[]>()\n for (const face of faces) {\n const key = [face.family.toLowerCase(), face.style, face.url, face.unicodeRange ?? ''].join('|')\n const group = groups.get(key)\n if (group) group.push(face)\n else groups.set(key, [face])\n }\n return [...groups.values()].map((group) => {\n if (group.length === 1) return group[0]\n const bounds = group.flatMap((face) => face.weight.split(' ').map(Number))\n const low = Math.min(...bounds)\n const high = Math.max(...bounds)\n return { ...group[0], weight: low === high ? String(low) : `${low} ${high}` }\n })\n}\n\n/** The rules for a page's faces, every one `font-display: swap`. */\nexport function siteFontFaceCss(faces: readonly SiteFontFace[]): string {\n return faces\n .filter((face) => isSiteFontUrl(face.url))\n .map((face) =>\n [\n '@font-face{',\n `font-family:${cssFamily(face.family)};`,\n `font-style:${face.style};`,\n `font-weight:${/^\\d{1,4}( \\d{1,4})?$/.test(face.weight) ? face.weight : '400'};`,\n 'font-display:swap;',\n `src:url(${face.url}) format('woff2');`,\n face.unicodeRange && /^[U+0-9A-Fa-f?,\\s-]{1,4000}$/.test(face.unicodeRange)\n ? `unicode-range:${face.unicodeRange};`\n : '',\n '}',\n ].join(''),\n )\n .join('')\n}\n\n/**\n * Whether a URL is one a rule may name: site-relative, on the font route or\n * the media route, with nothing that could close the `url()` it is written\n * into. A face whose URL fails is dropped rather than escaped.\n */\nexport function isSiteFontUrl(url: string): boolean {\n return /^\\/api\\/(fonts|media\\/cdn)\\/[A-Za-z0-9_\\-./:%]{1,600}(\\?v=[A-Za-z0-9_.-]{1,128})?$/.test(url)\n}\n\n/** A local face a fallback is drawn from, measured the way a theme font is. */\ninterface LocalFallbackFace {\n /** `local()` names, in order. */\n local: string[]\n metrics: HostThemeFontMetrics\n}\n\n/**\n * The local faces fallbacks are drawn from, by category. Their metrics are\n * read from the faces themselves with the fonts plugin's reader and its\n * frequency-weighted average width (Arial 2048/1854/-434/67/901), so the ratio\n * to a web font measured the same way is what a line of text changes by.\n * Arial and Times New Roman ship with Windows and macOS; where neither is\n * installed the `local()` source fails and the stack moves on unadjusted.\n */\nconst LOCAL_FALLBACKS: Record<'sans-serif' | 'serif' | 'monospace', LocalFallbackFace> = {\n 'sans-serif': {\n local: ['Arial', 'ArialMT'],\n metrics: { unitsPerEm: 2048, ascent: 1854, descent: -434, lineGap: 67, xWidthAvg: 901 },\n },\n serif: {\n local: ['Times New Roman', 'TimesNewRomanPSMT'],\n metrics: { unitsPerEm: 2048, ascent: 1825, descent: -443, lineGap: 87, xWidthAvg: 817 },\n },\n monospace: {\n local: ['Courier New', 'CourierNewPSMT'],\n metrics: { unitsPerEm: 2048, ascent: 1705, descent: -615, lineGap: 0, xWidthAvg: 1229 },\n },\n}\n\nfunction localFallbackFor(category: HostThemeFontCategory | undefined): LocalFallbackFace {\n if (category === 'serif') return LOCAL_FALLBACKS.serif\n if (category === 'monospace') return LOCAL_FALLBACKS.monospace\n return LOCAL_FALLBACKS['sans-serif']\n}\n\nfunction percent(value: number): string {\n return `${(Math.round(value * 10000) / 100).toString()}%`\n}\n\n/** Whether metrics are numbers a rule can be written from. */\nfunction usableMetrics(metrics: HostThemeFontMetrics | undefined): metrics is HostThemeFontMetrics {\n return (\n !!metrics &&\n [metrics.unitsPerEm, metrics.ascent, metrics.descent, metrics.lineGap, metrics.xWidthAvg].every(\n (value) => typeof value === 'number' && Number.isFinite(value),\n ) &&\n metrics.unitsPerEm > 0 &&\n metrics.xWidthAvg > 0 &&\n metrics.ascent > 0\n )\n}\n\n/**\n * The `@font-face` rule for a family's metric-matched fallback: the local face\n * of its category, scaled so its average character is as wide as the web\n * font's (`size-adjust`) and given the web font's ascent, descent and line gap\n * at that scale. Text set in it occupies the box the web font will, so the\n * swap moves nothing — the CLS a `font-display: swap` page otherwise pays.\n *\n * The overrides are divided by the size adjustment because the browser\n * applies them to the ADJUSTED size: an ascent of 0.97em on a face scaled to\n * 1.07 has to be written as 0.97/1.07 to land at 0.97em.\n *\n * Empty for metrics that are missing or not numbers.\n */\nexport function metricFallbackFontFaceCss(\n family: string,\n metrics: HostThemeFontMetrics | undefined,\n category?: HostThemeFontCategory,\n): string {\n if (!usableMetrics(metrics)) return ''\n const local = localFallbackFor(category)\n const sizeAdjust =\n metrics.xWidthAvg / metrics.unitsPerEm / (local.metrics.xWidthAvg / local.metrics.unitsPerEm)\n if (!Number.isFinite(sizeAdjust) || sizeAdjust <= 0) return ''\n const over = (value: number) => percent(Math.abs(value) / metrics.unitsPerEm / sizeAdjust)\n return [\n '@font-face{',\n `font-family:${cssFamily(metricFallbackFamily(family))};`,\n `src:${local.local.map((name) => `local(${cssFamily(name)})`).join(',')};`,\n `size-adjust:${percent(sizeAdjust)};`,\n `ascent-override:${over(metrics.ascent)};`,\n `descent-override:${over(metrics.descent)};`,\n `line-gap-override:${over(metrics.lineGap)};`,\n '}',\n ].join('')\n}\n\n/** What one family of a theme needs loaded: the weights, and which as italics. */\nexport interface ThemeFontNeed {\n family: string\n /** Upright weights the theme's text styles and named weights draw with. */\n weights: number[]\n /** The subset of `weights` text styles alone use, which a delivery choice is measured on. */\n textWeights: number[]\n italics: number[]\n}\n\nconst NAMED_WEIGHTS: Record<string, number> = { normal: 400, bold: 700, lighter: 300, bolder: 700 }\n\nfunction numericWeight(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return Math.round(value)\n if (typeof value !== 'string') return undefined\n if (/^\\d{3,4}$/.test(value.trim())) return Number(value.trim())\n return NAMED_WEIGHTS[value.trim().toLowerCase()]\n}\n\n/** MUI's four named weights, before a theme adds its own. */\nconst MUI_WEIGHT_TOKENS: Record<string, number> = {\n fontWeightLight: 300,\n fontWeightRegular: 400,\n fontWeightMedium: 500,\n fontWeightBold: 700,\n}\n\n/**\n * The faces each of a theme's web font families is drawn with (AGL-3656).\n *\n * A theme lists weights for its font (`fonts[].weights`), but the page draws\n * with whatever its text styles say: the platform's ramp sets `h1` in 900 and\n * `h2` in 800, so a theme that loaded Inter at 400, 500 and 700 drew its\n * headlines in a synthesized or substituted weight. The weights here are the\n * union of the listed ones, every text style's weight whose family resolves to\n * this one, and the theme's named weights (`fontWeightLight` … `Black`), which\n * the weight picker offers content.\n *\n * `baseTypography` is the typography the site's theme is layered onto\n * (`siteBaseOptions(...).typography`): a text style the theme leaves alone\n * keeps the base's weight and family.\n *\n * One italic is loaded per family, at the body text's weight, so emphasis in\n * running text is a real italic rather than a slanted upright; a theme can\n * list more under `italics`.\n */\nexport function themeFontNeeds(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n baseTypography: Record<string, unknown> | undefined,\n extraFonts: readonly HostThemeFont[] = [],\n): ThemeFontNeed[] {\n const fonts = [...(theme?.fonts ?? []), ...extraFonts].filter(\n (font) => font.family && (font.source ?? 'google') !== 'system',\n )\n if (!fonts.length) return []\n const base = baseTypography ?? {}\n const variants = (theme?.typography?.variants ?? {}) as Record<string, Record<string, unknown> | undefined>\n const baseStack =\n (typeof theme?.typography?.fontFamily === 'string' && theme.typography.fontFamily) ||\n (typeof base['fontFamily'] === 'string' ? (base['fontFamily'] as string) : '')\n const needs = new Map<string, { family: string; weights: Set<number>; text: Set<number>; italics: Set<number> }>()\n for (const font of fonts) {\n const key = font.family.trim().toLowerCase()\n const need = needs.get(key) ?? { family: font.family.trim(), weights: new Set(), text: new Set(), italics: new Set() }\n for (const weight of font.weights ?? []) if (numericWeight(weight)) need.weights.add(numericWeight(weight) as number)\n for (const weight of font.italics ?? []) if (numericWeight(weight)) need.italics.add(numericWeight(weight) as number)\n needs.set(key, need)\n }\n const needFor = (stack: unknown) => {\n const first = typeof stack === 'string' ? stackFamilies(stack)[0] : undefined\n return first ? needs.get(first.toLowerCase()) : undefined\n }\n\n const variantKeys = new Set<string>([\n ...Object.keys(variants),\n ...Object.keys(base).filter((key) => {\n const value = base[key]\n return !!value && typeof value === 'object' && !Array.isArray(value)\n }),\n ])\n for (const key of variantKeys) {\n const own = variants[key] ?? {}\n const inherited = (base[key] as Record<string, unknown> | undefined) ?? {}\n const need = needFor(own['fontFamily'] ?? inherited['fontFamily'] ?? baseStack)\n if (!need) continue\n const weight = numericWeight(own['fontWeight'] ?? inherited['fontWeight']) ?? 400\n need.weights.add(weight)\n need.text.add(weight)\n }\n\n const bodyNeed = needFor(baseStack)\n if (bodyNeed) {\n const tokens: Record<string, unknown> = { ...MUI_WEIGHT_TOKENS }\n for (const [key, value] of Object.entries(base)) if (/^fontWeight[A-Z]/.test(key)) tokens[key] = value\n for (const [key, value] of Object.entries(tokens)) {\n const weight = numericWeight(value)\n if (!weight) continue\n bodyNeed.weights.add(weight)\n if (key !== 'fontWeightLight' && MUI_WEIGHT_TOKENS[key]) bodyNeed.text.add(weight)\n }\n const body = variants['body1'] ?? {}\n const bodyWeight =\n numericWeight(body['fontWeight'] ?? (base['body1'] as Record<string, unknown> | undefined)?.['fontWeight']) ?? 400\n bodyNeed.italics.add(bodyWeight)\n }\n\n return [...needs.values()].map((need) => ({\n family: need.family,\n weights: [...need.weights].sort((a, b) => a - b),\n textWeights: [...need.text].sort((a, b) => a - b),\n italics: [...need.italics].sort((a, b) => a - b),\n }))\n}\n\n/**\n * The weights to load from what a family offers: each wanted weight as the\n * nearest one offered, without repeats. Unknown availability keeps the\n * wanted weights as they are.\n */\nexport function offeredWeights(wanted: readonly number[], offered: readonly number[] | undefined): number[] {\n if (!offered?.length) return [...new Set(wanted)].sort((a, b) => a - b)\n const picked = wanted.map((weight) =>\n offered.reduce((best, candidate) =>\n Math.abs(candidate - weight) < Math.abs(best - weight) ? candidate : best,\n ),\n )\n return [...new Set(picked)].sort((a, b) => a - b)\n}\n\n/**\n * Google's CSS2 URL for some faces of one family: `ital,wght` tuples, sorted\n * as the API requires. One weight asks for a static instance, which for a\n * variable family is a file holding that weight alone — Inter's Latin 400 is\n * 24 KB that way and 48 KB as the variable file.\n */\nexport function googleFontSheetUrl(\n family: string,\n faces: ReadonlyArray<{ weight: number; style: 'normal' | 'italic' }>,\n): string | undefined {\n const name = family.trim()\n if (!name || !/^[A-Za-z0-9 ]{1,80}$/.test(name) || !faces.length) return undefined\n const tuples = [...new Set(faces.map((face) => `${face.style === 'italic' ? 1 : 0},${face.weight}`))].sort(\n (a, b) => {\n const [ai, aw] = a.split(',').map(Number)\n const [bi, bw] = b.split(',').map(Number)\n return ai - bi || aw - bw\n },\n )\n const axis = tuples.some((tuple) => tuple.startsWith('1,'))\n ? `ital,wght@${tuples.join(';')}`\n : `wght@${tuples.map((tuple) => tuple.slice(2)).join(';')}`\n return `https://fonts.googleapis.com/css2?family=${name.replace(/ /g, '+')}:${axis}&display=swap`\n}\n\n/**\n * The files the first screen paints with, to preload: the body text's face\n * and the headline's, Latin and upright, from whatever faces the page\n * declares. A file covering both is preloaded once.\n */\nexport function siteFontPreloads(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n faces: readonly SiteFontFace[],\n baseTypography?: Record<string, unknown>,\n): string[] {\n const typography = theme?.typography\n const variants = typography?.variants\n const base = baseTypography ?? {}\n const baseH1 = (base['h1'] as Record<string, unknown> | undefined) ?? {}\n const baseBody = (base['body1'] as Record<string, unknown> | undefined) ?? {}\n const fallback = theme?.fonts?.find((font) => (font.source ?? 'google') !== 'system')?.family\n const bodyFamily =\n firstFamily(typography?.fontFamily) ?? firstFamily(base['fontFamily']) ?? fallback\n const pick = (family: string | undefined, weight: number) => {\n if (!family) return undefined\n const wanted = family.toLowerCase()\n const candidates = faces.filter(\n (face) =>\n face.family.toLowerCase() === wanted &&\n face.style === 'normal' &&\n (face.subset === undefined || face.subset === 'latin'),\n )\n return candidates.sort(\n (a, b) => weightDistance(a, weight) - weightDistance(b, weight),\n )[0]\n }\n const body = pick(bodyFamily, numericWeight(variants?.body1?.fontWeight ?? baseBody['fontWeight']) ?? 400)\n const heading = pick(\n firstFamily(variants?.h1?.fontFamily) ?? firstFamily(baseH1['fontFamily']) ?? bodyFamily,\n numericWeight(variants?.h1?.fontWeight ?? baseH1['fontWeight']) ?? 700,\n )\n return [...new Set([body, heading].filter((face): face is SiteFontFace => !!face).map((face) => face.url))]\n}\n"],"names":["metricFallbackFamily","stackFamilies","themeWebFontFamilies","withMetricFallbacks","SELF_HOSTED_FONT_ROUTE","GOOGLE_FONT_FILE_ORIGIN","FONT_FILE_PATH","isSelfHostedFontPath","path","test","selfHostedFontUrl","googleFontFileUrl","FACE_BLOCK","FAMILY","STYLE","WEIGHT","SRC","RANGE","parseGoogleFontFaces","css","faces","match","matchAll","subset","body","family","exec","style","weight","url","slice","length","unicodeRange","trim","push","replace","cssFamily","selfHostedFontFaceCss","map","face","join","firstFamily","stack","undefined","first","split","weightDistance","wanted","low","high","Number","Math","min","abs","latinFace","toLowerCase","candidates","filter","sort","a","b","selfHostedFontPreloads","theme","variants","typography","fallback","fonts","find","font","source","bodyFamily","fontFamily","body1","fontWeight","heading","h1","paths","Boolean","Set","siteFontFaceFromGoogle","mergeSharedFiles","groups","Map","key","group","get","set","values","bounds","flatMap","max","String","siteFontFaceCss","isSiteFontUrl","LOCAL_FALLBACKS","local","metrics","unitsPerEm","ascent","descent","lineGap","xWidthAvg","serif","monospace","localFallbackFor","category","percent","value","round","toString","usableMetrics","every","isFinite","metricFallbackFontFaceCss","sizeAdjust","over","name","NAMED_WEIGHTS","normal","bold","lighter","bolder","numericWeight","MUI_WEIGHT_TOKENS","fontWeightLight","fontWeightRegular","fontWeightMedium","fontWeightBold","themeFontNeeds","baseTypography","extraFonts","base","baseStack","needs","need","weights","text","italics","add","needFor","variantKeys","Object","keys","Array","isArray","own","inherited","bodyNeed","tokens","entries","bodyWeight","textWeights","offeredWeights","offered","picked","reduce","best","candidate","googleFontSheetUrl","tuples","ai","aw","bi","bw","axis","some","tuple","startsWith","siteFontPreloads","baseH1","baseBody","pick"],"mappings":"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"}