@navaramap/font 0.0.2 → 0.0.4

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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, afterEach, vi } from "vitest";
1
+ import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
2
2
 
3
3
  import {
4
4
  fetchFontFamilyFromCss,
@@ -6,182 +6,53 @@ import {
6
6
  parseFontFamilyFromCss,
7
7
  } from "./cssFontFamily";
8
8
 
9
- // Trimmed-down Google Fonts CSS API response: two subsets of one family plus
10
- // a second family, each block with its own unicode-range.
11
- const GOOGLE_FONTS_CSS = `
12
- /* vietnamese */
13
- @font-face {
14
- font-family: 'Archivo';
15
- font-style: normal;
16
- font-weight: 800;
17
- font-stretch: 125%;
18
- font-display: swap;
19
- src: url(https://fonts.gstatic.com/s/archivo/v25/viet.woff2) format('woff2');
20
- unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
21
- }
22
- /* latin */
23
- @font-face {
24
- font-family: 'Archivo';
25
- font-style: normal;
26
- font-weight: 800;
27
- font-stretch: 125%;
28
- font-display: swap;
29
- src: url(https://fonts.gstatic.com/s/archivo/v25/latin.woff2) format('woff2');
30
- unicode-range: U+0000-00FF, U+2000-206F;
31
- }
32
- /* [58] */
33
- @font-face {
34
- font-family: 'Noto Sans JP';
35
- font-style: normal;
36
- font-weight: 800;
37
- font-display: swap;
38
- src: url(https://fonts.gstatic.com/s/notosansjp/v56/slice.58.woff2) format('woff2');
39
- unicode-range: U+4E00-4EFF;
40
- }
41
- `;
9
+ // The @font-face / unicode-range parsing itself lives in Rust (navara_wasm_api)
10
+ // and is covered by that crate's tests. Here the WASM module is mocked so these
11
+ // tests stay WASM-free and focus on the TypeScript orchestration: lazy init,
12
+ // delegation, and the fetch handling in `fetchFontFamilyFromCss`.
13
+ const { initApi, wasmParseCssUnicodeRange, wasmParseFontFamilyFromCss } =
14
+ vi.hoisted(() => ({
15
+ initApi: vi.fn(async () => undefined),
16
+ wasmParseCssUnicodeRange: vi.fn(),
17
+ wasmParseFontFamilyFromCss: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("@navaramap/engine-api", () => ({
21
+ default: initApi,
22
+ parseCssUnicodeRange: wasmParseCssUnicodeRange,
23
+ parseFontFamilyFromCss: wasmParseFontFamilyFromCss,
24
+ }));
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
42
29
 
43
30
  describe("parseCssUnicodeRange", () => {
44
- it("parses single codepoints, intervals, and wildcards", () => {
45
- expect(parseCssUnicodeRange("U+26")).toEqual([{ from: 0x26, to: 0x26 }]);
46
- expect(parseCssUnicodeRange("U+0102-0103, U+20AB")).toEqual([
47
- { from: 0x102, to: 0x103 },
48
- { from: 0x20ab, to: 0x20ab },
49
- ]);
50
- expect(parseCssUnicodeRange("U+4??")).toEqual([{ from: 0x400, to: 0x4ff }]);
51
- expect(parseCssUnicodeRange("u+1ea0-1ef9")).toEqual([
52
- { from: 0x1ea0, to: 0x1ef9 },
53
- ]);
54
- });
55
-
56
- it("rejects malformed tokens", () => {
57
- expect(() => parseCssUnicodeRange("0102-0103")).toThrow();
58
- expect(() => parseCssUnicodeRange("U+GGGG")).toThrow();
59
- expect(() => parseCssUnicodeRange("U+4??-4FF")).toThrow();
60
- });
61
-
62
- it("rejects ranges whose end precedes their start", () => {
63
- expect(() => parseCssUnicodeRange("U+200-100")).toThrow();
64
- });
65
-
66
- it("rejects codepoints beyond U+10FFFF", () => {
67
- expect(() => parseCssUnicodeRange("U+110000")).toThrow();
68
- expect(() => parseCssUnicodeRange("U+10FFFF-110000")).toThrow();
69
- expect(() => parseCssUnicodeRange("U+??????")).toThrow();
70
- expect(parseCssUnicodeRange("U+10FFFF")).toEqual([
71
- { from: 0x10ffff, to: 0x10ffff },
72
- ]);
31
+ it("initializes the WASM module and delegates", async () => {
32
+ const ranges = [{ from: 0x26, to: 0x26 }];
33
+ wasmParseCssUnicodeRange.mockReturnValue(ranges);
34
+ // First wrapper call of the module: init runs here. Init is memoized
35
+ // (see `ensureApi`), so later tests share it and don't re-invoke it —
36
+ // hence the init assertion lives only in this first test.
37
+ const result = await parseCssUnicodeRange("U+26");
38
+ expect(initApi).toHaveBeenCalled();
39
+ expect(wasmParseCssUnicodeRange).toHaveBeenCalledWith("U+26");
40
+ expect(result).toBe(ranges);
73
41
  });
74
42
  });
75
43
 
76
44
  describe("parseFontFamilyFromCss", () => {
77
- it("builds one face per @font-face block, in reverse stylesheet order", () => {
78
- const family = parseFontFamilyFromCss("labels", GOOGLE_FONTS_CSS);
79
- expect(family.family).toBe("labels");
80
- expect(family.faces).toHaveLength(3);
81
- // CSS gives later @font-face rules precedence on range overlap, so
82
- // blocks come out reversed: Archivo's latin block ends up before its
83
- // latin-ext/vietnamese ones. Use the fontFamily array to order families.
84
- expect(family.faces[0].url).toContain("notosansjp");
85
- expect(family.faces[1]).toEqual({
86
- url: "https://fonts.gstatic.com/s/archivo/v25/latin.woff2",
87
- unicodeRanges: [
88
- { from: 0x0, to: 0xff },
89
- { from: 0x2000, to: 0x206f },
90
- ],
91
- });
92
- expect(family.faces[2].url).toContain("viet");
93
- });
94
-
95
- it("routes overlap-declared codepoints to the later (more specific) block", () => {
96
- // Mimics Google Fonts: latin-ext declares a broad range including
97
- // U+0131, but the glyph only exists in the latin file declared after it.
98
- const css = `
99
- @font-face { font-family: A; font-style: normal; font-weight: 800; src: url(latin-ext.woff2); unicode-range: U+0100-024F; }
100
- @font-face { font-family: A; font-style: normal; font-weight: 800; src: url(latin.woff2); unicode-range: U+0000-00FF, U+0131; }
101
- `;
102
- const family = parseFontFamilyFromCss("labels", css);
103
- expect(family.faces.map((f) => f.url)).toEqual([
104
- "latin.woff2",
105
- "latin-ext.woff2",
106
- ]);
107
- });
108
-
109
- it("filters blocks by font-family name", () => {
110
- const family = parseFontFamilyFromCss("labels", GOOGLE_FONTS_CSS, {
111
- fontFamily: "Noto Sans JP",
112
- });
113
- expect(family.faces).toHaveLength(1);
114
- expect(family.faces[0].url).toContain("notosansjp");
115
- });
116
-
117
- it("orders faces by fontFamily array position, not stylesheet order", () => {
118
- // Google Fonts sorts blocks alphabetically; the caller's array wins.
119
- const family = parseFontFamilyFromCss("labels", GOOGLE_FONTS_CSS, {
120
- fontFamily: ["Noto Sans JP", "Archivo"],
121
- });
122
- expect(family.faces.map((f) => f.url)).toEqual([
123
- "https://fonts.gstatic.com/s/notosansjp/v56/slice.58.woff2",
124
- "https://fonts.gstatic.com/s/archivo/v25/latin.woff2",
125
- "https://fonts.gstatic.com/s/archivo/v25/viet.woff2",
126
- ]);
127
- });
128
-
129
- it("filters blocks by weight and style", () => {
130
- const css = `
131
- @font-face { font-family: A; font-style: italic; font-weight: 400; src: url(a-italic.woff2); }
132
- @font-face { font-family: A; font-style: normal; font-weight: 700; src: url(a-bold.woff2); }
133
- `;
134
- const family = parseFontFamilyFromCss("labels", css, {
135
- fontStyle: "normal",
136
- fontWeight: 700,
137
- });
138
- expect(family.faces).toHaveLength(1);
139
- expect(family.faces[0].url).toBe("a-bold.woff2");
140
- });
141
-
142
- it("normalizes whitespace and case in weight/style filters", () => {
143
- const css = `
144
- @font-face { font-family: A; font-style: normal; font-weight: 100 900; src: url(a-var.woff2); }
145
- `;
146
- const family = parseFontFamilyFromCss("labels", css, {
147
- fontStyle: "Normal",
148
- fontWeight: " 100 900 ",
149
- });
150
- expect(family.faces).toHaveLength(1);
151
- expect(family.faces[0].url).toBe("a-var.woff2");
152
- });
153
-
154
- it("covers all codepoints when a block has no unicode-range", () => {
155
- const css = `@font-face { font-family: A; src: url(a.woff2); }`;
156
- const family = parseFontFamilyFromCss("labels", css);
157
- expect(family.faces[0].unicodeRanges).toEqual([{ from: 0, to: 0x10ffff }]);
158
- });
159
-
160
- it("resolves relative src URLs against baseUrl", () => {
161
- const css = `@font-face { font-family: A; src: url(./fonts/a.woff2); unicode-range: U+0-7F; }`;
162
- const family = parseFontFamilyFromCss("labels", css, {
163
- baseUrl: "https://example.com/styles/fonts.css",
164
- });
165
- expect(family.faces[0].url).toBe(
166
- "https://example.com/styles/fonts/a.woff2",
45
+ it("delegates to the WASM parser with options", async () => {
46
+ const family = { family: "labels", faces: [] };
47
+ wasmParseFontFamilyFromCss.mockReturnValue(family);
48
+ const options = { fontFamily: "Archivo", baseUrl: "https://a.test/" };
49
+ const result = await parseFontFamilyFromCss("labels", "css", options);
50
+ expect(wasmParseFontFamilyFromCss).toHaveBeenCalledWith(
51
+ "labels",
52
+ "css",
53
+ options,
167
54
  );
168
- });
169
-
170
- it("prefers the woff2 source when src lists several formats", () => {
171
- const css = `@font-face {
172
- font-family: A;
173
- src: url("a.eot") format("embedded-opentype"), url('a.woff2') format('woff2'), url(a.ttf) format(truetype);
174
- }`;
175
- const family = parseFontFamilyFromCss("labels", css);
176
- expect(family.faces[0].url).toBe("a.woff2");
177
- });
178
-
179
- it("throws when no block matches", () => {
180
- expect(() =>
181
- parseFontFamilyFromCss("labels", GOOGLE_FONTS_CSS, {
182
- fontFamily: "Nope",
183
- }),
184
- ).toThrow(/no @font-face rules matched/);
55
+ expect(result).toBe(family);
185
56
  });
186
57
  });
187
58
 
@@ -192,8 +63,8 @@ describe("fetchFontFamilyFromCss", () => {
192
63
 
193
64
  it("fetches stylesheets and concatenates faces in URL order", async () => {
194
65
  const cssByUrl: Record<string, string> = {
195
- "https://a.test/fonts.css": `@font-face { font-family: A; src: url(/a.woff2); unicode-range: U+0-7F; }`,
196
- "https://b.test/fonts.css": `@font-face { font-family: B; src: url(/b.woff2); unicode-range: U+4E00-9FFF; }`,
66
+ "https://a.test/fonts.css": "@font-face-a",
67
+ "https://b.test/fonts.css": "@font-face-b",
197
68
  };
198
69
  vi.stubGlobal(
199
70
  "fetch",
@@ -203,16 +74,53 @@ describe("fetchFontFamilyFromCss", () => {
203
74
  text: async () => cssByUrl[url],
204
75
  })),
205
76
  );
77
+ // Echo the resolved baseUrl back as the face URL so we can assert both the
78
+ // per-stylesheet baseUrl threading and the URL-order concatenation.
79
+ wasmParseFontFamilyFromCss.mockImplementation(
80
+ (family: string, _cssText: string, options: { baseUrl?: string }) => ({
81
+ family,
82
+ faces: [{ url: options.baseUrl, unicodeRanges: [] }],
83
+ }),
84
+ );
85
+
206
86
  const family = await fetchFontFamilyFromCss("labels", [
207
87
  "https://a.test/fonts.css",
208
88
  "https://b.test/fonts.css",
209
89
  ]);
90
+
91
+ expect(family.family).toBe("labels");
210
92
  expect(family.faces.map((f) => f.url)).toEqual([
211
- "https://a.test/a.woff2",
212
- "https://b.test/b.woff2",
93
+ "https://a.test/fonts.css",
94
+ "https://b.test/fonts.css",
213
95
  ]);
214
96
  });
215
97
 
98
+ it("passes requestInit to fetch and filter options (minus requestInit) to the parser", async () => {
99
+ const requestInit = { credentials: "include" as const };
100
+ const fetchMock = vi.fn(async (url: string) => ({
101
+ ok: true,
102
+ url,
103
+ text: async () => "css",
104
+ }));
105
+ vi.stubGlobal("fetch", fetchMock);
106
+ wasmParseFontFamilyFromCss.mockReturnValue({ family: "labels", faces: [] });
107
+
108
+ await fetchFontFamilyFromCss("labels", "https://a.test/fonts.css", {
109
+ requestInit,
110
+ fontFamily: "Archivo",
111
+ });
112
+
113
+ expect(fetchMock).toHaveBeenCalledWith(
114
+ "https://a.test/fonts.css",
115
+ requestInit,
116
+ );
117
+ // requestInit must not leak into the parser options.
118
+ expect(wasmParseFontFamilyFromCss).toHaveBeenCalledWith("labels", "css", {
119
+ fontFamily: "Archivo",
120
+ baseUrl: "https://a.test/fonts.css",
121
+ });
122
+ });
123
+
216
124
  it("throws on a failed response", async () => {
217
125
  vi.stubGlobal(
218
126
  "fetch",
@@ -1,4 +1,9 @@
1
- import type { FontFace, FontFamily, UnicodeRange } from "./types";
1
+ import initApi, {
2
+ parseCssUnicodeRange as wasmParseCssUnicodeRange,
3
+ parseFontFamilyFromCss as wasmParseFontFamilyFromCss,
4
+ } from "@navaramap/engine-api";
5
+
6
+ import type { FontFamily, UnicodeRange } from "./types";
2
7
 
3
8
  /** Filters selecting which `@font-face` blocks of a stylesheet to include. */
4
9
  export type CssFontFaceFilter = {
@@ -31,136 +36,32 @@ export type FetchCssFontFamilyOptions = CssFontFaceFilter & {
31
36
  requestInit?: RequestInit;
32
37
  };
33
38
 
39
+ // The @font-face / unicode-range parsing lives in Rust (`navara_wasm_api`,
40
+ // published as `@navaramap/engine-api`); these functions are thin wrappers that
41
+ // initialize that WASM module and delegate. Parsing correctness is covered by
42
+ // the crate's Rust tests; the TS tests here cover the fetch orchestration.
43
+ //
44
+ // The WASM module is instantiated lazily and at most once — the promise is
45
+ // memoized so concurrent callers share a single init (`__wbg_init` itself is
46
+ // idempotent once resolved, but memoizing avoids a redundant instantiate while
47
+ // the first is still in flight).
48
+ let apiReady: Promise<unknown> | undefined;
49
+ function ensureApi(): Promise<unknown> {
50
+ return (apiReady ??= initApi());
51
+ }
52
+
34
53
  /**
35
54
  * Parse a CSS `unicode-range` descriptor value (e.g. `"U+0-7F, U+131, U+4??"`)
36
55
  * into inclusive codepoint ranges. Supports single codepoints (`U+26`),
37
56
  * intervals (`U+0102-0103`) and wildcards (`U+4??`).
57
+ *
58
+ * Backed by WASM, so it resolves once `@navaramap/engine-api` has initialized.
38
59
  */
39
- export function parseCssUnicodeRange(value: string): UnicodeRange[] {
40
- const ranges: UnicodeRange[] = [];
41
- for (const token of value.split(",")) {
42
- const trimmed = token.trim();
43
- if (!trimmed) continue;
44
- const match = /^u\+([0-9a-f?]{1,6})(?:-([0-9a-f]{1,6}))?$/i.exec(trimmed);
45
- if (!match) {
46
- throw new Error(`Invalid unicode-range token: "${trimmed}"`);
47
- }
48
- const [, start, end] = match;
49
- let range: UnicodeRange;
50
- if (start.includes("?")) {
51
- // Wildcard form: `U+4??` means U+400-4FF. A wildcard cannot be
52
- // combined with an explicit end.
53
- if (end !== undefined) {
54
- throw new Error(`Invalid unicode-range token: "${trimmed}"`);
55
- }
56
- range = {
57
- from: parseInt(start.replace(/\?/g, "0"), 16),
58
- to: parseInt(start.replace(/\?/g, "f"), 16),
59
- };
60
- } else {
61
- const from = parseInt(start, 16);
62
- range = { from, to: end !== undefined ? parseInt(end, 16) : from };
63
- }
64
- if (range.from > range.to || range.to > 0x10ffff) {
65
- throw new Error(`Invalid unicode-range token: "${trimmed}"`);
66
- }
67
- ranges.push(range);
68
- }
69
- return ranges;
70
- }
71
-
72
- /** A single parsed `@font-face` block's descriptors. */
73
- type CssFontFaceBlock = {
74
- fontFamily?: string;
75
- fontStyle?: string;
76
- fontWeight?: string;
77
- src?: string;
78
- unicodeRange?: string;
79
- };
80
-
81
- /** Covers every codepoint — used when a block declares no `unicode-range`. */
82
- const FULL_RANGE: UnicodeRange[] = [{ from: 0, to: 0x10ffff }];
83
-
84
- function stripQuotes(value: string): string {
85
- const first = value[0];
86
- if ((first === '"' || first === "'") && value.endsWith(first)) {
87
- return value.slice(1, -1);
88
- }
89
- return value;
90
- }
91
-
92
- /** Extract the font URL from a `src` descriptor, preferring woff2 sources. */
93
- function parseSrcUrl(src: string): string | undefined {
94
- const sources = [
95
- ...src.matchAll(
96
- /url\(\s*("[^"]*"|'[^']*'|[^)]*?)\s*\)(\s*format\(\s*("[^"]*"|'[^']*'|[^)]*?)\s*\))?/gi,
97
- ),
98
- ];
99
- if (sources.length === 0) return undefined;
100
- const woff2 = sources.find(
101
- (m) => m[3] && stripQuotes(m[3]).toLowerCase() === "woff2",
102
- );
103
- return stripQuotes((woff2 ?? sources[0])[1].trim());
104
- }
105
-
106
- function parseFontFaceBlocks(cssText: string): CssFontFaceBlock[] {
107
- const withoutComments = cssText.replace(/\/\*[\s\S]*?\*\//g, "");
108
- const blocks: CssFontFaceBlock[] = [];
109
- for (const [, body] of withoutComments.matchAll(
110
- /@font-face\s*\{([^}]*)\}/gi,
111
- )) {
112
- const block: CssFontFaceBlock = {};
113
- for (const declaration of body.split(";")) {
114
- const colon = declaration.indexOf(":");
115
- if (colon < 0) continue;
116
- const property = declaration.slice(0, colon).trim().toLowerCase();
117
- const value = declaration.slice(colon + 1).trim();
118
- if (property === "font-family") block.fontFamily = stripQuotes(value);
119
- else if (property === "font-style") block.fontStyle = value;
120
- else if (property === "font-weight") block.fontWeight = value;
121
- else if (property === "src") block.src = value;
122
- else if (property === "unicode-range") block.unicodeRange = value;
123
- }
124
- blocks.push(block);
125
- }
126
- return blocks;
127
- }
128
-
129
- /**
130
- * Normalize a CSS descriptor value for comparison: CSS keywords are
131
- * case-insensitive, and whitespace (e.g. in variable-font ranges like
132
- * `"100 900"`) is insignificant beyond separating tokens.
133
- */
134
- function normalizeCssValue(value: string): string {
135
- return value.trim().replace(/\s+/g, " ").toLowerCase();
136
- }
137
-
138
- function matchesFilter(
139
- block: CssFontFaceBlock,
140
- filter: CssFontFaceFilter,
141
- ): boolean {
142
- if (filter.fontFamily !== undefined) {
143
- const wanted = Array.isArray(filter.fontFamily)
144
- ? filter.fontFamily
145
- : [filter.fontFamily];
146
- const name = block.fontFamily?.toLowerCase();
147
- if (!wanted.some((w) => w.toLowerCase() === name)) return false;
148
- }
149
- if (
150
- filter.fontWeight !== undefined &&
151
- normalizeCssValue(block.fontWeight ?? "") !==
152
- normalizeCssValue(String(filter.fontWeight))
153
- ) {
154
- return false;
155
- }
156
- if (
157
- filter.fontStyle !== undefined &&
158
- normalizeCssValue(block.fontStyle ?? "") !==
159
- normalizeCssValue(filter.fontStyle)
160
- ) {
161
- return false;
162
- }
163
- return true;
60
+ export async function parseCssUnicodeRange(
61
+ value: string,
62
+ ): Promise<UnicodeRange[]> {
63
+ await ensureApi();
64
+ return wasmParseCssUnicodeRange(value) as UnicodeRange[];
164
65
  }
165
66
 
166
67
  /**
@@ -178,57 +79,20 @@ function matchesFilter(
178
79
  * codepoints no face covers — same semantics as a hand-written
179
80
  * {@link FontFamily}.
180
81
  *
82
+ * The parsing runs in WASM (`@navaramap/engine-api`), so this resolves once
83
+ * that module has initialized.
84
+ *
181
85
  * @param family Family name to register the faces under.
182
86
  * @param cssText Stylesheet text containing `@font-face` rules.
183
87
  * @param options Optional block filters and a `baseUrl` for relative URLs.
184
88
  */
185
- export function parseFontFamilyFromCss(
89
+ export async function parseFontFamilyFromCss(
186
90
  family: string,
187
91
  cssText: string,
188
92
  options: ParseCssFontFamilyOptions = {},
189
- ): FontFamily {
190
- const faces: { face: FontFace; cssFamily: string }[] = [];
191
- for (const block of parseFontFaceBlocks(cssText)) {
192
- if (!matchesFilter(block, options)) continue;
193
- if (block.src === undefined) continue;
194
- const rawUrl = parseSrcUrl(block.src);
195
- if (rawUrl === undefined) continue;
196
- const url = options.baseUrl
197
- ? new URL(rawUrl, options.baseUrl).toString()
198
- : rawUrl;
199
- const unicodeRanges = block.unicodeRange
200
- ? parseCssUnicodeRange(block.unicodeRange)
201
- : FULL_RANGE;
202
- faces.push({
203
- face: { url, unicodeRanges },
204
- cssFamily: block.fontFamily?.toLowerCase() ?? "",
205
- });
206
- }
207
- if (faces.length === 0) {
208
- throw new Error(
209
- `parseFontFamilyFromCss: no @font-face rules matched for family "${family}"`,
210
- );
211
- }
212
- // CSS gives LATER @font-face rules precedence when their unicode-ranges
213
- // overlap, so reverse the block order. Stylesheets rely on this: e.g.
214
- // Google Fonts' `latin-ext` block declares broad ranges (U+0100-024F)
215
- // that include codepoints only present in the `latin` file declared
216
- // after it — without the reversal those codepoints would route to a face
217
- // missing the glyph and shape as tofu.
218
- faces.reverse();
219
- if (Array.isArray(options.fontFamily)) {
220
- // Some stylesheets (e.g. the Google Fonts CSS API) order @font-face
221
- // blocks alphabetically, so restore the priority the caller asked for.
222
- const priority = options.fontFamily.map((name) => name.toLowerCase());
223
- // Families absent from the list rank last, not first (indexOf's -1
224
- // would otherwise give them top priority).
225
- const rank = (cssFamily: string) => {
226
- const index = priority.indexOf(cssFamily);
227
- return index === -1 ? priority.length : index;
228
- };
229
- faces.sort((a, b) => rank(a.cssFamily) - rank(b.cssFamily));
230
- }
231
- return { family, faces: faces.map(({ face }) => face) };
93
+ ): Promise<FontFamily> {
94
+ await ensureApi();
95
+ return wasmParseFontFamilyFromCss(family, cssText, options) as FontFamily;
232
96
  }
233
97
 
234
98
  /**
@@ -263,7 +127,7 @@ export async function fetchFontFamilyFromCss(
263
127
  ): Promise<FontFamily> {
264
128
  const urls = Array.isArray(cssUrl) ? cssUrl : [cssUrl];
265
129
  const { requestInit, ...filter } = options;
266
- const faces: FontFace[] = [];
130
+ const faces: FontFamily["faces"] = [];
267
131
  const cssTexts = await Promise.all(
268
132
  urls.map(async (url) => {
269
133
  const response = await fetch(url, requestInit);
@@ -278,9 +142,11 @@ export async function fetchFontFamilyFromCss(
278
142
  }),
279
143
  );
280
144
  for (const { text, baseUrl } of cssTexts) {
281
- faces.push(
282
- ...parseFontFamilyFromCss(family, text, { ...filter, baseUrl }).faces,
283
- );
145
+ const parsed = await parseFontFamilyFromCss(family, text, {
146
+ ...filter,
147
+ baseUrl,
148
+ });
149
+ faces.push(...parsed.faces);
284
150
  }
285
151
  return { family, faces };
286
152
  }