@react-x11/components 0.13.0 → 0.14.1
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/dist/html/controls.d.ts +29 -2
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +70 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +42 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +336 -16
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +18 -4
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +66 -28
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +44 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +194 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +128 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +547 -144
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +14 -2
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/fonts.d.ts +55 -5
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +284 -23
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +104 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +410 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +72 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +40 -2
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +144 -24
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +585 -85
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +144 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +97 -31
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +4 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +319 -47
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +83 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1474 -217
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +39 -9
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +482 -37
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.js +87 -7
- package/dist/html/widgets.js.map +1 -1
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/html/controls.ts +85 -2
- package/src/html/css/cascade.ts +366 -8
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +71 -31
- package/src/html/css/shapes.ts +239 -0
- package/src/html/css/style.ts +652 -147
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +15 -2
- package/src/html/css/values.ts +4 -0
- package/src/html/fonts.ts +312 -29
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +466 -37
- package/src/html/layout/boxes.ts +95 -3
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +169 -32
- package/src/html/layout/flex.ts +686 -79
- package/src/html/layout/inline.ts +149 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +105 -35
- package/src/html/node.ts +354 -48
- package/src/html/paint.ts +1804 -315
- package/src/html/svg.ts +573 -36
- package/src/html/widgets.ts +95 -9
- package/src/html/woff2.ts +612 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/fonts.ts
CHANGED
|
@@ -47,12 +47,50 @@
|
|
|
47
47
|
// none left the family stays out of the list and the text is set in the
|
|
48
48
|
// next one, as a browser sets it when it cannot use a font.
|
|
49
49
|
//
|
|
50
|
+
// **A WOFF2 an engine does not take is handed over as the font inside it.**
|
|
51
|
+
// The web serves a font as WOFF2 and as little else, and not every engine
|
|
52
|
+
// reads one: CoreText reads an sfnt and nothing more, so react-x11's
|
|
53
|
+
// `loadFont` throws for a WOFF2 on macOS, and the fontkit ntk cuts a
|
|
54
|
+
// variable face with cut nothing out of one. Either way the page was set in
|
|
55
|
+
// its fallback — nextjs.org in Arial, the family its `local()` names, where
|
|
56
|
+
// a browser sets it in Geist. So a file is offered as it was served, and
|
|
57
|
+
// where that is refused the sfnt it wraps is rebuilt (`woff2.ts`) and
|
|
58
|
+
// offered in its place (`_register`); only a font refused both ways is a
|
|
59
|
+
// source that did not load. Nothing asks which engine it is.
|
|
60
|
+
//
|
|
61
|
+
// **A variable face is set at the weight its rule has for a style's.** The
|
|
62
|
+
// weight a style asks for is a place on the face's `wght` axis, clamped to
|
|
63
|
+
// the range its `@font-face` declares (CSS Fonts 4, 7.2), and the rule is
|
|
64
|
+
// the document's: the value is said here (`wght`) and handed to the engine
|
|
65
|
+
// with the run (`layout/axes.ts`). Left alone, ntk moves the axis to the
|
|
66
|
+
// style's weight whatever the rule declared, and CoreText, for a face
|
|
67
|
+
// react-x11 registered, does not move it — every weight of Geist was its
|
|
68
|
+
// regular on macOS.
|
|
69
|
+
//
|
|
50
70
|
// A family split by `unicode-range` is registered a name per range, since the
|
|
51
71
|
// font manager picks among one name's faces by weight and slant alone, and
|
|
52
72
|
// the range that holds the most of the document's characters goes first: the
|
|
53
73
|
// first family is the one a run is set in, and the others are reached through
|
|
54
74
|
// the fallback for the characters it lacks.
|
|
55
75
|
//
|
|
76
|
+
// **A family the document declares is never the system's by that name.**
|
|
77
|
+
// CSS Fonts 4 (5.2) has a family defined by `@font-face` with no face
|
|
78
|
+
// present treated as missing, and forbids matching a platform font of the
|
|
79
|
+
// same name. That holds for a family whose sources are all `local()`s too,
|
|
80
|
+
// which is the one that used to get past: next/font declares
|
|
81
|
+
// `"GeistSans Fallback"` as `src: local("Arial")`, the rule was dropped for
|
|
82
|
+
// having no `url()`, and the name went to the text engine as written —
|
|
83
|
+
// where fontconfig's nearest guess for a family nobody has was Hiragino Sans
|
|
84
|
+
// at 400 and Gill Sans Ultra Bold at 450.
|
|
85
|
+
//
|
|
86
|
+
// A `local()` is a source like a `url()`, tried in the order `src` lists
|
|
87
|
+
// them, and what it comes to is **an alias**: a face the system has goes
|
|
88
|
+
// into the list as its family's name (`localFamily`), in the place of the
|
|
89
|
+
// document's, and nothing is registered. A font manager has no lookup by a
|
|
90
|
+
// face's name — it answers any family with its best guess — so the question
|
|
91
|
+
// is put as a match, and the answer is believed only when the face that
|
|
92
|
+
// comes back says it is the one named.
|
|
93
|
+
//
|
|
56
94
|
// What stays the application's: registering a face adds it to ntk's
|
|
57
95
|
// fallback chain, where it can supply a glyph no other face has to text
|
|
58
96
|
// anywhere in the app (react-x11's `loadFont` says so), and nothing is ever
|
|
@@ -64,6 +102,7 @@ import type { Element } from 'domhandler';
|
|
|
64
102
|
import type { FontFaceRule } from './css/parse.js';
|
|
65
103
|
import type { ComputedStyle } from './css/style.js';
|
|
66
104
|
import type { ResourceRequest, ResourceResult } from './resources.js';
|
|
105
|
+
import { isWoff2, sfntFromWoff2 } from './woff2.js';
|
|
67
106
|
|
|
68
107
|
/** What the cascade asks of the document's families (`Cascade`). */
|
|
69
108
|
export interface FontFamilies {
|
|
@@ -108,6 +147,13 @@ interface Face {
|
|
|
108
147
|
family: Family;
|
|
109
148
|
/** Where loading got to. `idle` has not been asked for. */
|
|
110
149
|
state: 'idle' | 'loading' | 'ready' | 'failed';
|
|
150
|
+
/** The system's family a `ready` face came to through a `local()`, which
|
|
151
|
+
* the list names in place of the group's; null for a face registered
|
|
152
|
+
* from a file. */
|
|
153
|
+
local: string | null;
|
|
154
|
+
/** The range of the `wght` axis a `ready` face's file has, or null for a
|
|
155
|
+
* file with none: a static face, or the system's. */
|
|
156
|
+
wght: [number, number] | null;
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
/** A family's faces for one unicode range: one registered name, among whose
|
|
@@ -135,6 +181,12 @@ interface Registry {
|
|
|
135
181
|
names: Map<string, string>;
|
|
136
182
|
/** Faces registered, by `faceKey`. */
|
|
137
183
|
ready: Set<string>;
|
|
184
|
+
/** The ones among them that are the system's (`local()`), and registered
|
|
185
|
+
* under no name: the family each came to. */
|
|
186
|
+
locals: Map<string, string>;
|
|
187
|
+
/** The ones whose file is a variable font with a `wght` axis, and the
|
|
188
|
+
* axis's range. */
|
|
189
|
+
axes: Map<string, [number, number]>;
|
|
138
190
|
/** Faces being loaded, by `faceKey`, so a second document waits for the
|
|
139
191
|
* first document's request rather than making its own. */
|
|
140
192
|
pending: Map<string, Promise<boolean>>;
|
|
@@ -151,6 +203,8 @@ function registryOf(app: object): Registry {
|
|
|
151
203
|
next: 0,
|
|
152
204
|
names: new Map(),
|
|
153
205
|
ready: new Set(),
|
|
206
|
+
locals: new Map(),
|
|
207
|
+
axes: new Map(),
|
|
154
208
|
pending: new Map(),
|
|
155
209
|
warned: false,
|
|
156
210
|
};
|
|
@@ -187,6 +241,14 @@ export class WebFonts implements FontFamilies {
|
|
|
187
241
|
* lists that come to one string share an entry, which can ask for a face
|
|
188
242
|
* a little early; never for one nothing wants. */
|
|
189
243
|
private _sources = new Map<string, Family[]>();
|
|
244
|
+
/** The groups by the names they are registered under, which is how a
|
|
245
|
+
* list the engine is handed names one (`wght`). */
|
|
246
|
+
private _groups = new Map<string, Group>();
|
|
247
|
+
/** Lists → weight and slant → the axis value text in them is set at. */
|
|
248
|
+
private _axes = new Map<string, Map<number, WeightAxis | null>>();
|
|
249
|
+
/** Whether any loaded face has a weight axis to set; null for not yet
|
|
250
|
+
* asked since the faces changed. */
|
|
251
|
+
private _variable: boolean | null = null;
|
|
190
252
|
/** The document's characters, as the last `request` saw them. */
|
|
191
253
|
private _text: string | null = null;
|
|
192
254
|
private _seen = new Set<number>();
|
|
@@ -269,14 +331,17 @@ export class WebFonts implements FontFamilies {
|
|
|
269
331
|
order: order++,
|
|
270
332
|
};
|
|
271
333
|
for (const m of members) {
|
|
272
|
-
const ready = registry?.ready.has(faceKey(name, m.rule)) ?? false;
|
|
273
334
|
const face: Face = {
|
|
274
335
|
rule: m.rule,
|
|
275
336
|
element: m.element,
|
|
276
337
|
group,
|
|
277
338
|
family,
|
|
278
|
-
state:
|
|
339
|
+
state: 'idle',
|
|
340
|
+
local: null,
|
|
341
|
+
wght: null,
|
|
279
342
|
};
|
|
343
|
+
if (registry?.ready.has(faceKey(name, m.rule)))
|
|
344
|
+
arrive(face, registry);
|
|
280
345
|
group.faces.push(face);
|
|
281
346
|
made.set(m, face);
|
|
282
347
|
}
|
|
@@ -285,6 +350,10 @@ export class WebFonts implements FontFamilies {
|
|
|
285
350
|
families.set(key, family);
|
|
286
351
|
}
|
|
287
352
|
this._families = families;
|
|
353
|
+
this._groups.clear();
|
|
354
|
+
for (const family of families.values()) {
|
|
355
|
+
for (const group of family.groups) this._groups.set(group.name, group);
|
|
356
|
+
}
|
|
288
357
|
this._faces = declared.map((d) => made.get(d)!);
|
|
289
358
|
// the ranges are new, and so is what they cover
|
|
290
359
|
if (this._text !== null) this._measure(this._text, true);
|
|
@@ -307,7 +376,15 @@ export class WebFonts implements FontFamilies {
|
|
|
307
376
|
}
|
|
308
377
|
used.push(family);
|
|
309
378
|
for (const group of orderedGroups(family)) {
|
|
310
|
-
|
|
379
|
+
// the name its files are registered under, then the families its
|
|
380
|
+
// `local()`s came to
|
|
381
|
+
const ready = group.faces.filter((f) => f.state === 'ready');
|
|
382
|
+
if (ready.some((f) => f.local === null)) out.push(group.name);
|
|
383
|
+
for (const face of ready) {
|
|
384
|
+
if (face.local !== null && !out.includes(face.local)) {
|
|
385
|
+
out.push(face.local);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
311
388
|
}
|
|
312
389
|
}
|
|
313
390
|
let mapped = out.length ? out.join(', ') : this._fallback;
|
|
@@ -337,6 +414,64 @@ export class WebFonts implements FontFamilies {
|
|
|
337
414
|
for (const family of families) family.wants.add(want);
|
|
338
415
|
}
|
|
339
416
|
|
|
417
|
+
/** Whether any face the document has loaded is a variable font whose rule
|
|
418
|
+
* declares a range of weights: whether `wght` has anything to say. */
|
|
419
|
+
get variable(): boolean {
|
|
420
|
+
if (this._variable === null) {
|
|
421
|
+
this._variable = false;
|
|
422
|
+
for (const group of this._groups.values()) {
|
|
423
|
+
if (group.faces.some((f) => ranged(f) !== null)) this._variable = true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return this._variable;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Where on its weight axis text in a family list — one `map` made — is
|
|
431
|
+
* set at a weight and slant, or null where no axis is this document's to
|
|
432
|
+
* set.
|
|
433
|
+
*
|
|
434
|
+
* CSS has the weight a style asks for applied to a variable face's `wght`
|
|
435
|
+
* axis, clamped to the range its `@font-face` rule declares (CSS Fonts 4,
|
|
436
|
+
* 7.2): `font-weight: 100 900` is every weight the file has, and
|
|
437
|
+
* under `font-weight: 400 700` text at 900 is set at 700. The rule is the
|
|
438
|
+
* document's, so the value is said here and handed to the engine with the
|
|
439
|
+
* run (`layout/axes.ts`). An engine left to itself either moves the axis
|
|
440
|
+
* to the style's weight, past what the rule declared — ntk — or not at
|
|
441
|
+
* all: a face react-x11 registers with CoreText is drawn at its file's
|
|
442
|
+
* default, and every weight of Geist on macOS was the regular.
|
|
443
|
+
*
|
|
444
|
+
* Only the list's first family is asked, the one text is set in, and only
|
|
445
|
+
* a face whose rule declares a range: one declared at a single weight is
|
|
446
|
+
* left as the engine sets it.
|
|
447
|
+
*/
|
|
448
|
+
wght(list: string, weight: number, italic: boolean): WeightAxis | null {
|
|
449
|
+
if (!this.variable) return null;
|
|
450
|
+
let byFace = this._axes.get(list);
|
|
451
|
+
if (!byFace) this._axes.set(list, (byFace = new Map()));
|
|
452
|
+
const key = italic ? -weight : weight;
|
|
453
|
+
let axis = byFace.get(key);
|
|
454
|
+
if (axis === undefined) {
|
|
455
|
+
axis = null;
|
|
456
|
+
const comma = list.indexOf(',');
|
|
457
|
+
const first = (comma < 0 ? list : list.slice(0, comma)).trim();
|
|
458
|
+
const face = bestFace(
|
|
459
|
+
// the faces registered under the name, which the engine picks among
|
|
460
|
+
this._groups
|
|
461
|
+
.get(first)
|
|
462
|
+
?.faces.filter((f) => f.state === 'ready' && f.local === null) ?? [],
|
|
463
|
+
weight,
|
|
464
|
+
italic,
|
|
465
|
+
);
|
|
466
|
+
const range = face && ranged(face);
|
|
467
|
+
if (range) {
|
|
468
|
+
axis = weightAxis(Math.max(range[0], Math.min(weight, range[1])));
|
|
469
|
+
}
|
|
470
|
+
byFace.set(key, axis);
|
|
471
|
+
}
|
|
472
|
+
return axis;
|
|
473
|
+
}
|
|
474
|
+
|
|
340
475
|
/**
|
|
341
476
|
* Ask for the faces the styles noted since the last call want, for the
|
|
342
477
|
* characters `text` holds — the document's own. True when the lists the
|
|
@@ -366,6 +501,7 @@ export class WebFonts implements FontFamilies {
|
|
|
366
501
|
destroy(): void {
|
|
367
502
|
this._destroyed = true;
|
|
368
503
|
this._families.clear();
|
|
504
|
+
this._groups.clear();
|
|
369
505
|
this._forget();
|
|
370
506
|
}
|
|
371
507
|
|
|
@@ -374,6 +510,8 @@ export class WebFonts implements FontFamilies {
|
|
|
374
510
|
private _forget(): void {
|
|
375
511
|
this._memo.clear();
|
|
376
512
|
this._sources.clear();
|
|
513
|
+
this._axes.clear();
|
|
514
|
+
this._variable = null;
|
|
377
515
|
}
|
|
378
516
|
|
|
379
517
|
/**
|
|
@@ -466,7 +604,7 @@ export class WebFonts implements FontFamilies {
|
|
|
466
604
|
const registry = registryOf(app);
|
|
467
605
|
const key = faceKey(face.group.name, face.rule);
|
|
468
606
|
if (registry.ready.has(key)) {
|
|
469
|
-
face
|
|
607
|
+
arrive(face, registry);
|
|
470
608
|
return 'ready';
|
|
471
609
|
}
|
|
472
610
|
face.state = 'loading';
|
|
@@ -487,7 +625,7 @@ export class WebFonts implements FontFamilies {
|
|
|
487
625
|
};
|
|
488
626
|
|
|
489
627
|
const sources = face.rule.sources.filter(
|
|
490
|
-
(s) => s.format === null || READABLE.has(s.format),
|
|
628
|
+
(s) => 'local' in s || s.format === null || READABLE.has(s.format),
|
|
491
629
|
);
|
|
492
630
|
let i = 0;
|
|
493
631
|
let sync = true;
|
|
@@ -496,6 +634,18 @@ export class WebFonts implements FontFamilies {
|
|
|
496
634
|
const next = (): 'ready' | 'waiting' | 'none' => {
|
|
497
635
|
while (i < sources.length) {
|
|
498
636
|
const source = sources[i++];
|
|
637
|
+
if ('local' in source) {
|
|
638
|
+
// nothing to ask the host for, and nothing to wait on
|
|
639
|
+
const family = this._destroyed
|
|
640
|
+
? null
|
|
641
|
+
: localFamily(app, source.local, face.rule);
|
|
642
|
+
if (family === null) continue;
|
|
643
|
+
registry.locals.set(key, family);
|
|
644
|
+
finish(true);
|
|
645
|
+
if (sync) arrive(face, registry);
|
|
646
|
+
else this._settled(face, true);
|
|
647
|
+
return 'ready';
|
|
648
|
+
}
|
|
499
649
|
let answer: ReturnType<Ask>;
|
|
500
650
|
try {
|
|
501
651
|
answer = this._ask({
|
|
@@ -507,15 +657,20 @@ export class WebFonts implements FontFamilies {
|
|
|
507
657
|
continue;
|
|
508
658
|
}
|
|
509
659
|
if (isPromise(answer)) {
|
|
510
|
-
answer
|
|
511
|
-
(result) =>
|
|
512
|
-
() => after(false)
|
|
513
|
-
|
|
660
|
+
answer
|
|
661
|
+
.then((result) => this._register(face, result, source.url))
|
|
662
|
+
.then(after, () => after(false));
|
|
663
|
+
return 'waiting';
|
|
664
|
+
}
|
|
665
|
+
const registered = this._register(face, answer, source.url);
|
|
666
|
+
if (isPromise(registered)) {
|
|
667
|
+
// a WOFF2 being handed over as the font inside it
|
|
668
|
+
registered.then(after, () => after(false));
|
|
514
669
|
return 'waiting';
|
|
515
670
|
}
|
|
516
|
-
if (
|
|
671
|
+
if (registered) {
|
|
517
672
|
finish(true);
|
|
518
|
-
if (sync) face
|
|
673
|
+
if (sync) arrive(face, registry);
|
|
519
674
|
else this._settled(face, true);
|
|
520
675
|
return 'ready';
|
|
521
676
|
}
|
|
@@ -541,27 +696,47 @@ export class WebFonts implements FontFamilies {
|
|
|
541
696
|
return outcome === 'ready' ? 'ready' : 'loading';
|
|
542
697
|
}
|
|
543
698
|
|
|
544
|
-
/**
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
699
|
+
/**
|
|
700
|
+
* Register a face's bytes under its group's name. False when the host
|
|
701
|
+
* declined, or the font manager could not read them — a file that is not
|
|
702
|
+
* a font — or read them and cannot set text in them (`refusal`), and the
|
|
703
|
+
* next source is tried.
|
|
704
|
+
*
|
|
705
|
+
* A WOFF2 the engine does not take as served is handed over again as the
|
|
706
|
+
* font inside it (`woff2.ts`), and the answer is then a promise: CoreText
|
|
707
|
+
* reads no such container, so react-x11's `loadFont` refuses every one on
|
|
708
|
+
* macOS, and an ntk whose fontkit cuts no instance out of one cuts it out
|
|
709
|
+
* of the sfnt. Nothing here asks which engine it is: the file is offered
|
|
710
|
+
* as the web served it, and rebuilt only for an engine that said no.
|
|
711
|
+
*/
|
|
549
712
|
private _register(
|
|
550
713
|
face: Face,
|
|
551
714
|
result: ResourceResult | null,
|
|
552
715
|
url: string,
|
|
553
|
-
): boolean {
|
|
716
|
+
): boolean | Promise<boolean> {
|
|
554
717
|
if (this._destroyed || !this._app || result?.kind !== 'font') return false;
|
|
718
|
+
const refused = this._set(face, result.bytes);
|
|
719
|
+
if (refused === null) return true;
|
|
720
|
+
if (!isWoff2(result.bytes)) return this._declined(url, refused);
|
|
721
|
+
return sfntFromWoff2(result.bytes).then((sfnt) => {
|
|
722
|
+
if (this._destroyed || !this._app) return false;
|
|
723
|
+
const still = sfnt ? this._set(face, sfnt) : refused;
|
|
724
|
+
return still === null || this._declined(url, still);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Hand a font file to the font manager, under a face's group's name.
|
|
729
|
+
* Null when it registered; otherwise why the engine cannot set text in
|
|
730
|
+
* it (`refusal`), or `''` for bytes it could not read at all. */
|
|
731
|
+
private _set(face: Face, bytes: Uint8Array): string | null {
|
|
555
732
|
const { weight, style } = face.rule;
|
|
556
733
|
const app = this._app as Parameters<typeof loadFont>[0];
|
|
557
734
|
try {
|
|
558
735
|
// `loadFont` opens the file first too, and finds this one opened
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
loadFont(app, result.bytes, {
|
|
736
|
+
const opened: OpenedFace = openFont(app, bytes);
|
|
737
|
+
const refused = refusal(app, opened, this._fallback);
|
|
738
|
+
if (refused !== null) return refused;
|
|
739
|
+
loadFont(app, bytes, {
|
|
565
740
|
family: face.group.name,
|
|
566
741
|
// a range is registered at the weight nearest regular in it: the
|
|
567
742
|
// font manager picks among a group's faces by distance from one
|
|
@@ -569,24 +744,74 @@ export class WebFonts implements FontFamilies {
|
|
|
569
744
|
weight: Math.max(weight[0], Math.min(400, weight[1])),
|
|
570
745
|
style,
|
|
571
746
|
});
|
|
572
|
-
|
|
747
|
+
const axis = opened.variationAxes?.wght;
|
|
748
|
+
if (axis && axis.min < axis.max) {
|
|
749
|
+
const key = faceKey(face.group.name, face.rule);
|
|
750
|
+
registryOf(this._app!).axes.set(key, [axis.min, axis.max]);
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
573
753
|
} catch {
|
|
574
|
-
return
|
|
754
|
+
return '';
|
|
575
755
|
}
|
|
576
756
|
}
|
|
577
757
|
|
|
758
|
+
/** A source did not register, and the one the engine refused is said. */
|
|
759
|
+
private _declined(url: string, why: string): false {
|
|
760
|
+
if (why && this._app) warnRefused(registryOf(this._app), url, why);
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
|
|
578
764
|
/** A face arrived, or will not. Either way the boxes are built again: an
|
|
579
765
|
* arrival changes the lists, and a failure has the next face asked for,
|
|
580
766
|
* by a family that has to be asked again to ask it. */
|
|
581
767
|
private _settled(face: Face, ok: boolean): void {
|
|
582
768
|
if (this._destroyed) return;
|
|
583
|
-
|
|
769
|
+
if (ok && this._app) arrive(face, registryOf(this._app));
|
|
770
|
+
else face.state = ok ? 'ready' : 'failed';
|
|
584
771
|
if (!ok) face.family.asked.clear();
|
|
585
772
|
this._forget();
|
|
586
773
|
this._changed();
|
|
587
774
|
}
|
|
588
775
|
}
|
|
589
776
|
|
|
777
|
+
/** A registered face is the document's to use: what the connection knows
|
|
778
|
+
* of it — the family a `local()` came to, the axis its file has — is the
|
|
779
|
+
* face's. */
|
|
780
|
+
function arrive(face: Face, registry: Registry): void {
|
|
781
|
+
const key = faceKey(face.group.name, face.rule);
|
|
782
|
+
face.state = 'ready';
|
|
783
|
+
face.local = registry.locals.get(key) ?? null;
|
|
784
|
+
face.wght = registry.axes.get(key) ?? null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** A point on the weight axis, as an engine takes one with a run: its
|
|
788
|
+
* `variations`. */
|
|
789
|
+
export interface WeightAxis {
|
|
790
|
+
wght: number;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** One object a value: ntk tells two runs' variations apart by identity,
|
|
794
|
+
* and would set two runs at one weight as two. */
|
|
795
|
+
const WEIGHTS = new Map<number, WeightAxis>();
|
|
796
|
+
|
|
797
|
+
function weightAxis(wght: number): WeightAxis {
|
|
798
|
+
let axis = WEIGHTS.get(wght);
|
|
799
|
+
if (!axis) WEIGHTS.set(wght, (axis = { wght }));
|
|
800
|
+
return axis;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** The weights a loaded face's axis is set within: the range its rule
|
|
804
|
+
* declares, inside the one its file has. Null for a face with no axis, or
|
|
805
|
+
* declared at a single weight. */
|
|
806
|
+
function ranged(face: Face): [number, number] | null {
|
|
807
|
+
if (face.state !== 'ready' || !face.wght) return null;
|
|
808
|
+
const lo = Math.max(face.rule.weight[0], face.wght[0]);
|
|
809
|
+
const hi = Math.min(face.rule.weight[1], face.wght[1]);
|
|
810
|
+
return face.rule.weight[0] < face.rule.weight[1] && lo <= hi
|
|
811
|
+
? [lo, hi]
|
|
812
|
+
: null;
|
|
813
|
+
}
|
|
814
|
+
|
|
590
815
|
/** The slice of an opened face `refusal` reads: react-x11's `Font`. */
|
|
591
816
|
interface OpenedFace {
|
|
592
817
|
variationAxes?: Record<string, { min: number; default: number; max: number }>;
|
|
@@ -677,9 +902,67 @@ function faceKey(name: string, rule: FontFaceRule): string {
|
|
|
677
902
|
|
|
678
903
|
function faceSignature(rule: FontFaceRule): string {
|
|
679
904
|
const range = rangeKey(rule.unicodeRange);
|
|
680
|
-
|
|
681
|
-
'
|
|
682
|
-
)
|
|
905
|
+
const sources = rule.sources.map((s) =>
|
|
906
|
+
'local' in s ? `local(${s.local})` : s.url,
|
|
907
|
+
);
|
|
908
|
+
return `${sources.join(' ')}@${rule.weight.join('-')}${rule.style}#${range}`;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** What is asked of a font manager to find a `local()`: a match, and the
|
|
912
|
+
* names of the face it answers with. Both engines' faces carry them. */
|
|
913
|
+
interface FontLookup {
|
|
914
|
+
match?(
|
|
915
|
+
family: string,
|
|
916
|
+
opts: { weight: number; style: string },
|
|
917
|
+
): { familyName?: string; postscriptName?: string } | null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* The family of the face a `local()` names, or null when the system has no
|
|
922
|
+
* such face and the next source is tried.
|
|
923
|
+
*
|
|
924
|
+
* CSS Fonts 4 (4.3.1) has the name be one face's — its full name or its
|
|
925
|
+
* PostScript name, never a family's, and never a platform's substitute for
|
|
926
|
+
* it. A font manager has no lookup by either: it takes a family and answers
|
|
927
|
+
* with its best guess, which for a name nobody has is some other font. So
|
|
928
|
+
* the name is matched as a family, at the weight and slant the rule declares
|
|
929
|
+
* its face to have, and the answer counts only when the face that comes back
|
|
930
|
+
* says it is the one asked for: by its family, which is the full name of a
|
|
931
|
+
* family's regular face and how `local()` is nearly always spelled
|
|
932
|
+
* (`local(Arial)`), or by its PostScript name, where an engine finds a face
|
|
933
|
+
* by one. A full name with a style in it (`local(Arial Bold)`) is no family,
|
|
934
|
+
* and is not found.
|
|
935
|
+
*
|
|
936
|
+
* What comes back is the family and not the face: the list names it, and
|
|
937
|
+
* the engine picks among its faces by the weight and slant of the text, as
|
|
938
|
+
* it does for any family. Bold text in a family declared as the one face
|
|
939
|
+
* `local(Arial)` is set in Arial's bold, where a browser emboldens the
|
|
940
|
+
* regular.
|
|
941
|
+
*/
|
|
942
|
+
function localFamily(
|
|
943
|
+
app: object,
|
|
944
|
+
name: string,
|
|
945
|
+
rule: FontFaceRule,
|
|
946
|
+
): string | null {
|
|
947
|
+
const fonts = (app as { fonts?: FontLookup }).fonts;
|
|
948
|
+
if (typeof fonts?.match !== 'function') return null;
|
|
949
|
+
const [lo, hi] = rule.weight;
|
|
950
|
+
try {
|
|
951
|
+
const face = fonts.match(name, {
|
|
952
|
+
weight: Math.max(lo, Math.min(400, hi)),
|
|
953
|
+
style: rule.style,
|
|
954
|
+
});
|
|
955
|
+
const family = face?.familyName;
|
|
956
|
+
if (!family) return null;
|
|
957
|
+
const want = name.toLowerCase();
|
|
958
|
+
return family.toLowerCase() === want ||
|
|
959
|
+
face.postscriptName?.toLowerCase() === want
|
|
960
|
+
? family
|
|
961
|
+
: null;
|
|
962
|
+
} catch {
|
|
963
|
+
// no fontconfig, or a name it could not read: not found
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
683
966
|
}
|
|
684
967
|
|
|
685
968
|
function rangeKey(ranges: [number, number][] | null): string {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A text engine that sets a variable web font at the weight CSS says.
|
|
2
|
+
//
|
|
3
|
+
// A style's weight picks a face, and where the face is a variable font it is
|
|
4
|
+
// also a place on the font's `wght` axis — the style's weight, clamped to
|
|
5
|
+
// the range the face's `@font-face` rule declares (CSS Fonts 4, 7.2). The
|
|
6
|
+
// rule is the document's and not the engine's, so which value that is is
|
|
7
|
+
// the document's to say (`WebFonts.wght`), and it is said with the run: a
|
|
8
|
+
// run's `variations`, which both engines take, and which wins over whatever
|
|
9
|
+
// an engine would have done with the weight by itself. ntk moves the axis
|
|
10
|
+
// to the style's weight, past a rule that declared less of it; CoreText,
|
|
11
|
+
// handed a face react-x11 registered, does not move it at all, and every
|
|
12
|
+
// weight of a page set in Geist was the regular on macOS.
|
|
13
|
+
//
|
|
14
|
+
// Only a run whose list leads with such a face is touched, and it is handed
|
|
15
|
+
// on as a copy: the run is the caller's, and the layouts kept from one pass
|
|
16
|
+
// to the next are found by its fields (`TextLayoutCache`), which this is
|
|
17
|
+
// below and adds nothing to. A run in any other family carries no
|
|
18
|
+
// `variations` — an axis value set on the system's own variable font would
|
|
19
|
+
// be this document's rule applied to a font it never declared.
|
|
20
|
+
import type { TextRun } from '../../richtext/index.js';
|
|
21
|
+
import type { FontsLike } from './inline.js';
|
|
22
|
+
import { withLayout } from './shaping.js';
|
|
23
|
+
|
|
24
|
+
/** What says where on its weight axis text is set: `WebFonts`. */
|
|
25
|
+
export interface WeightAxes {
|
|
26
|
+
/** Whether any family has an axis to set. While none has, the engine is
|
|
27
|
+
* called as it is. */
|
|
28
|
+
readonly variable: boolean;
|
|
29
|
+
wght(list: string, weight: number, italic: boolean): { wght: number } | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NUMERIC: Record<string, number> = { normal: 400, bold: 700 };
|
|
33
|
+
|
|
34
|
+
/** `engine`, laying each run out at the weight axis value `axes` gives its
|
|
35
|
+
* family list, weight and slant. */
|
|
36
|
+
export function weightAxes(engine: FontsLike, axes: WeightAxes): FontsLike {
|
|
37
|
+
return withLayout(engine, (content, style, options) => {
|
|
38
|
+
if (!axes.variable || !Array.isArray(content)) {
|
|
39
|
+
return engine.layout(content, style, options);
|
|
40
|
+
}
|
|
41
|
+
let runs: TextRun[] | null = null;
|
|
42
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
43
|
+
const run = content[i];
|
|
44
|
+
const family = run.family ?? style.family;
|
|
45
|
+
const weight = run.weight ?? style.weight ?? 400;
|
|
46
|
+
const axis =
|
|
47
|
+
typeof family === 'string'
|
|
48
|
+
? axes.wght(
|
|
49
|
+
family,
|
|
50
|
+
typeof weight === 'number'
|
|
51
|
+
? weight
|
|
52
|
+
: (NUMERIC[weight as string] ?? 400),
|
|
53
|
+
(run.style ?? style.style) === 'italic',
|
|
54
|
+
)
|
|
55
|
+
: null;
|
|
56
|
+
if (!axis) continue;
|
|
57
|
+
runs ??= content.slice();
|
|
58
|
+
runs[i] = { ...run, variations: axis } as TextRun;
|
|
59
|
+
}
|
|
60
|
+
return engine.layout(runs ?? content, style, options);
|
|
61
|
+
});
|
|
62
|
+
}
|