@hyperframes/core 0.9.12 → 0.9.14

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.
@@ -142,6 +142,7 @@ export interface InjectDeterministicFontFacesOptions {
142
142
  * planning; tests may lower delays and timeouts without replacing timers.
143
143
  */
144
144
  fontFetchRetryPolicy?: Partial<FontFetchRetryPolicy>;
145
+ projectFontFamilies?: readonly string[];
145
146
  /**
146
147
  * When `true` (default for local renders), fonts that aren't resolved by
147
148
  * the bundled alias map or Google Fonts are located on the local filesystem,
@@ -150,5 +151,6 @@ export interface InjectDeterministicFontFacesOptions {
150
151
  * to contain the same fonts as the authoring machine.
151
152
  */
152
153
  allowSystemFontCapture?: boolean;
154
+ systemFontsByInnerName?: boolean;
153
155
  }
154
156
  export declare function injectDeterministicFontFaces(html: string, options?: InjectDeterministicFontFacesOptions): Promise<string>;
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { fontShorthandFamilies } from "@hyperframes/parsers/composition";
5
6
  import { FONT_ALIAS_MAP, resolveAliasDisplayName } from "./aliases.js";
7
+ import { extractSfntFace } from "./sfntNames.js";
6
8
  import { locateSystemFontVariants, SYSTEM_FONT_SIZE_LIMIT } from "./systemFontLocator.js";
7
9
  import { parseHTML } from "linkedom";
8
10
  import postcss from "postcss";
@@ -214,30 +216,6 @@ function collectCssCustomProperties(css, customProperties) {
214
216
  customProperties.set(decl.prop, decl.value);
215
217
  });
216
218
  }
