@flighthq/font 0.1.0 → 0.2.0

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,2 +1,3 @@
1
- export declare function inferFontFormat(url: string): string | null;
1
+ export declare function detectFontFormat(bytes: ArrayBuffer | Uint8Array): string | null;
2
+ export declare function inferFontFormatFromUrl(url: string): string | null;
2
3
  //# sourceMappingURL=fontFormat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fontFormat.d.ts","sourceRoot":"","sources":["../src/fontFormat.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB1D"}
1
+ {"version":3,"file":"fontFormat.d.ts","sourceRoot":"","sources":["../src/fontFormat.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAuB/E;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjE"}
@@ -1,4 +1,28 @@
1
- export function inferFontFormat(url) {
1
+ export function detectFontFormat(bytes) {
2
+ const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
3
+ if (b.byteLength < 4)
4
+ return null;
5
+ // sfnt version 0x00010000 → TrueType outlines
6
+ if (b[0] === 0x00 && b[1] === 0x01 && b[2] === 0x00 && b[3] === 0x00)
7
+ return 'truetype';
8
+ // 'OTTO' → CFF/OpenType outlines
9
+ if (b[0] === 0x4f && b[1] === 0x54 && b[2] === 0x54 && b[3] === 0x4f)
10
+ return 'opentype';
11
+ // 'wOFF' → WOFF
12
+ if (b[0] === 0x77 && b[1] === 0x4f && b[2] === 0x46 && b[3] === 0x46)
13
+ return 'woff';
14
+ // 'wOF2' → WOFF2
15
+ if (b[0] === 0x77 && b[1] === 0x4f && b[2] === 0x46 && b[3] === 0x32)
16
+ return 'woff2';
17
+ // 'ttcf' → TrueType/OpenType Collection
18
+ if (b[0] === 0x74 && b[1] === 0x74 && b[2] === 0x63 && b[3] === 0x66)
19
+ return 'collection';
20
+ // 'true' → legacy Apple TrueType
21
+ if (b[0] === 0x74 && b[1] === 0x72 && b[2] === 0x75 && b[3] === 0x65)
22
+ return 'truetype';
23
+ return null;
24
+ }
25
+ export function inferFontFormatFromUrl(url) {
2
26
  const ext = url.split('?')[0].split('.').pop()?.toLowerCase();
3
27
  switch (ext) {
4
28
  case 'woff':
@@ -1 +1 @@
1
- {"version":3,"file":"fontFormat.js","sourceRoot":"","sources":["../src/fontFormat.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,CAAC;IAC9D,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,KAAK;YACR,OAAO,UAAU,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,UAAU,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,mBAAmB,CAAC;QAC7B,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;QACf;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"fontFormat.js","sourceRoot":"","sources":["../src/fontFormat.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,KAA+B;IAC9D,MAAM,CAAC,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACtE,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,8CAA8C;IAC9C,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAExF,iCAAiC;IACjC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAExF,gBAAgB;IAChB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAEpF,iBAAiB;IACjB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC;IAErF,wCAAwC;IACxC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,YAAY,CAAC;IAE1F,iCAAiC;IACjC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAExF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,CAAC;IAC9D,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,KAAK;YACR,OAAO,UAAU,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,UAAU,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,mBAAmB,CAAC;QAC7B,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;QACf;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"fontFrom.d.ts","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAKrD,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxF;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlE;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF"}
1
+ {"version":3,"file":"fontFrom.d.ts","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAMrD,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxF;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlE;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF"}
package/dist/fontFrom.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createFont } from './font';
2
- import { inferFontFormat } from './fontFormat';
2
+ import { inferFontFormatFromUrl } from './fontFormat';
3
+ import { getFontShorthand } from './fontShorthand';
3
4
  export async function loadFontFromBytes(bytes, family) {
4
5
  const face = new FontFace(family, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
5
6
  await face.load();
@@ -7,7 +8,7 @@ export async function loadFontFromBytes(bytes, family) {
7
8
  return createFont(family);
8
9
  }
9
10
  export async function loadFontFromName(name) {
10
- await document.fonts.load(`1em '${name}'`);
11
+ await document.fonts.load(getFontShorthand(name));
11
12
  return createFont(name);
12
13
  }
13
14
  export async function loadFontFromUrl(url, family) {
@@ -19,7 +20,7 @@ export async function loadFontFromUrl(url, family) {
19
20
  export async function loadFontFromUrls(sources, family) {
20
21
  const src = sources
21
22
  .map(({ url, format }) => {
22
- const fmt = format ?? inferFontFormat(url);
23
+ const fmt = format ?? inferFontFormatFromUrl(url);
23
24
  return fmt !== null ? `url(${url}) format('${fmt}')` : `url(${url})`;
24
25
  })
25
26
  .join(', ');
@@ -1 +1 @@
1
- {"version":3,"file":"fontFrom.js","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAiB,EAAE,MAAc;IACvE,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,MAAM,EACL,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC;IAC3C,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,MAAc;IAC/D,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACjD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAkB,EAAE,MAAc;IACvE,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC;QAC3C,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC"}
1
+ {"version":3,"file":"fontFrom.js","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAiB,EAAE,MAAc;IACvE,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,MAAM,EACL,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,MAAc;IAC/D,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACjD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAkB,EAAE,MAAc;IACvE,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"fontResourceFrom.d.ts","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI7D,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3G;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAIvF;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAMnG;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAY3G"}
1
+ {"version":3,"file":"fontResourceFrom.d.ts","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAK7D,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3G;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAIvF;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAMnG;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAY3G"}
@@ -1,4 +1,5 @@
1
- import { inferFontFormat } from './fontFormat';
1
+ import { inferFontFormatFromUrl } from './fontFormat';
2
+ import { getFontShorthand } from './fontShorthand';
2
3
  export async function loadFontResourceFromBytes(out, bytes) {
3
4
  const face = new FontFace(out.family, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
4
5
  await face.load();
@@ -7,7 +8,7 @@ export async function loadFontResourceFromBytes(out, bytes) {
7
8
  return out;
8
9
  }
9
10
  export async function loadFontResourceFromName(out) {
10
- const faces = await document.fonts.load(`1em '${out.family}'`);
11
+ const faces = await document.fonts.load(getFontShorthand(out.family));
11
12
  if (faces.length > 0)
12
13
  out.face = faces[0];
13
14
  return out;
@@ -22,7 +23,7 @@ export async function loadFontResourceFromUrl(out, url) {
22
23
  export async function loadFontResourceFromUrls(out, sources) {
23
24
  const src = sources
24
25
  .map(({ url, format }) => {
25
- const fmt = format ?? inferFontFormat(url);
26
+ const fmt = format ?? inferFontFormatFromUrl(url);
26
27
  return fmt !== null ? `url(${url}) format('${fmt}')` : `url(${url})`;
27
28
  })
28
29
  .join(', ');
@@ -1 +1 @@
1
- {"version":3,"file":"fontResourceFrom.js","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAiB,EAAE,KAAiB;IAClF,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,GAAG,CAAC,MAAM,EACT,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB;IAC9D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAiB,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB,EAAE,OAAkB;IAClF,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC;QAC3C,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"fontResourceFrom.js","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAiB,EAAE,KAAiB;IAClF,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,GAAG,CAAC,MAAM,EACT,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB;IAC9D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAiB,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB,EAAE,OAAkB;IAClF,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function getFontShorthand(family: string, style?: string): string;
2
+ //# sourceMappingURL=fontShorthand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fontShorthand.d.ts","sourceRoot":"","sources":["../src/fontShorthand.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAKvE"}
@@ -0,0 +1,7 @@
1
+ export function getFontShorthand(family, style) {
2
+ // Backslash-escape backslashes and single quotes so a family name containing either
3
+ // cannot break out of the quoted CSS string in the `font` shorthand.
4
+ const quoted = `'${family.replace(/[\\']/g, '\\$&')}'`;
5
+ return style !== undefined && style !== '' ? `${style} 1em ${quoted}` : `1em ${quoted}`;
6
+ }
7
+ //# sourceMappingURL=fontShorthand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fontShorthand.js","sourceRoot":"","sources":["../src/fontShorthand.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,KAAc;IAC7D,oFAAoF;IACpF,qEAAqE;IACrE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC;IACvD,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,QAAQ,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,MAAM,EAAE,CAAC;AAC1F,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function isFontLoaded(family: string, style?: string): boolean;
2
+ export declare function whenFontsReady(): Promise<void>;
3
+ //# sourceMappingURL=fontStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fontStatus.d.ts","sourceRoot":"","sources":["../src/fontStatus.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpD"}
@@ -0,0 +1,8 @@
1
+ import { getFontShorthand } from './fontShorthand';
2
+ export function isFontLoaded(family, style) {
3
+ return document.fonts.check(getFontShorthand(family, style));
4
+ }
5
+ export async function whenFontsReady() {
6
+ await document.fonts.ready;
7
+ }
8
+ //# sourceMappingURL=fontStatus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fontStatus.js","sourceRoot":"","sources":["../src/fontStatus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,KAAc;IACzD,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;AAC7B,CAAC"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export * from './fontFormat';
3
3
  export * from './fontFrom';
4
4
  export * from './fontResource';
5
5
  export * from './fontResourceFrom';
6
+ export * from './fontShorthand';
7
+ export * from './fontStatus';
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -3,4 +3,6 @@ export * from './fontFormat';
3
3
  export * from './fontFrom';
4
4
  export * from './fontResource';
5
5
  export * from './fontResourceFrom';
6
+ export * from './fontShorthand';
7
+ export * from './fontStatus';
6
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@flighthq/font",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/font"
8
+ },
4
9
  "type": "module",
5
10
  "main": "dist/index.js",
6
11
  "types": "dist/index.d.ts",
@@ -27,8 +32,8 @@
27
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
33
  },
29
34
  "dependencies": {
30
- "@flighthq/entity": "0.1.0",
31
- "@flighthq/types": "0.1.0"
35
+ "@flighthq/entity": "0.2.0",
36
+ "@flighthq/types": "0.2.0"
32
37
  },
33
38
  "devDependencies": {
34
39
  "typescript": "^5.3.0"
@@ -1,43 +1,95 @@
1
- import { inferFontFormat } from './fontFormat';
1
+ import { detectFontFormat, inferFontFormatFromUrl } from './fontFormat';
2
2
 
3
- describe('inferFontFormat', () => {
3
+ function bytesOf(...values: number[]): Uint8Array {
4
+ return new Uint8Array(values);
5
+ }
6
+
7
+ function asciiBytes(text: string): Uint8Array {
8
+ const b = new Uint8Array(text.length);
9
+ for (let i = 0; i < text.length; i++) b[i] = text.charCodeAt(i);
10
+ return b;
11
+ }
12
+
13
+ describe('detectFontFormat', () => {
14
+ it('returns "truetype" for the 0x00010000 sfnt signature', () => {
15
+ expect(detectFontFormat(bytesOf(0x00, 0x01, 0x00, 0x00))).toBe('truetype');
16
+ });
17
+
18
+ it('returns "opentype" for the "OTTO" signature', () => {
19
+ expect(detectFontFormat(asciiBytes('OTTO'))).toBe('opentype');
20
+ });
21
+
22
+ it('returns "woff" for the "wOFF" signature', () => {
23
+ expect(detectFontFormat(asciiBytes('wOFF'))).toBe('woff');
24
+ });
25
+
26
+ it('returns "woff2" for the "wOF2" signature', () => {
27
+ expect(detectFontFormat(asciiBytes('wOF2'))).toBe('woff2');
28
+ });
29
+
30
+ it('returns "collection" for the "ttcf" signature', () => {
31
+ expect(detectFontFormat(asciiBytes('ttcf'))).toBe('collection');
32
+ });
33
+
34
+ it('returns "truetype" for the legacy "true" signature', () => {
35
+ expect(detectFontFormat(asciiBytes('true'))).toBe('truetype');
36
+ });
37
+
38
+ it('accepts an ArrayBuffer as well as a Uint8Array', () => {
39
+ expect(detectFontFormat(asciiBytes('wOFF').buffer as ArrayBuffer)).toBe('woff');
40
+ });
41
+
42
+ it('ignores trailing bytes past the 4-byte signature', () => {
43
+ expect(detectFontFormat(asciiBytes('OTTO with more data'))).toBe('opentype');
44
+ });
45
+
46
+ it('returns null for an unrecognized signature', () => {
47
+ expect(detectFontFormat(asciiBytes('%PDF'))).toBeNull();
48
+ });
49
+
50
+ it('returns null when there are fewer than 4 bytes', () => {
51
+ expect(detectFontFormat(bytesOf(0x00, 0x01, 0x00))).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe('inferFontFormatFromUrl', () => {
4
56
  it('returns "woff" for .woff files', () => {
5
- expect(inferFontFormat('font.woff')).toBe('woff');
57
+ expect(inferFontFormatFromUrl('font.woff')).toBe('woff');
6
58
  });
7
59
 
8
60
  it('returns "woff2" for .woff2 files', () => {
9
- expect(inferFontFormat('font.woff2')).toBe('woff2');
61
+ expect(inferFontFormatFromUrl('font.woff2')).toBe('woff2');
10
62
  });
11
63
 
12
64
  it('returns "truetype" for .ttf files', () => {
13
- expect(inferFontFormat('font.ttf')).toBe('truetype');
65
+ expect(inferFontFormatFromUrl('font.ttf')).toBe('truetype');
14
66
  });
15
67
 
16
68
  it('returns "opentype" for .otf files', () => {
17
- expect(inferFontFormat('font.otf')).toBe('opentype');
69
+ expect(inferFontFormatFromUrl('font.otf')).toBe('opentype');
18
70
  });
19
71
 
20
72
  it('returns "embedded-opentype" for .eot files', () => {
21
- expect(inferFontFormat('font.eot')).toBe('embedded-opentype');
73
+ expect(inferFontFormatFromUrl('font.eot')).toBe('embedded-opentype');
22
74
  });
23
75
 
24
76
  it('returns "svg" for .svg files', () => {
25
- expect(inferFontFormat('font.svg')).toBe('svg');
77
+ expect(inferFontFormatFromUrl('font.svg')).toBe('svg');
26
78
  });
27
79
 
28
80
  it('returns null for unrecognized extensions', () => {
29
- expect(inferFontFormat('font.bin')).toBeNull();
81
+ expect(inferFontFormatFromUrl('font.bin')).toBeNull();
30
82
  });
31
83
 
32
84
  it('strips query parameters before matching', () => {
33
- expect(inferFontFormat('font.woff2?v=123')).toBe('woff2');
85
+ expect(inferFontFormatFromUrl('font.woff2?v=123')).toBe('woff2');
34
86
  });
35
87
 
36
88
  it('is case-insensitive', () => {
37
- expect(inferFontFormat('font.WOFF2')).toBe('woff2');
89
+ expect(inferFontFormatFromUrl('font.WOFF2')).toBe('woff2');
38
90
  });
39
91
 
40
92
  it('returns null for a URL with no extension', () => {
41
- expect(inferFontFormat('font')).toBeNull();
93
+ expect(inferFontFormatFromUrl('font')).toBeNull();
42
94
  });
43
95
  });
@@ -1,17 +1,34 @@
1
1
  import { loadFontFromBytes, loadFontFromName, loadFontFromUrl, loadFontFromUrls } from './fontFrom';
2
2
 
3
+ interface FontFaceConstruction {
4
+ family: string;
5
+ source: string | ArrayBuffer;
6
+ instance: MockFontFace;
7
+ }
8
+
9
+ let constructions: FontFaceConstruction[];
10
+ let addMock: ReturnType<typeof vi.fn>;
11
+ let loadMock: ReturnType<typeof vi.fn>;
12
+
3
13
  class MockFontFace {
4
14
  load = vi.fn().mockResolvedValue(undefined);
15
+ constructor(family: string, source: string | ArrayBuffer) {
16
+ const construction = { family, source, instance: this };
17
+ constructions.push(construction);
18
+ }
5
19
  }
6
20
 
7
21
  // Re-applied per test (not once) and torn down, so under a shared (isolate:false) worker with
8
22
  // unstubGlobals the FontFace stub survives every test and the document.fonts patch never leaks out.
9
23
  let originalFonts: PropertyDescriptor | undefined;
10
24
  beforeEach(() => {
25
+ constructions = [];
26
+ addMock = vi.fn();
27
+ loadMock = vi.fn().mockResolvedValue([]);
11
28
  vi.stubGlobal('FontFace', MockFontFace);
12
29
  originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
13
30
  Object.defineProperty(document, 'fonts', {
14
- value: { add: vi.fn(), load: vi.fn().mockResolvedValue([]) },
31
+ value: { add: addMock, load: loadMock },
15
32
  configurable: true,
16
33
  });
17
34
  });
@@ -29,6 +46,35 @@ describe('loadFontFromBytes', () => {
29
46
  const font = await loadFontFromBytes(new Uint8Array(0), 'TestFont');
30
47
  expect(font.name).toBe('TestFont');
31
48
  });
49
+
50
+ it('loads the face and registers it with document.fonts', async () => {
51
+ await loadFontFromBytes(new Uint8Array(8), 'TestFont');
52
+ expect(constructions).toHaveLength(1);
53
+ expect(constructions[0].family).toBe('TestFont');
54
+ expect(constructions[0].instance.load).toHaveBeenCalledOnce();
55
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
56
+ });
57
+
58
+ it('slices only the view bytes out of a larger backing buffer', async () => {
59
+ const backing = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
60
+ const view = backing.subarray(2, 6);
61
+ await loadFontFromBytes(view, 'TestFont');
62
+ const source = constructions[0].source as ArrayBuffer;
63
+ expect(new Uint8Array(source)).toEqual(new Uint8Array([3, 4, 5, 6]));
64
+ });
65
+
66
+ it('propagates a load failure and does not register the face', async () => {
67
+ const failing = new MockFontFace('', new ArrayBuffer(0));
68
+ failing.load.mockRejectedValue(new Error('bad font'));
69
+ vi.stubGlobal(
70
+ 'FontFace',
71
+ vi.fn(function () {
72
+ return failing;
73
+ }),
74
+ );
75
+ await expect(loadFontFromBytes(new Uint8Array(4), 'TestFont')).rejects.toThrow('bad font');
76
+ expect(addMock).not.toHaveBeenCalled();
77
+ });
32
78
  });
33
79
 
34
80
  describe('loadFontFromName', () => {
@@ -36,6 +82,21 @@ describe('loadFontFromName', () => {
36
82
  const font = await loadFontFromName('MyFont');
37
83
  expect(font.name).toBe('MyFont');
38
84
  });
85
+
86
+ it('loads the family via the escaped font shorthand', async () => {
87
+ await loadFontFromName('MyFont');
88
+ expect(loadMock).toHaveBeenCalledWith("1em 'MyFont'");
89
+ });
90
+
91
+ it('escapes a single quote in the family name so the shorthand stays valid', async () => {
92
+ await loadFontFromName("Josh's Font");
93
+ expect(loadMock).toHaveBeenCalledWith("1em 'Josh\\'s Font'");
94
+ });
95
+
96
+ it('propagates a load failure', async () => {
97
+ loadMock.mockRejectedValue(new Error('no such font'));
98
+ await expect(loadFontFromName('MyFont')).rejects.toThrow('no such font');
99
+ });
39
100
  });
40
101
 
41
102
  describe('loadFontFromUrl', () => {
@@ -43,6 +104,13 @@ describe('loadFontFromUrl', () => {
43
104
  const font = await loadFontFromUrl('font.woff2', 'MyFont');
44
105
  expect(font.name).toBe('MyFont');
45
106
  });
107
+
108
+ it('builds a url() source and registers the face', async () => {
109
+ await loadFontFromUrl('font.woff2', 'MyFont');
110
+ expect(constructions[0].family).toBe('MyFont');
111
+ expect(constructions[0].source).toBe('url(font.woff2)');
112
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
113
+ });
46
114
  });
47
115
 
48
116
  describe('loadFontFromUrls', () => {
@@ -50,4 +118,28 @@ describe('loadFontFromUrls', () => {
50
118
  const font = await loadFontFromUrls([{ url: 'font.woff2', format: 'woff2' }], 'MyFont');
51
119
  expect(font.name).toBe('MyFont');
52
120
  });
121
+
122
+ it('composes a comma-joined src with explicit and inferred format() hints', async () => {
123
+ await loadFontFromUrls([{ url: 'font.woff2', format: 'woff2' }, { url: 'font.ttf' }], 'MyFont');
124
+ expect(constructions[0].source).toBe("url(font.woff2) format('woff2'), url(font.ttf) format('truetype')");
125
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
126
+ });
127
+
128
+ it('omits the format() hint when it cannot be inferred and none is given', async () => {
129
+ await loadFontFromUrls([{ url: 'font.bin' }], 'MyFont');
130
+ expect(constructions[0].source).toBe('url(font.bin)');
131
+ });
132
+
133
+ it('propagates a load failure and does not register the face', async () => {
134
+ const failing = new MockFontFace('', '');
135
+ failing.load.mockRejectedValue(new Error('bad url'));
136
+ vi.stubGlobal(
137
+ 'FontFace',
138
+ vi.fn(function () {
139
+ return failing;
140
+ }),
141
+ );
142
+ await expect(loadFontFromUrls([{ url: 'font.woff2' }], 'MyFont')).rejects.toThrow('bad url');
143
+ expect(addMock).not.toHaveBeenCalled();
144
+ });
53
145
  });
@@ -0,0 +1,23 @@
1
+ import { getFontShorthand } from './fontShorthand';
2
+
3
+ describe('getFontShorthand', () => {
4
+ it('quotes a plain family name at 1em', () => {
5
+ expect(getFontShorthand('MyFont')).toBe("1em 'MyFont'");
6
+ });
7
+
8
+ it('backslash-escapes single quotes in the family name', () => {
9
+ expect(getFontShorthand("Josh's Font")).toBe("1em 'Josh\\'s Font'");
10
+ });
11
+
12
+ it('backslash-escapes backslashes in the family name', () => {
13
+ expect(getFontShorthand('Back\\slash')).toBe("1em 'Back\\\\slash'");
14
+ });
15
+
16
+ it('prefixes a style when provided', () => {
17
+ expect(getFontShorthand('MyFont', 'italic')).toBe("italic 1em 'MyFont'");
18
+ });
19
+
20
+ it('omits the style prefix for an empty style string', () => {
21
+ expect(getFontShorthand('MyFont', '')).toBe("1em 'MyFont'");
22
+ });
23
+ });
@@ -0,0 +1,49 @@
1
+ import { isFontLoaded, whenFontsReady } from './fontStatus';
2
+
3
+ let originalFonts: PropertyDescriptor | undefined;
4
+ let checkMock: ReturnType<typeof vi.fn>;
5
+
6
+ beforeEach(() => {
7
+ checkMock = vi.fn().mockReturnValue(true);
8
+ originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
9
+ Object.defineProperty(document, 'fonts', {
10
+ value: { check: checkMock, ready: Promise.resolve() },
11
+ configurable: true,
12
+ });
13
+ });
14
+
15
+ afterEach(() => {
16
+ if (originalFonts) {
17
+ Object.defineProperty(document, 'fonts', originalFonts);
18
+ } else {
19
+ delete (document as unknown as { fonts?: unknown }).fonts;
20
+ }
21
+ });
22
+
23
+ describe('isFontLoaded', () => {
24
+ it('queries document.fonts.check with the family shorthand', () => {
25
+ expect(isFontLoaded('MyFont')).toBe(true);
26
+ expect(checkMock).toHaveBeenCalledWith("1em 'MyFont'");
27
+ });
28
+
29
+ it('includes the style in the shorthand when provided', () => {
30
+ isFontLoaded('MyFont', 'italic');
31
+ expect(checkMock).toHaveBeenCalledWith("italic 1em 'MyFont'");
32
+ });
33
+
34
+ it('escapes single quotes in the family name', () => {
35
+ isFontLoaded("Josh's Font");
36
+ expect(checkMock).toHaveBeenCalledWith("1em 'Josh\\'s Font'");
37
+ });
38
+
39
+ it('returns false when the font is not available', () => {
40
+ checkMock.mockReturnValue(false);
41
+ expect(isFontLoaded('Absent')).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('whenFontsReady', () => {
46
+ it('resolves once document.fonts.ready resolves', async () => {
47
+ await expect(whenFontsReady()).resolves.toBeUndefined();
48
+ });
49
+ });