@aglyn/shared-ui-theme 1.0.0-beta.221 → 1.0.0-beta.222

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/README.md CHANGED
@@ -18,6 +18,7 @@ From the root entry:
18
18
  - **MUI module augmentation.** Importing the package extends MUI's types with the palette additions these themes use (`tertiary`, `surface`, `tint`), extra typography variants such as `displayXl`, and the matching component `color` overrides. It also re-exports a selection of MUI types and helpers (`Theme`, `ThemeOptions`, `SxProps`, `darkScrollbar`, `visuallyHidden`).
19
19
  - **Default themes.** `consoleThemeLight` / `consoleThemeDark` / `consoleThemeCssVar` / `getConsoleTheme(mode)` and `tenantThemeLight` / `tenantThemeDark`, with their `ThemeOptions` (`consoleOptions`, `tenantOptions` and the dark variants). `siteFallbackTheme(host, scheme)` and `siteBaseOptions(host, scheme)` pick between them for a host.
20
20
  - **Host theme.** A `HostTheme` (the type lives in `@aglyn/shared-data-types`) is a site's theme customization as plain data. `hostThemeToThemeOptions(theme, scheme)` converts it to `ThemeOptions`; `sanitizeHostTheme`, `mergeThemeOptions`, `hasHostTheme` and `getGoogleFontsUrl` support it. `HostThemeProvider` renders children under it, taking `theme`, a required `fallback` theme or `[light, dark]` pair, `baseOptions`, and `initialMode` / `initialDeviceMode` so a server render can choose the scheme before hydration.
21
+ - **Self-hosted fonts.** `util/self-hosted-fonts` reads a Google Fonts CSS2 stylesheet into `@font-face` rules (`parseGoogleFontFaces`), writes them back with every file on the site's own `/api/fonts` route (`selfHostedFontFaceCss`), and picks the one or two faces worth preloading (`selfHostedFontPreloads`). The tenant fetches and serves; this half is pure.
21
22
  - **Light/dark mode.** `createWithThemeProvider({ theme })` returns a higher-order component that provides the theme and mode state; `useThemeMode()` reads and toggles it. The choice is stored in the `theme-color-mode` cookie (`COOKIE_THEME_KEY`).
22
23
  - **Emotion.** `createEmotionCache`, `CacheProvider`, `createLayeredEmotionCache`, `createWithEmotionClientCache`, and the constants `EMOTION_CACHE_KEY`, `APP_EMOTION_CACHE_OPTIONS`, `MUI_CSS_LAYER_NAME`.
23
24
  - **Contrast.** `contrastRatio`, `relativeLuminance`, `meetsContrast`, `accessibleShade`, `accentTextColor`, `accentFillColor`, `auditPaletteContrast`, with `AA_TEXT_CONTRAST` (4.5) and `AA_NON_TEXT_CONTRAST` (3).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-theme",
3
- "version": "1.0.0-beta.221",
3
+ "version": "1.0.0-beta.222",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-types": "1.0.0-beta.221",
29
- "@aglyn/shared-util-tools": "1.0.0-beta.221",
30
- "@aglyn/shared-util-vendor": "1.0.0-beta.221",
28
+ "@aglyn/shared-data-types": "1.0.0-beta.222",
29
+ "@aglyn/shared-util-tools": "1.0.0-beta.222",
30
+ "@aglyn/shared-util-vendor": "1.0.0-beta.222",
31
31
  "@emotion/cache": "^11.14.0",
32
32
  "@emotion/react": "11.14.0",
33
33
  "@swc/helpers": "0.5.23",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { HostTheme } from '@aglyn/shared-data-types';