217
- const FONT_SIZE_KEYWORD = /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|smaller|larger)$/i;
218
- const FONT_SIZE_TOKEN = /^(?:\d*\.?\d+[a-z%]+|(?:calc|min|max|clamp|var)\(.*\))(?:\/\S+)?$/i;
219
- function maskParens(value) {
220
- let depth = 0;
221
- return value.replace(/[\s\S]/g, (char) => {
222
- if (char === "(")
223
- depth += 1;
224
- const inside = depth > 0;
225
- if (char === ")")
226
- depth -= 1;
227
- return inside ? "_" : char;
228
- });
229
- }
230
- function fontShorthandFamilies(value) {
231
- for (const { 0: masked, index } of maskParens(value).matchAll(/\S+/g)) {
232
- const end = index + masked.length;
233
- const token = value.slice(index, end);
234
- const size = token.split("/")[0] ?? "";
235
- if (FONT_SIZE_KEYWORD.test(size) || FONT_SIZE_TOKEN.test(token)) {
236
- return value.slice(end).trim();
237
- }
238
- }
239
- return "";
240
- }
241
219
  function* iterateCssRootFontFamilyDeclarations(root) {
242
220
  const declarations = [];
243
221
  root.walkDecls((decl) => {
@@ -656,16 +634,17 @@ async function googleFamilyFaceRules(lookupFamily, emitFamily, optional, options
656
634
  return googleFaces.map((face) => buildFontFaceRule(emitFamily, face.dataUri, face.weight, face.style, face.unicodeRange));
657
635
  }
658
636
  // Path 3: locate font on the local filesystem, compress, and embed.
659
- async function systemFamilyFaceRules(lookupFamily, emitFamily, log) {
660
- const variants = locateSystemFontVariants(lookupFamily);
637
+ async function systemFamilyFaceRules(lookupFamily, emitFamily, log, byInnerName) {
638
+ const variants = locateSystemFontVariants(lookupFamily, { byInnerName });
661
639
  if (variants.length === 0)
662
640
  return null;
663
641
  const rules = [];
664
642
  let totalBytes = 0;
665
643
  for (const variant of variants) {
666
- const fontBuffer = readFileSync(variant.path);
644
+ const single = variant.face === undefined ? null : extractSfntFace(variant.path, variant.face);
645
+ const fontBuffer = single?.bytes ?? readFileSync(variant.path);
667
646
  totalBytes += fontBuffer.length;
668
- const dataUri = await fontToDataUri(fontBuffer, variant.format);
647
+ const dataUri = await fontToDataUri(fontBuffer, single?.format ?? variant.format);
669
648
  rules.push(buildFontFaceRule(emitFamily, dataUri, variant.weight, variant.style));
670
649
  }
671
650
  if (totalBytes > SYSTEM_FONT_SIZE_LIMIT) {
@@ -685,7 +664,7 @@ async function resolveFamilyFaceRules(normalizedFamily, lookupFamily, emitFamily
685
664
  }
686
665
  return ((await googleFamilyFaceRules(lookupFamily, emitFamily, optional, options, fontText)) ??
687
666
  (options.allowSystemFontCapture && !pageNamedThisFile(lookupFamily, options)
688
- ? await systemFamilyFaceRules(lookupFamily, emitFamily, options.log)
667
+ ? await systemFamilyFaceRules(lookupFamily, emitFamily, options.log, options.systemFontsByInnerName)
689
668
  : null));
690
669
  }
691
670
  // css2 repeats `family=Name:axes`; the legacy css API packs `family=Name:400|Other`.
@@ -817,6 +796,14 @@ function findDeclaredFamilyAlias(authoredFamily, declaredFamilies) {
817
796
  }
818
797
  return match;
819
798
  }
799
+ // Design exports name a family by class, `font-<Family>-<weight>`; serve all <Family> faces under that name.
800
+ const WEIGHTED_CLASS_FAMILY = /^font-(.+)-[1-9]00$/i;
801
+ async function resolveWeightedClassFamily(authoredFamily, optional, options, fontText) {
802
+ const family = WEIGHTED_CLASS_FAMILY.exec(authoredFamily)?.[1];
803
+ if (!family)
804
+ return null;
805
+ return resolveFamilyFaceRules(normalizeFamilyName(family), family, authoredFamily, optional, options, fontText);
806
+ }
820
807
  // Emits under the authored name instead of rewriting usages, as FONT_ALIASES does,
821
808
  // so script-assigned and var()-indirected usages match too.
822
809
  async function resolveDeclaredFamilyAlias(authoredFamily, declaredFamilies, optional, options, fontText) {
@@ -853,7 +840,8 @@ async function buildFontFaceCss(requestedFamilies, options, declaredFamilies, fo
853
840
  options.diag.beginFamily(!optional);
854
841
  const familyRules = (await resolveFamilyFaceRules(normalizedFamily, originalCaseFamily, originalCaseFamily, optional, options, fontText)) ??
855
842
  // Last, so a family that already resolves keeps exactly the faces it had.
856
- (await resolveDeclaredFamilyAlias(originalCaseFamily, declaredFamilies(), optional, options, fontText));
843
+ (await resolveDeclaredFamilyAlias(originalCaseFamily, declaredFamilies(), optional, options, fontText)) ??
844
+ (await resolveWeightedClassFamily(originalCaseFamily, optional, options, fontText));
857
845
  options.diag.endFamily(Boolean(familyRules));
858
846
  if (familyRules) {
859
847
  rules.push(...familyRules);
@@ -1534,6 +1522,7 @@ export async function injectDeterministicFontFaces(html, options = {}) {
1534
1522
  failClosedFontFetch,
1535
1523
  fetchImpl,
1536
1524
  allowSystemFontCapture,
1525
+ systemFontsByInnerName: options.systemFontsByInnerName === true,
1537
1526
  abortSignal: options.abortSignal,
1538
1527
  retryPolicy,
1539
1528
  retryDeadlineMs: Date.now() + retryPolicy.maxElapsedMs,
@@ -1541,6 +1530,8 @@ export async function injectDeterministicFontFaces(html, options = {}) {
1541
1530
  diag,
1542
1531
  };
1543
1532
  const existingFaces = extractExistingFontFaces(html);
1533
+ for (const family of options.projectFontFamilies ?? [])
1534
+ existingFaces.add(normalizeFamilyName(family));
1544
1535
  const requestedFamilies = extractRequestedFontFamilies(html);
1545
1536
  const pendingFamilies = new Map();
1546
1537
  for (const [normalizedFamily, requested] of requestedFamilies) {
@@ -0,0 +1,14 @@
1
+ /** Points the locator's per-user font folder, on every platform, at an empty folder and returns it. */
2
+ export declare function fixtureFontDir(): string;
3
+ interface FaceSpec {
4
+ families: string[];
5
+ weight?: number;
6
+ italic?: boolean;
7
+ flavor?: "ttf" | "otf";
8
+ }
9
+ export declare function makeSfnt(spec: FaceSpec): Buffer;
10
+ export declare function makeTtc(specs: FaceSpec[]): {
11
+ file: Buffer;
12
+ offsets: number[];
13
+ };
14
+ export {};
@@ -0,0 +1,103 @@
1
+ // Builds small font files in memory so tests need no binary fixtures.
2
+ import { mkdirSync, mkdtempSync } from "node:fs";
3
+ import { afterAll } from "vitest";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ /** Points the locator's per-user font folder, on every platform, at an empty folder and returns it. */
7
+ export function fixtureFontDir() {
8
+ const home = mkdtempSync(join(tmpdir(), "hf-home-"));
9
+ const set = {
10
+ HOME: home,
11
+ USERPROFILE: home,
12
+ WINDIR: join(home, "Win"),
13
+ LOCALAPPDATA: join(home, "Local"),
14
+ };
15
+ const before = Object.fromEntries(Object.keys(set).map((k) => [k, process.env[k]]));
16
+ Object.assign(process.env, set);
17
+ afterAll(() => {
18
+ for (const [k, v] of Object.entries(before)) {
19
+ if (v === undefined)
20
+ delete process.env[k];
21
+ else
22
+ process.env[k] = v;
23
+ }
24
+ });
25
+ const dir = process.platform === "win32"
26
+ ? join(home, "Local", "Microsoft", "Windows", "Fonts")
27
+ : process.platform === "darwin"
28
+ ? join(home, "Library", "Fonts")
29
+ : join(home, ".fonts");
30
+ mkdirSync(dir, { recursive: true });
31
+ return dir;
32
+ }
33
+ function nameTable(families) {
34
+ const strings = families.map((f) => Buffer.from(f, "utf16le").swap16());
35
+ const header = Buffer.alloc(6 + families.length * 12);
36
+ header.writeUInt16BE(0, 0);
37
+ header.writeUInt16BE(families.length, 2);
38
+ header.writeUInt16BE(header.length, 4);
39
+ let at = 0;
40
+ strings.forEach((s, i) => {
41
+ const r = 6 + i * 12;
42
+ header.writeUInt16BE(3, r);
43
+ header.writeUInt16BE(1, r + 2);
44
+ header.writeUInt16BE(0x409, r + 4);
45
+ header.writeUInt16BE(i === 0 ? 1 : 16, r + 6);
46
+ header.writeUInt16BE(s.length, r + 8);
47
+ header.writeUInt16BE(at, r + 10);
48
+ at += s.length;
49
+ });
50
+ return Buffer.concat([header, ...strings]);
51
+ }
52
+ function os2Table(weight, italic) {
53
+ const t = Buffer.alloc(78);
54
+ t.writeUInt16BE(weight, 4);
55
+ t.writeUInt16BE(italic ? 1 : 0, 62);
56
+ return t;
57
+ }
58
+ /** One face: its tables laid out from `base`, so a collection can place it anywhere in the file. */
59
+ function face(spec, base) {
60
+ const tables = [
61
+ ["OS/2", os2Table(spec.weight ?? 400, spec.italic ?? false)],
62
+ ["glyf", Buffer.from("glyph-data-" + spec.families[0])],
63
+ ["name", nameTable(spec.families)],
64
+ ];
65
+ const head = Buffer.alloc(12 + tables.length * 16);
66
+ head.write(spec.flavor === "otf" ? "OTTO" : "\0\x01\0\0", 0, "latin1");
67
+ head.writeUInt16BE(tables.length, 4);
68
+ let at = base + head.length;
69
+ const body = [];
70
+ tables.forEach(([tag, data], i) => {
71
+ const r = 12 + i * 16;
72
+ head.write(tag, r, "latin1");
73
+ head.writeUInt32BE(0, r + 4);
74
+ head.writeUInt32BE(at, r + 8);
75
+ head.writeUInt32BE(data.length, r + 12);
76
+ const padded = Buffer.alloc((data.length + 3) & ~3);
77
+ data.copy(padded);
78
+ body.push(padded);
79
+ at += padded.length;
80
+ });
81
+ return { head, body: Buffer.concat(body) };
82
+ }
83
+ export function makeSfnt(spec) {
84
+ const { head, body } = face(spec, 0);
85
+ return Buffer.concat([head, body]);
86
+ }
87
+ export function makeTtc(specs) {
88
+ let at = 12 + specs.length * 4;
89
+ const offsets = [];
90
+ const parts = [];
91
+ for (const spec of specs) {
92
+ const { head, body } = face(spec, at);
93
+ offsets.push(at);
94
+ parts.push(head, body);
95
+ at += head.length + body.length;
96
+ }
97
+ const header = Buffer.alloc(12 + specs.length * 4);
98
+ header.write("ttcf", 0, "latin1");
99
+ header.writeUInt32BE(0x00010000, 4);
100
+ header.writeUInt32BE(specs.length, 8);
101
+ offsets.forEach((o, i) => header.writeUInt32BE(o, 12 + i * 4));
102
+ return { file: Buffer.concat([header, ...parts]), offsets };
103
+ }
@@ -0,0 +1,14 @@
1
+ /** One face of a font file: where it starts and the family names written inside it. */
2
+ export interface SfntFace {
3
+ offset: number;
4
+ families: string[];
5
+ weight: string;
6
+ style: "normal" | "italic";
7
+ }
8
+ /** Every face of a font file, with the family names inside it; a collection (.ttc) yields one per member. */
9
+ export declare function readSfntFaces(path: string): SfntFace[];
10
+ /** One face of a font file as a standalone font, which a browser can load; a collection cannot be loaded whole. */
11
+ export declare function extractSfntFace(path: string, faceOffset: number): {
12
+ bytes: Buffer;
13
+ format: "ttf" | "otf";
14
+ };
@@ -0,0 +1,115 @@
1
+ import { closeSync, openSync, readFileSync, readSync } from "node:fs";
2
+ const NAME_FAMILY_IDS = new Set([1, 16, 21]);
3
+ const MAX_FACES = 64;
4
+ function readAt(fd, position, length) {
5
+ const buf = Buffer.alloc(length);
6
+ const read = readSync(fd, buf, 0, length, position);
7
+ return read === length ? buf : buf.subarray(0, read);
8
+ }
9
+ function tableDirectory(fd, faceOffset) {
10
+ const header = readAt(fd, faceOffset, 12);
11
+ const count = header.length === 12 ? header.readUInt16BE(4) : 0;
12
+ const dir = readAt(fd, faceOffset + 12, count * 16);
13
+ const tables = new Map();
14
+ for (let i = 0; i + 16 <= dir.length; i += 16) {
15
+ tables.set(dir.toString("latin1", i, i + 4), {
16
+ offset: dir.readUInt32BE(i + 8),
17
+ length: dir.readUInt32BE(i + 12),
18
+ });
19
+ }
20
+ return tables;
21
+ }
22
+ function familiesIn(name) {
23
+ if (name.length < 6)
24
+ return [];
25
+ const count = name.readUInt16BE(2);
26
+ const strings = name.readUInt16BE(4);
27
+ const found = new Set();
28
+ for (let i = 0; i < count && 6 + i * 12 + 12 <= name.length; i++) {
29
+ const at = 6 + i * 12;
30
+ const platform = name.readUInt16BE(at);
31
+ const nameId = name.readUInt16BE(at + 6);
32
+ if (!NAME_FAMILY_IDS.has(nameId))
33
+ continue;
34
+ const length = name.readUInt16BE(at + 8);
35
+ const raw = name.subarray(strings + name.readUInt16BE(at + 10), strings + name.readUInt16BE(at + 10) + length);
36
+ // Windows (3) and Unicode (0) names are UTF-16BE; Macintosh (1) names are single-byte.
37
+ const text = platform === 1 ? raw.toString("latin1") : Buffer.from(raw).swap16().toString("utf16le");
38
+ if (text.trim())
39
+ found.add(text.trim());
40
+ }
41
+ return [...found];
42
+ }
43
+ function readFace(fd, offset) {
44
+ const tables = tableDirectory(fd, offset);
45
+ const nameTable = tables.get("name");
46
+ if (!nameTable)
47
+ return null;
48
+ const families = familiesIn(readAt(fd, nameTable.offset, Math.min(nameTable.length, 1 << 20)));
49
+ const os2Table = tables.get("OS/2");
50
+ const os2 = os2Table
51
+ ? readAt(fd, os2Table.offset, Math.min(os2Table.length, 64))
52
+ : Buffer.alloc(0);
53
+ const weightClass = os2.length >= 6 ? os2.readUInt16BE(4) : 400;
54
+ const italic = os2.length >= 64 && (os2.readUInt16BE(62) & 1) === 1;
55
+ return {
56
+ offset,
57
+ families,
58
+ weight: String(weightClass >= 100 && weightClass <= 900 ? weightClass : 400),
59
+ style: italic ? "italic" : "normal",
60
+ };
61
+ }
62
+ /** Every face of a font file, with the family names inside it; a collection (.ttc) yields one per member. */
63
+ export function readSfntFaces(path) {
64
+ let fd;
65
+ try {
66
+ fd = openSync(path, "r");
67
+ const head = readAt(fd, 0, 12);
68
+ if (head.length < 12)
69
+ return [];
70
+ const collection = head.toString("latin1", 0, 4) === "ttcf";
71
+ const table = collection ? readAt(fd, 12, Math.min(head.readUInt32BE(8), MAX_FACES) * 4) : head;
72
+ const offsets = collection
73
+ ? Array.from({ length: Math.floor(table.length / 4) }, (_, i) => table.readUInt32BE(i * 4))
74
+ : [0];
75
+ return offsets.map((offset) => readFace(fd, offset)).filter((f) => f !== null);
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ finally {
81
+ if (fd !== undefined)
82
+ closeSync(fd);
83
+ }
84
+ }
85
+ /** One face of a font file as a standalone font, which a browser can load; a collection cannot be loaded whole. */
86
+ export function extractSfntFace(path, faceOffset) {
87
+ const file = readFileSync(path);
88
+ const count = file.readUInt16BE(faceOffset + 4);
89
+ const records = Array.from({ length: count }, (_, i) => {
90
+ const at = faceOffset + 12 + i * 16;
91
+ return {
92
+ tag: file.subarray(at, at + 4),
93
+ checksum: file.readUInt32BE(at + 4),
94
+ data: file.subarray(file.readUInt32BE(at + 8), file.readUInt32BE(at + 8) + file.readUInt32BE(at + 12)),
95
+ };
96
+ });
97
+ const dirEnd = 12 + count * 16;
98
+ const parts = [Buffer.alloc(dirEnd)];
99
+ let next = dirEnd;
100
+ const out = parts[0];
101
+ file.copy(out, 0, faceOffset, faceOffset + 12);
102
+ records.forEach((record, i) => {
103
+ const at = 12 + i * 16;
104
+ record.tag.copy(out, at);
105
+ out.writeUInt32BE(record.checksum, at + 4);
106
+ out.writeUInt32BE(next, at + 8);
107
+ out.writeUInt32BE(record.data.length, at + 12);
108
+ const padded = Buffer.alloc((record.data.length + 3) & ~3);
109
+ record.data.copy(padded);
110
+ parts.push(padded);
111
+ next += padded.length;
112
+ });
113
+ const bytes = Buffer.concat(parts);
114
+ return { bytes, format: bytes.toString("latin1", 0, 4) === "OTTO" ? "otf" : "ttf" };
115
+ }
@@ -17,11 +17,14 @@ export interface FontFileEntry {
17
17
  * path, filename, and derived family name. Shared by the font listing route
18
18
  * and the per-family locator to avoid duplicating the directory scan loop.
19
19
  */
20
- export declare function collectFontFileEntries(dir: string, depth?: number): FontFileEntry[];
20
+ export declare function collectFontFileEntries(dir: string, depth?: number, maxDepth?: number): FontFileEntry[];
21
21
  export declare function locateSystemFont(family: string): LocatedFont | null;
22
22
  export interface LocatedFontVariant extends LocatedFont {
23
23
  weight: string;
24
24
  style: "normal" | "italic";
25
+ face?: number;
25
26
  }
26
- export declare function locateSystemFontVariants(family: string): LocatedFontVariant[];
27
+ export declare function locateSystemFontVariants(family: string, options?: {
28
+ byInnerName?: boolean;
29
+ }): LocatedFontVariant[];
27
30
  export declare function clearSystemFontCache(): void;
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
+ import { readSfntFaces } from "./sfntNames.js";
5
6
  export const SYSTEM_FONT_SIZE_LIMIT = 5 * 1024 * 1024;
6
7
  const FC_MATCH_TIMEOUT_MS = 3000;
7
8
  export const FONT_EXT_RE = /\.(otf|ttf|ttc|woff2?)$/i;
@@ -124,6 +125,7 @@ export function fontDirectories() {
124
125
  "/Library/Fonts",
125
126
  "/System/Library/Fonts",
126
127
  "/System/Library/Fonts/Supplemental",
128
+ "/System/Library/AssetsV2",
127
129
  ];
128
130
  }
129
131
  if (platform() === "win32") {
@@ -144,15 +146,15 @@ export function fontDirectories() {
144
146
  * path, filename, and derived family name. Shared by the font listing route
145
147
  * and the per-family locator to avoid duplicating the directory scan loop.
146
148
  */
147
- export function collectFontFileEntries(dir, depth = 0) {
148
- if (!existsSync(dir) || depth > 2)
149
+ export function collectFontFileEntries(dir, depth = 0, maxDepth = 2) {
150
+ if (!existsSync(dir) || depth > maxDepth)
149
151
  return [];
150
152
  const entries = [];
151
153
  try {
152
154
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
153
155
  const fullPath = join(dir, entry.name);
154
156
  if (entry.isDirectory()) {
155
- entries.push(...collectFontFileEntries(fullPath, depth + 1));
157
+ entries.push(...collectFontFileEntries(fullPath, depth + 1, maxDepth));
156
158
  continue;
157
159
  }
158
160
  if (!FONT_EXT_RE.test(entry.name))
@@ -262,7 +264,7 @@ function inferWeightAndStyle(fileName) {
262
264
  }
263
265
  return { weight: "400", style };
264
266
  }
265
- export function locateSystemFontVariants(family) {
267
+ export function locateSystemFontVariants(family, options = {}) {
266
268
  const normalized = normalizeName(family);
267
269
  if (!normalized)
268
270
  return [];
@@ -275,8 +277,47 @@ export function locateSystemFontVariants(family) {
275
277
  const { weight, style } = inferWeightAndStyle(c.path);
276
278
  variants.push({ path: c.path, format: c.format, weight, style });
277
279
  }
280
+ if (variants.length === 0 && options.byInnerName) {
281
+ return regularAndBold(dedupeVariants(variantsByInnerName(normalized)));
282
+ }
278
283
  return dedupeVariants(variants);
279
284
  }
285
+ const INNER_NAME_MAX_DEPTH = 4;
286
+ let innerNameIndex = null;
287
+ function innerNameVariants(entry) {
288
+ const ext = entry.fileName.match(FONT_EXT_RE)?.[1]?.toLowerCase() ?? "";
289
+ if (ext !== "ttf" && ext !== "otf" && ext !== "ttc")
290
+ return [];
291
+ const collection = ext === "ttc";
292
+ return readSfntFaces(entry.path).flatMap((face) => face.families.map((family) => [
293
+ normalizeName(family),
294
+ {
295
+ path: entry.path,
296
+ format: collection ? "ttc" : extensionToFormat(ext),
297
+ weight: face.weight,
298
+ style: face.style,
299
+ ...(collection && { face: face.offset }),
300
+ },
301
+ ]));
302
+ }
303
+ function buildInnerNameIndex() {
304
+ const index = new Map();
305
+ const entries = fontDirectories().flatMap((dir) => collectFontFileEntries(dir, 0, INNER_NAME_MAX_DEPTH));
306
+ for (const [family, variant] of entries.flatMap(innerNameVariants)) {
307
+ index.set(family, [...(index.get(family) ?? []), variant]);
308
+ }
309
+ return index;
310
+ }
311
+ function regularAndBold(variants) {
312
+ const nearest = (target) => variants.reduce((best, v) => !best || Math.abs(Number(v.weight) - target) < Math.abs(Number(best.weight) - target)
313
+ ? v
314
+ : best, undefined);
315
+ return [...new Set([nearest(400), nearest(700)])].filter((v) => !!v);
316
+ }
317
+ function variantsByInnerName(normalized) {
318
+ innerNameIndex ??= buildInnerNameIndex();
319
+ return innerNameIndex.get(normalized) ?? [];
320
+ }
280
321
  function dedupeVariants(variants) {
281
322
  const seen = new Map();
282
323
  for (const v of variants) {
@@ -288,5 +329,6 @@ function dedupeVariants(variants) {
288
329
  }
289
330
  export function clearSystemFontCache() {
290
331
  cache.clear();
332
+ innerNameIndex = null;
291
333
  allowedDirsCache = null;
292
334
  }