blume 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +449 -135
- package/dist/cli/index.js.map +24 -23
- package/dist/types/ai/ask-context.d.ts +78 -0
- package/dist/types/core/config-input.d.ts +54 -2
- package/dist/types/core/data.d.ts +19 -2
- package/dist/types/core/open-in-chat.d.ts +9 -0
- package/dist/types/core/schema.d.ts +48 -1
- package/dist/types/core/types.d.ts +10 -3
- package/dist/types/openapi/references.d.ts +9 -0
- package/dist/types/search/orama-index.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +67 -5
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/ai.mdx +35 -0
- package/docs/configuration/index.mdx +14 -2
- package/docs/configuration/search.mdx +4 -4
- package/docs/configuration/theming.mdx +4 -2
- package/docs/reference/cli.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/ask-context.ts +51 -11
- package/src/ai/mcp/data.ts +3 -2
- package/src/ai/mcp/server.ts +3 -2
- package/src/assets/icon-dark.png +0 -0
- package/src/astro/generate.ts +172 -18
- package/src/astro/templates.ts +89 -15
- package/src/components/content/AccordionItem.astro +4 -0
- package/src/components/content/Update.astro +3 -0
- package/src/components/islands/AskAI.astro +6 -0
- package/src/components/islands/ask-ai.tsx +39 -9
- package/src/components/layout/Analytics.astro +9 -1
- package/src/components/layout/Favicon.astro +29 -8
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/PageActions.astro +120 -78
- package/src/components/layout/PageFeedback.astro +12 -3
- package/src/components/layout/PageLayout.astro +79 -5
- package/src/components/layout/ReferenceLayout.astro +12 -9
- package/src/components/layout/RootLayout.astro +153 -121
- package/src/components/layout/Search.astro +41 -26
- package/src/components/layout/drawer-inert.ts +10 -5
- package/src/components/layout/head-scripts.ts +34 -16
- package/src/components/layout/nav-utils.ts +34 -15
- package/src/components/layout/search/orama.ts +3 -2
- package/src/components/openapi/AsyncApiOperation.astro +22 -7
- package/src/components/openapi/MessageComposer.astro +238 -0
- package/src/components/openapi/Operation.astro +26 -12
- package/src/components/openapi/PanelTabs.astro +7 -0
- package/src/components/openapi/Playground.astro +320 -0
- package/src/components/openapi/RequestPanel.astro +1 -0
- package/src/components/openapi/async-snippets.ts +20 -7
- package/src/components/openapi/async.ts +13 -2
- package/src/components/openapi/message-composer.ts +242 -0
- package/src/components/openapi/message-model.ts +108 -0
- package/src/components/openapi/message.ts +153 -0
- package/src/components/openapi/operation-model.ts +260 -0
- package/src/components/openapi/playground-client.ts +486 -0
- package/src/components/openapi/playground-schema.ts +109 -0
- package/src/components/openapi/request.ts +287 -0
- package/src/components/openapi/security.ts +0 -56
- package/src/components/openapi/snippets.ts +23 -136
- package/src/components/openapi/validate-json.ts +144 -0
- package/src/components/openapi/ws-client.ts +194 -0
- package/src/core/config-input.ts +67 -1
- package/src/core/content-assets.ts +66 -15
- package/src/core/data.ts +16 -2
- package/src/core/last-modified.ts +76 -2
- package/src/core/links.ts +30 -4
- package/src/core/navigation.ts +26 -1
- package/src/core/open-in-chat.ts +17 -0
- package/src/core/project-graph.ts +11 -0
- package/src/core/schema.ts +60 -1
- package/src/core/server-features.ts +11 -0
- package/src/core/sources/normalize.ts +10 -2
- package/src/core/types.ts +10 -3
- package/src/deploy/vercel-negotiation.ts +34 -14
- package/src/og/card.ts +3 -1
- package/src/openapi/model.ts +7 -0
- package/src/openapi/proxy.ts +217 -0
- package/src/openapi/references.ts +8 -0
- package/src/openapi/source.ts +13 -0
- package/src/registry/eject.ts +4 -5
- package/src/search/orama-index.ts +109 -36
- package/src/theme/entry.ts +15 -2
- package/src/theme/fonts.ts +75 -3
|
@@ -56,19 +56,59 @@ const toFacetTerms = (facets: Record<string, string>): string[] =>
|
|
|
56
56
|
const BOOST = { description: 2, title: 3 };
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* The script whose text Orama's default tokenizer keeps mostly intact. Its
|
|
60
|
+
* delimiter class is `/[^A-Za-zàèéìòóù0-9_'-]+/`, so every character outside
|
|
61
|
+
* that set counts as a separator: text in any other script collapses to zero
|
|
62
|
+
* tokens and every query silently returns no hits. Unspaced scripts are the
|
|
63
|
+
* best-known casualty, but the failure is not about spacing — Russian, Greek,
|
|
64
|
+
* Hebrew and Hindi lose their tokens the same way Japanese does.
|
|
63
65
|
*/
|
|
64
|
-
const
|
|
66
|
+
const LATIN_SCRIPT = "Latn";
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* Parse a locale tag, tolerating the legacy forms that reach here from
|
|
70
|
+
* hand-written config: underscores (`ru_RU`), POSIX suffixes (`ja_JP.UTF-8`,
|
|
71
|
+
* `zh_TW@Big5`), and extlang tags ICU rejects (`zh-cmn-Hans`). `Intl.Locale`
|
|
72
|
+
* throws on all of these, so a failed parse retries with the primary language
|
|
73
|
+
* subtag alone — enough to resolve the script, which is all that gates the
|
|
74
|
+
* tokenizer. Returns `undefined` when even that subtag is unparseable.
|
|
75
|
+
*/
|
|
76
|
+
const parseLocale = (tag: string): Intl.Locale | undefined => {
|
|
77
|
+
const hyphenated = tag.replaceAll("_", "-");
|
|
78
|
+
try {
|
|
79
|
+
return new Intl.Locale(hyphenated);
|
|
80
|
+
} catch {
|
|
81
|
+
// Retry below with the primary subtag.
|
|
82
|
+
}
|
|
83
|
+
const [primary = ""] = hyphenated.split("-");
|
|
84
|
+
try {
|
|
85
|
+
return new Intl.Locale(primary);
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The maximized locale — script and language filled in from CLDR likely-subtags
|
|
93
|
+
* and alias data, so `ru` resolves to `Cyrl`, `cmn` to the canonical `zh`, and
|
|
94
|
+
* an explicit script subtag is honored: `sr-Latn` reports `Latn` while
|
|
95
|
+
* `az-Cyrl` reports `Cyrl`. `undefined` for tags {@link parseLocale} cannot
|
|
96
|
+
* salvage; a well-formed tag ICU knows nothing about keeps an `undefined`
|
|
97
|
+
* script instead.
|
|
98
|
+
*/
|
|
99
|
+
const resolveLocale = (tag: string): Intl.Locale | undefined =>
|
|
100
|
+
parseLocale(tag)?.maximize();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Indexes whose language maximizes to one of these scripts are built as
|
|
104
|
+
* character bigrams rather than whole segments, and queried to match
|
|
105
|
+
* accordingly. Japanese and Chinese write compounds in {@link BIGRAM_SCRIPTS}
|
|
106
|
+
* without delimiters, and the writing system — not the language subtag — is
|
|
107
|
+
* what carries that convention: `yue` (Cantonese) maximizes to `Hant` and
|
|
108
|
+
* needs bigrams the same way `zh` does. Korean maximizes to `Kore` and Thai
|
|
109
|
+
* to `Thai`; Korean separates words with spaces and Thai has no comparable
|
|
110
|
+
* bigram convention, so both keep the plain segmented tokens even where a
|
|
111
|
+
* page mixes in Han or kana.
|
|
72
112
|
*
|
|
73
113
|
* Dictionary segmentation alone drops the adjacency that makes a compound term
|
|
74
114
|
* distinctive: 資金決済法 becomes 資金 / 決済 / 法, and because Orama scores a
|
|
@@ -77,7 +117,19 @@ const SEGMENTED_LANGUAGES = new Set(["ja", "ko", "th", "zh"]);
|
|
|
77
117
|
* terms, and dropping the whole-segment tokens keeps a fragment as common as
|
|
78
118
|
* 法 from matching on its own.
|
|
79
119
|
*/
|
|
80
|
-
const
|
|
120
|
+
const BIGRAM_INDEX_SCRIPTS = new Set(["Hans", "Hant", "Jpan"]);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether the index keyed to this tokenizer language is built from bigrams.
|
|
124
|
+
* {@link segmentingTokenizer} stores the canonical maximized language, so the
|
|
125
|
+
* builder and the strict pass in {@link queryOramaIndex} share this predicate
|
|
126
|
+
* — an index is never built from bigrams that the query side then matches
|
|
127
|
+
* loosely, or vice versa.
|
|
128
|
+
*/
|
|
129
|
+
const isBigramLanguage = (language: string): boolean => {
|
|
130
|
+
const script = resolveLocale(language)?.script;
|
|
131
|
+
return script !== undefined && BIGRAM_INDEX_SCRIPTS.has(script);
|
|
132
|
+
};
|
|
81
133
|
|
|
82
134
|
/**
|
|
83
135
|
* Segments written entirely in these scripts are the ones re-cut into bigrams,
|
|
@@ -107,6 +159,23 @@ const BIGRAM_SCRIPTS =
|
|
|
107
159
|
const TERM =
|
|
108
160
|
/[\p{L}\p{M}\p{N}]+(?:(?:['’](?=\p{L})|(?<=\p{N})[.,](?=\p{N}))[\p{L}\p{M}\p{N}]+)*/gu;
|
|
109
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A term written entirely in Latin script (plus digits and the separators
|
|
164
|
+
* {@link TERM} keeps within a word). Orama's default tokenizer folds the
|
|
165
|
+
* accented vowels it recognizes (café → cafe), so a segmented index folds
|
|
166
|
+
* Latin terms too — otherwise switching a Cyrillic- or Greek-default site to
|
|
167
|
+
* the segmenting tokenizer would silently drop the unaccented-query matches
|
|
168
|
+
* the default tokenizer provided. Only all-Latin terms fold: marks are
|
|
169
|
+
* spelling elsewhere (Thai vowels and tones; the breve that separates
|
|
170
|
+
* Cyrillic й from и), so a term carrying any other script keeps its marks.
|
|
171
|
+
*/
|
|
172
|
+
const LATIN_TERM = /^[\p{Script=Latin}\p{N}'’.,]+$/u;
|
|
173
|
+
|
|
174
|
+
const MARKS = /\p{M}+/gu;
|
|
175
|
+
|
|
176
|
+
const foldDiacritics = (term: string): string =>
|
|
177
|
+
LATIN_TERM.test(term) ? term.normalize("NFD").replace(MARKS, "") : term;
|
|
178
|
+
|
|
110
179
|
/**
|
|
111
180
|
* Emit every overlapping 2-character window of `run`, or the lone character.
|
|
112
181
|
* Windows are cut by code point: an ideograph outside the basic plane is a
|
|
@@ -134,6 +203,14 @@ const addBigrams = (run: string, tokens: Set<string>): void => {
|
|
|
134
203
|
}
|
|
135
204
|
};
|
|
136
205
|
|
|
206
|
+
/**
|
|
207
|
+
* `Intl.Segmenter` is missing on some runtimes even though the lib type
|
|
208
|
+
* declares it, so the constructor's presence is probed before use.
|
|
209
|
+
*/
|
|
210
|
+
const hasSegmenter = (
|
|
211
|
+
segmenter: typeof Intl.Segmenter | undefined
|
|
212
|
+
): segmenter is typeof Intl.Segmenter => typeof segmenter === "function";
|
|
213
|
+
|
|
137
214
|
/**
|
|
138
215
|
* A word-segmenting tokenizer for languages the default splitter can't handle,
|
|
139
216
|
* built on `Intl.Segmenter` (the same engine `@orama/tokenizers` wraps).
|
|
@@ -141,37 +218,32 @@ const addBigrams = (run: string, tokens: Set<string>): void => {
|
|
|
141
218
|
* upstream tokenizers — so decomposed text (macOS filenames, some CMS
|
|
142
219
|
* pipelines) indexes the same terms a composed query produces, and Latin
|
|
143
220
|
* terms ("GDPR", English pages on a mixed-locale site) still match
|
|
144
|
-
* case-insensitively
|
|
145
|
-
*
|
|
146
|
-
*
|
|
221
|
+
* case-insensitively, with their diacritics folded by
|
|
222
|
+
* {@link foldDiacritics}. Returns `undefined` for scripts the default
|
|
223
|
+
* tokenizer already serves ({@link resolveLocale} decides, so `sr-Latn` keeps
|
|
224
|
+
* the default while `az-Cyrl` is segmented), and on runtimes without
|
|
225
|
+
* `Intl.Segmenter`, where the caller falls back to Orama's default.
|
|
147
226
|
*
|
|
148
|
-
* On a {@link
|
|
227
|
+
* On a {@link BIGRAM_INDEX_SCRIPTS} index, runs of adjacent
|
|
149
228
|
* {@link BIGRAM_SCRIPTS} segments are joined and re-cut into character
|
|
150
229
|
* bigrams; everything else (Latin, digits, and every segment on a Korean or
|
|
151
230
|
* Thai index) is emitted one {@link TERM} at a time. Separators end a run
|
|
152
231
|
* either way — whether they stand between segments, as 「クーリング・オフ」
|
|
153
232
|
* does, or inside one.
|
|
154
233
|
*/
|
|
155
|
-
/**
|
|
156
|
-
* `Intl.Segmenter` is missing on some runtimes even though the lib type
|
|
157
|
-
* declares it, so the constructor's presence is probed before use.
|
|
158
|
-
*/
|
|
159
|
-
const hasSegmenter = (
|
|
160
|
-
segmenter: typeof Intl.Segmenter | undefined
|
|
161
|
-
): segmenter is typeof Intl.Segmenter => typeof segmenter === "function";
|
|
162
|
-
|
|
163
234
|
const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
235
|
+
const resolved = resolveLocale(locale ?? "");
|
|
236
|
+
if (!resolved?.script || resolved.script === LATIN_SCRIPT) {
|
|
166
237
|
return;
|
|
167
238
|
}
|
|
168
239
|
if (!hasSegmenter(Intl.Segmenter)) {
|
|
169
240
|
return;
|
|
170
241
|
}
|
|
242
|
+
// The canonical maximized language (`cmn` → `zh`), so the bigram predicate
|
|
243
|
+
// here and the strict query pass read the same name.
|
|
244
|
+
const { language } = resolved;
|
|
171
245
|
const segmenter = new Intl.Segmenter(language, { granularity: "word" });
|
|
172
|
-
|
|
173
|
-
// built from bigrams that the query side then matches loosely.
|
|
174
|
-
const bigram = BIGRAM_LANGUAGES.has(language);
|
|
246
|
+
const bigram = isBigramLanguage(language);
|
|
175
247
|
return {
|
|
176
248
|
language,
|
|
177
249
|
normalizationCache: new Map(),
|
|
@@ -190,7 +262,7 @@ const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
|
190
262
|
return;
|
|
191
263
|
}
|
|
192
264
|
flush();
|
|
193
|
-
tokens.add(term);
|
|
265
|
+
tokens.add(foldDiacritics(term));
|
|
194
266
|
};
|
|
195
267
|
for (const segment of segmenter.segment(
|
|
196
268
|
raw.normalize("NFC").toLowerCase()
|
|
@@ -224,12 +296,13 @@ const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
|
224
296
|
* Build an in-memory Orama full-text index from search documents. Shared by the
|
|
225
297
|
* Orama client loader (browser), the MCP server, and Ask AI grounding (Node),
|
|
226
298
|
* so ranking is identical wherever docs are queried. `locale` — the site's
|
|
227
|
-
* `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for
|
|
228
|
-
*
|
|
229
|
-
* belongs to the database, so on a mixed-locale site it
|
|
230
|
-
* document
|
|
231
|
-
*
|
|
232
|
-
*
|
|
299
|
+
* `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for every
|
|
300
|
+
* non-Latin script, all of which Orama's default tokenizer reduces to zero
|
|
301
|
+
* tokens; the tokenizer belongs to the database, so on a mixed-locale site it
|
|
302
|
+
* applies to every document. That is safe in one direction only: Latin words
|
|
303
|
+
* survive segmentation intact, so English pages on a segmented index stay
|
|
304
|
+
* searchable, but non-Latin translations on a Latin-default index still
|
|
305
|
+
* collapse to zero tokens.
|
|
233
306
|
*/
|
|
234
307
|
export const buildOramaIndex = async (
|
|
235
308
|
documents: OramaDoc[],
|
|
@@ -322,7 +395,7 @@ export const queryOramaIndex = async (
|
|
|
322
395
|
};
|
|
323
396
|
const params =
|
|
324
397
|
Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
|
|
325
|
-
const bigrammed =
|
|
398
|
+
const bigrammed = isBigramLanguage(db.tokenizer?.language ?? "");
|
|
326
399
|
// The result-document generic is OramaDoc because `buildOramaIndex` is the
|
|
327
400
|
// only writer to this database and inserts OramaDoc records (plus the
|
|
328
401
|
// derived `facetTerms`).
|
package/src/theme/entry.ts
CHANGED
|
@@ -179,7 +179,11 @@ ${THEME_MAPPING}
|
|
|
179
179
|
scroll-padding-top: 4.5rem;
|
|
180
180
|
text-rendering: optimizeLegibility;
|
|
181
181
|
}
|
|
182
|
-
/* Headings use the display font (defaults to the body font when unset).
|
|
182
|
+
/* Headings use the display font (defaults to the body font when unset).
|
|
183
|
+
The tightened tracking is part of the theme, not the font: display-tuned
|
|
184
|
+
families bake it into their metrics, but a text family promoted to
|
|
185
|
+
headings (including the Inter default) reads loose without it. -0.05em
|
|
186
|
+
was matched visually against Inter Tight, the previous display default. */
|
|
183
187
|
h1,
|
|
184
188
|
h2,
|
|
185
189
|
h3,
|
|
@@ -187,6 +191,7 @@ ${THEME_MAPPING}
|
|
|
187
191
|
h5,
|
|
188
192
|
h6 {
|
|
189
193
|
font-family: var(--font-display);
|
|
194
|
+
letter-spacing: -0.05em;
|
|
190
195
|
}
|
|
191
196
|
:focus-visible {
|
|
192
197
|
outline: 2px solid var(--blume-accent);
|
|
@@ -207,6 +212,13 @@ ${THEME_MAPPING}
|
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
214
|
|
|
215
|
+
/* Same-origin navigations ride Astro's client router (the layouts render
|
|
216
|
+
ClientRouter), which owns the page-transition styling — including its own
|
|
217
|
+
prefers-reduced-motion guard — so the theme declares nothing about it. The
|
|
218
|
+
cross-document view-transition opt-in that briefly lived here is
|
|
219
|
+
deliberately gone: mixing it with the client router double-animates the
|
|
220
|
+
full-load navigations the router hands back to the browser. */
|
|
221
|
+
|
|
210
222
|
/* Code reads left-to-right regardless of page direction; only the surrounding
|
|
211
223
|
chrome mirrors for RTL. Inline code is isolated so LTR identifiers don't
|
|
212
224
|
disturb the bidi flow of right-to-left prose. */
|
|
@@ -242,9 +254,10 @@ ${THEME_MAPPING}
|
|
|
242
254
|
line-height: 1.7;
|
|
243
255
|
}
|
|
244
256
|
|
|
257
|
+
/* No letter-spacing here: prose headings inherit the base h1-h6 rule's
|
|
258
|
+
display tracking, same as headings outside the prose column. */
|
|
245
259
|
.prose :where(h1, h2, h3, h4) {
|
|
246
260
|
font-weight: 500;
|
|
247
|
-
letter-spacing: 0;
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
/* A heading can carry one long unbreakable token — an OpenAPI operation's title
|
package/src/theme/fonts.ts
CHANGED
|
@@ -352,6 +352,78 @@ export const buildFontsCss = (fonts: FontsConfig): string => {
|
|
|
352
352
|
: "";
|
|
353
353
|
};
|
|
354
354
|
|
|
355
|
-
/**
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Weights worth preloading per role — the faces above-the-fold text actually
|
|
357
|
+
* renders in: body copy and UI chrome at 400/500, headings at 500/600, code at
|
|
358
|
+
* 400. Every other face still loads on demand through its `@font-face` rule
|
|
359
|
+
* (and `font-display: swap` never blocks text on it), so preloading the long
|
|
360
|
+
* tail only competes with the critical CSS for bandwidth and pushes LCP out.
|
|
361
|
+
*/
|
|
362
|
+
const PRELOAD_WEIGHTS = {
|
|
363
|
+
body: [400, 500],
|
|
364
|
+
display: [500, 600],
|
|
365
|
+
mono: [400],
|
|
366
|
+
} satisfies Record<FontSlot, number[]>;
|
|
367
|
+
|
|
368
|
+
/** One `<Font>` render in the head: its CSS variable + weights to preload. */
|
|
369
|
+
export interface FontHead {
|
|
370
|
+
cssVariable: string;
|
|
371
|
+
preloadWeights: number[];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The weights an entry's faces declare (`undefined` = inferred from files). */
|
|
375
|
+
const entryWeights = (entry: FontEntry): (number | string | undefined)[] =>
|
|
376
|
+
entry.kind === "remote"
|
|
377
|
+
? entry.weights
|
|
378
|
+
: entry.variants.map((variant) => variant.weight);
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The role's preferred preload weights, narrowed to faces the family loads.
|
|
382
|
+
* Variable ranges (`"100..900"`) and weight-inferred local files can serve any
|
|
383
|
+
* weight, so they keep the preferred list; a family whose numeric weights miss
|
|
384
|
+
* the preferred ones entirely preloads all of its faces instead — those are
|
|
385
|
+
* what its text renders in.
|
|
386
|
+
*/
|
|
387
|
+
const preloadWeightsFor = (slot: FontSlot, entry: FontEntry): number[] => {
|
|
388
|
+
const preferred = PRELOAD_WEIGHTS[slot];
|
|
389
|
+
const weights = entryWeights(entry);
|
|
390
|
+
const numeric = weights.filter(
|
|
391
|
+
(weight): weight is number => typeof weight === "number"
|
|
392
|
+
);
|
|
393
|
+
const hits = preferred.filter((weight) => numeric.includes(weight));
|
|
394
|
+
if (hits.length > 0) {
|
|
395
|
+
return hits;
|
|
396
|
+
}
|
|
397
|
+
return numeric.length === weights.length ? numeric : preferred;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The fonts to feed Astro's `<Font>` component in the document head, deduped
|
|
402
|
+
* by CSS variable with preload weights unioned across the roles that share a
|
|
403
|
+
* family (so `display` and `body` both set to Inter preload 400/500/600 once).
|
|
404
|
+
*/
|
|
405
|
+
export const configuredFonts = (fonts: FontsConfig): FontHead[] => {
|
|
406
|
+
if (!fonts) {
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
const heads = new Map<string, Set<number>>();
|
|
410
|
+
for (const slot of SLOTS) {
|
|
411
|
+
const value = fonts[slot];
|
|
412
|
+
if (value === undefined) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const entry = resolveFontValue(slot, value);
|
|
416
|
+
if (!entry) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const weights = heads.get(entry.cssVariable) ?? new Set();
|
|
420
|
+
for (const weight of preloadWeightsFor(slot, entry)) {
|
|
421
|
+
weights.add(weight);
|
|
422
|
+
}
|
|
423
|
+
heads.set(entry.cssVariable, weights);
|
|
424
|
+
}
|
|
425
|
+
return [...heads].map(([cssVariable, weights]) => ({
|
|
426
|
+
cssVariable,
|
|
427
|
+
preloadWeights: [...weights].toSorted((a, b) => a - b),
|
|
428
|
+
}));
|
|
429
|
+
};
|