18
+ /**
19
+ * A theme's Google fonts, served from the site's own origin (AGL-3485).
20
+ *
21
+ * A published page loaded its theme font as a `<link rel="stylesheet">` to
22
+ * `fonts.googleapis.com`: a render-blocking request to a third origin, then a
23
+ * second connection to `fonts.gstatic.com` for the files it names — about
24
+ * 800 ms before first paint on the page Lighthouse measured. Nothing about a
25
+ * theme font needs either round trip at view time. The stylesheet is a short
26
+ * list of `@font-face` rules that changes only when the theme does, and the
27
+ * files are versioned in their own paths (`/s/inter/v18/…woff2`), so neither
28
+ * changes under a URL.
29
+ *
30
+ * So the server fetches the stylesheet once, keeps the rules, and inlines them
31
+ * in the page with each `src` pointing at {@link SELF_HOSTED_FONT_ROUTE} on the
32
+ * site's own origin, which serves the file with a year-long `immutable`
33
+ * policy. The one or two faces the first screen paints with are preloaded.
34
+ * Nothing on the page asks Google for anything, which also means a visitor's
35
+ * address is no longer handed to Google by every page view.
36
+ *
37
+ * This module is the pure half: reading Google's stylesheet, writing the
38
+ * rules back, and choosing the faces to preload. The fetching and the file
39
+ * route live with the tenant (`libs/tenant/runtime/.../self-hosted-fonts.ts`).
40
+ *
41
+ * The rules are REBUILT from validated fields rather than echoed, so the
42
+ * inlined CSS can only ever contain a family name the theme chose, a style, a
43
+ * weight, a unicode range and a path on our own route.
44
+ */
45
+ /** Where a site serves its theme's font files from. */
46
+ export declare const SELF_HOSTED_FONT_ROUTE = "/api/fonts";
47
+ /** The origin Google serves font files from. */
48
+ export declare const GOOGLE_FONT_FILE_ORIGIN = "https://fonts.gstatic.com";
49
+ /** One `@font-face` rule from Google's stylesheet. */
50
+ export interface GoogleFontFace {
51
+ family: string;
52
+ style: 'normal' | 'italic';
53
+ /** A weight (`400`) or, for a variable font, a range (`100 900`). */
54
+ weight: string;
55
+ /** Google's subset comment above the rule (`latin`, `latin-ext`, …). */
56
+ subset?: string;
57
+ /** The file's path below `/s/` on Google's origin. */
58
+ path: string;
59
+ unicodeRange?: string;
60
+ }
61
+ /** Whether a path is one {@link SELF_HOSTED_FONT_ROUTE} will serve. */
62
+ export declare function isSelfHostedFontPath(path: string): boolean;
63
+ /** The site-relative URL a face's file is served from. */
64
+ export declare function selfHostedFontUrl(path: string): string;
65
+ /** Google's own URL for a self-hosted path. */
66
+ export declare function googleFontFileUrl(path: string): string;
67
+ /**
68
+ * Reads the `@font-face` rules out of a Google Fonts CSS2 stylesheet fetched
69
+ * with a browser that accepts WOFF2. A rule that is not exactly the shape
70
+ * Google writes — a missing field, a file off Google's origin, a path the
71
+ * route would refuse — is dropped, never repaired.
72
+ */
73
+ export declare function parseGoogleFontFaces(css: string): GoogleFontFace[];
74
+ /**
75
+ * The faces as `@font-face` rules on the site's own route, every one
76
+ * `font-display: swap` so text paints in the fallback while a file loads.
77
+ *
78
+ * Every subset Google declared is kept, each with its `unicode-range`: a
79
+ * browser downloads a subset only when the page uses a character in it, so
80
+ * the Cyrillic file costs an English page nothing, and a Polish or Greek page
81
+ * keeps its typeface. Only the Latin faces are preloaded.
82
+ */
83
+ export declare function selfHostedFontFaceCss(faces: readonly GoogleFontFace[]): string;
84
+ /**
85
+ * The paths of the one or two faces the first screen paints with: the body
86
+ * text's face, and the headline's when it is a different file. Those are the
87
+ * fonts the largest text on the page — usually the LCP element — waits for,
88
+ * so they are worth starting with the document; every other face is fetched
89
+ * when the page first uses it.
90
+ *
91
+ * The body face is the theme's `fontFamily` (else the first Google font it
92
+ * loads) at `body1`'s weight, 400 by default; the headline face is `h1`'s
93
+ * family and weight, 700 by default. A variable font serves every weight
94
+ * from one file, which is then preloaded once.
95
+ */
96
+ export declare function selfHostedFontPreloads(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, faces: readonly GoogleFontFace[]): string[];
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /**
18
+ * A theme's Google fonts, served from the site's own origin (AGL-3485).
19
+ *
20
+ * A published page loaded its theme font as a `<link rel="stylesheet">` to
21
+ * `fonts.googleapis.com`: a render-blocking request to a third origin, then a
22
+ * second connection to `fonts.gstatic.com` for the files it names — about
23
+ * 800 ms before first paint on the page Lighthouse measured. Nothing about a
24
+ * theme font needs either round trip at view time. The stylesheet is a short
25
+ * list of `@font-face` rules that changes only when the theme does, and the
26
+ * files are versioned in their own paths (`/s/inter/v18/…woff2`), so neither
27
+ * changes under a URL.
28
+ *
29
+ * So the server fetches the stylesheet once, keeps the rules, and inlines them
30
+ * in the page with each `src` pointing at {@link SELF_HOSTED_FONT_ROUTE} on the
31
+ * site's own origin, which serves the file with a year-long `immutable`
32
+ * policy. The one or two faces the first screen paints with are preloaded.
33
+ * Nothing on the page asks Google for anything, which also means a visitor's
34
+ * address is no longer handed to Google by every page view.
35
+ *
36
+ * This module is the pure half: reading Google's stylesheet, writing the
37
+ * rules back, and choosing the faces to preload. The fetching and the file
38
+ * route live with the tenant (`libs/tenant/runtime/.../self-hosted-fonts.ts`).
39
+ *
40
+ * The rules are REBUILT from validated fields rather than echoed, so the
41
+ * inlined CSS can only ever contain a family name the theme chose, a style, a
42
+ * weight, a unicode range and a path on our own route.
43
+ */ /** Where a site serves its theme's font files from. */ export const SELF_HOSTED_FONT_ROUTE = '/api/fonts';
44
+ /** The origin Google serves font files from. */ export const GOOGLE_FONT_FILE_ORIGIN = 'https://fonts.gstatic.com';
45
+ /**
46
+ * A font file's path on {@link GOOGLE_FONT_FILE_ORIGIN} below `/s/`, as the
47
+ * 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
+ /** Whether a path is one {@link SELF_HOSTED_FONT_ROUTE} will serve. */ export function isSelfHostedFontPath(path) {
51
+ return FONT_FILE_PATH.test(path);
52
+ }
53
+ /** The site-relative URL a face's file is served from. */ export function selfHostedFontUrl(path) {
54
+ return `${SELF_HOSTED_FONT_ROUTE}/${path}`;
55
+ }
56
+ /** Google's own URL for a self-hosted path. */ export function googleFontFileUrl(path) {
57
+ return `${GOOGLE_FONT_FILE_ORIGIN}/s/${path}`;
58
+ }
59
+ const FACE_BLOCK = /(?:\/\*\s*([a-z0-9-]+)\s*\*\/\s*)?@font-face\s*\{([^}]*)\}/g;
60
+ const FAMILY = /font-family:\s*'([^']{1,80})'/;
61
+ const STYLE = /font-style:\s*(normal|italic)/;
62
+ const WEIGHT = /font-weight:\s*(\d{3}(?:\s+\d{3})?)/;
63
+ const SRC = /src:\s*url\((https:\/\/fonts\.gstatic\.com\/s\/[^)\s]+)\)\s*format\('woff2'\)/;
64
+ const RANGE = /unicode-range:\s*([U+0-9A-Fa-f?,\s-]{1,4000});/;
65
+ /**
66
+ * Reads the `@font-face` rules out of a Google Fonts CSS2 stylesheet fetched
67
+ * with a browser that accepts WOFF2. A rule that is not exactly the shape
68
+ * Google writes — a missing field, a file off Google's origin, a path the
69
+ * route would refuse — is dropped, never repaired.
70
+ */ export function parseGoogleFontFaces(css) {
71
+ const faces = [];
72
+ for (const match of css.matchAll(FACE_BLOCK)){
73
+ var _FAMILY_exec, _STYLE_exec, _WEIGHT_exec, _SRC_exec, _RANGE_exec_, _RANGE_exec;
74
+ const [, subset, body] = match;
75
+ const family = (_FAMILY_exec = FAMILY.exec(body)) == null ? void 0 : _FAMILY_exec[1];
76
+ const style = (_STYLE_exec = STYLE.exec(body)) == null ? void 0 : _STYLE_exec[1];
77
+ const weight = (_WEIGHT_exec = WEIGHT.exec(body)) == null ? void 0 : _WEIGHT_exec[1];
78
+ const url = (_SRC_exec = SRC.exec(body)) == null ? void 0 : _SRC_exec[1];
79
+ if (!family || !style || !weight || !url) continue;
80
+ const path = url.slice(`${GOOGLE_FONT_FILE_ORIGIN}/s/`.length);
81
+ if (!isSelfHostedFontPath(path)) continue;
82
+ const unicodeRange = (_RANGE_exec = RANGE.exec(body)) == null ? void 0 : (_RANGE_exec_ = _RANGE_exec[1]) == null ? void 0 : _RANGE_exec_.trim();
83
+ faces.push(_extends({
84
+ family,
85
+ style,
86
+ weight: weight.replace(/\s+/g, ' ')
87
+ }, subset ? {
88
+ subset
89
+ } : {}, {
90
+ path
91
+ }, unicodeRange ? {
92
+ unicodeRange
93
+ } : {}));
94
+ }
95
+ return faces;
96
+ }
97
+ /** A family name as it may appear quoted in CSS. */ function cssFamily(family) {
98
+ return `'${family.replace(/['\\\n\r]/g, '')}'`;
99
+ }
100
+ /**
101
+ * The faces as `@font-face` rules on the site's own route, every one
102
+ * `font-display: swap` so text paints in the fallback while a file loads.
103
+ *
104
+ * Every subset Google declared is kept, each with its `unicode-range`: a
105
+ * browser downloads a subset only when the page uses a character in it, so
106
+ * the Cyrillic file costs an English page nothing, and a Polish or Greek page
107
+ * keeps its typeface. Only the Latin faces are preloaded.
108
+ */ export function selfHostedFontFaceCss(faces) {
109
+ return faces.map((face)=>[
110
+ '@font-face{',
111
+ `font-family:${cssFamily(face.family)};`,
112
+ `font-style:${face.style};`,
113
+ `font-weight:${face.weight};`,
114
+ 'font-display:swap;',
115
+ `src:url(${selfHostedFontUrl(face.path)}) format('woff2');`,
116
+ face.unicodeRange ? `unicode-range:${face.unicodeRange};` : '',
117
+ '}'
118
+ ].join('')).join('');
119
+ }
120
+ /** The first family named in a CSS `font-family` stack. */ function firstFamily(stack) {
121
+ var _stack_split_;
122
+ if (typeof stack !== 'string') return undefined;
123
+ const first = (_stack_split_ = stack.split(',')[0]) == null ? void 0 : _stack_split_.trim().replace(/^['"]|['"]$/g, '');
124
+ return first || undefined;
125
+ }
126
+ /** How far a face is from a wanted weight; 0 when its range covers it. */ function weightDistance(face, wanted) {
127
+ const [low, high = low] = face.weight.split(' ').map(Number);
128
+ if (wanted >= low && wanted <= high) return 0;
129
+ return Math.min(Math.abs(wanted - low), Math.abs(wanted - high));
130
+ }
131
+ /** The Latin, upright face of `family` closest to `weight`. */ function latinFace(faces, family, weight) {
132
+ if (!family) return undefined;
133
+ const wanted = family.toLowerCase();
134
+ const candidates = faces.filter((face)=>face.family.toLowerCase() === wanted && face.style === 'normal' && (face.subset === undefined || face.subset === 'latin'));
135
+ return candidates.sort((a, b)=>weightDistance(a, weight) - weightDistance(b, weight))[0];
136
+ }
137
+ /**
138
+ * The paths of the one or two faces the first screen paints with: the body
139
+ * text's face, and the headline's when it is a different file. Those are the
140
+ * fonts the largest text on the page — usually the LCP element — waits for,
141
+ * so they are worth starting with the document; every other face is fetched
142
+ * when the page first uses it.
143
+ *
144
+ * The body face is the theme's `fontFamily` (else the first Google font it
145
+ * loads) at `body1`'s weight, 400 by default; the headline face is `h1`'s
146
+ * family and weight, 700 by default. A variable font serves every weight
147
+ * from one file, which is then preloaded once.
148
+ */ export function selfHostedFontPreloads(theme, faces) {
149
+ var _firstFamily, _ref, _firstFamily1, _ref1;
150
+ var _theme_fonts_find, _theme_fonts, _variants_body1, _variants_h1, _variants_h11;
151
+ const typography = theme == null ? void 0 : theme.typography;
152
+ const variants = typography == null ? void 0 : typography.variants;
153
+ const fallback = theme == null ? void 0 : (_theme_fonts = theme.fonts) == null ? void 0 : (_theme_fonts_find = _theme_fonts.find((font)=>{
154
+ var _font_source;
155
+ return ((_font_source = font.source) != null ? _font_source : 'google') === 'google';
156
+ })) == null ? void 0 : _theme_fonts_find.family;
157
+ const bodyFamily = (_firstFamily = firstFamily(typography == null ? void 0 : typography.fontFamily)) != null ? _firstFamily : fallback;
158
+ const body = latinFace(faces, bodyFamily, (_ref = variants == null ? void 0 : (_variants_body1 = variants.body1) == null ? void 0 : _variants_body1.fontWeight) != null ? _ref : 400);
159
+ const heading = latinFace(faces, (_firstFamily1 = firstFamily(variants == null ? void 0 : (_variants_h1 = variants.h1) == null ? void 0 : _variants_h1.fontFamily)) != null ? _firstFamily1 : bodyFamily, (_ref1 = variants == null ? void 0 : (_variants_h11 = variants.h1) == null ? void 0 : _variants_h11.fontWeight) != null ? _ref1 : 700);
160
+ const paths = [
161
+ body,
162
+ heading
163
+ ].filter((face)=>Boolean(face)).map((face)=>selfHostedFontUrl(face.path));
164
+ return [
165
+ ...new Set(paths)
166
+ ];
167
+ }
168
+
169
+ //# sourceMappingURL=self-hosted-fonts.js.map
@@ -0,0 +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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"}