@react-x11/components 0.12.0 → 0.14.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.
- package/dist/html/controls.d.ts +41 -5
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +85 -57
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +545 -126
- 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 +47 -6
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +336 -30
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +142 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +563 -147
- 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 +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -9
- 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/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +78 -4
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +373 -19
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.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 +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +393 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +90 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +74 -10
- 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 +90 -18
- 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 +574 -84
- 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 +154 -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 +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +90 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +771 -186
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +101 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1502 -160
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +700 -0
- package/dist/html/widgets.js.map +1 -0
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- 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 +2 -2
- package/src/html/controls.ts +114 -51
- package/src/html/css/cascade.ts +614 -140
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +388 -33
- package/src/html/css/style.ts +688 -150
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +77 -10
- package/src/html/css/values.ts +4 -0
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +417 -24
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +447 -37
- package/src/html/layout/boxes.ts +143 -14
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +675 -78
- package/src/html/layout/inline.ts +159 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +798 -172
- package/src/html/paint.ts +1742 -241
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +821 -0
- package/src/html/woff2.ts +612 -0
- package/src/index.ts +1 -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
|
@@ -35,23 +35,74 @@
|
|
|
35
35
|
// keyed by a family list, the text layouts', the metrics', ntk's, that the
|
|
36
36
|
// text has to be set again.
|
|
37
37
|
//
|
|
38
|
+
// **A face the text engine cannot set is not registered.** A face is drawn by
|
|
39
|
+
// the engine, and an engine can read a file it cannot draw every weight of:
|
|
40
|
+
// ntk cuts an instance out of a variable face for the weight and the size a
|
|
41
|
+
// style asks for, and fontkit, which does the cutting, could not cut one out
|
|
42
|
+
// of a WOFF2 — the container nearly every variable web font is served in.
|
|
43
|
+
// The throw came out of the first text layout in the family, and the
|
|
44
|
+
// document it took down was nextjs.org's blog, for Geist. So a face is asked
|
|
45
|
+
// for an instance once, before it is registered (`refusal`), and one the
|
|
46
|
+
// engine refuses is a source that did not load: the next is tried, and with
|
|
47
|
+
// none left the family stays out of the list and the text is set in the
|
|
48
|
+
// next one, as a browser sets it when it cannot use a font.
|
|
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
|
+
//
|
|
38
70
|
// A family split by `unicode-range` is registered a name per range, since the
|
|
39
71
|
// font manager picks among one name's faces by weight and slant alone, and
|
|
40
72
|
// the range that holds the most of the document's characters goes first: the
|
|
41
73
|
// first family is the one a run is set in, and the others are reached through
|
|
42
74
|
// the fallback for the characters it lacks.
|
|
43
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
|
+
//
|
|
44
94
|
// What stays the application's: registering a face adds it to ntk's
|
|
45
95
|
// fallback chain, where it can supply a glyph no other face has to text
|
|
46
96
|
// anywhere in the app (react-x11's `loadFont` says so), and nothing is ever
|
|
47
97
|
// unregistered — the font manager has no way to. A session that visits many
|
|
48
98
|
// sites keeps their faces, one registration per distinct declaration.
|
|
49
|
-
import { loadFont } from 'react-x11';
|
|
99
|
+
import { loadFont, openFont } from 'react-x11';
|
|
50
100
|
import type { Element } from 'domhandler';
|
|
51
101
|
|
|
52
102
|
import type { FontFaceRule } from './css/parse.js';
|
|
53
103
|
import type { ComputedStyle } from './css/style.js';
|
|
54
104
|
import type { ResourceRequest, ResourceResult } from './resources.js';
|
|
105
|
+
import { isWoff2, sfntFromWoff2 } from './woff2.js';
|
|
55
106
|
|
|
56
107
|
/** What the cascade asks of the document's families (`Cascade`). */
|
|
57
108
|
export interface FontFamilies {
|
|
@@ -96,6 +147,13 @@ interface Face {
|
|
|
96
147
|
family: Family;
|
|
97
148
|
/** Where loading got to. `idle` has not been asked for. */
|
|
98
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;
|
|
99
157
|
}
|
|
100
158
|
|
|
101
159
|
/** A family's faces for one unicode range: one registered name, among whose
|
|
@@ -123,9 +181,17 @@ interface Registry {
|
|
|
123
181
|
names: Map<string, string>;
|
|
124
182
|
/** Faces registered, by `faceKey`. */
|
|
125
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]>;
|
|
126
190
|
/** Faces being loaded, by `faceKey`, so a second document waits for the
|
|
127
191
|
* first document's request rather than making its own. */
|
|
128
192
|
pending: Map<string, Promise<boolean>>;
|
|
193
|
+
/** Whether a face the engine refused has been said (`warnRefused`). */
|
|
194
|
+
warned: boolean;
|
|
129
195
|
}
|
|
130
196
|
|
|
131
197
|
const REGISTRIES = new WeakMap<object, Registry>();
|
|
@@ -137,7 +203,10 @@ function registryOf(app: object): Registry {
|
|
|
137
203
|
next: 0,
|
|
138
204
|
names: new Map(),
|
|
139
205
|
ready: new Set(),
|
|
206
|
+
locals: new Map(),
|
|
207
|
+
axes: new Map(),
|
|
140
208
|
pending: new Map(),
|
|
209
|
+
warned: false,
|
|
141
210
|
};
|
|
142
211
|
REGISTRIES.set(app, registry);
|
|
143
212
|
}
|
|
@@ -172,6 +241,14 @@ export class WebFonts implements FontFamilies {
|
|
|
172
241
|
* lists that come to one string share an entry, which can ask for a face
|
|
173
242
|
* a little early; never for one nothing wants. */
|
|
174
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;
|
|
175
252
|
/** The document's characters, as the last `request` saw them. */
|
|
176
253
|
private _text: string | null = null;
|
|
177
254
|
private _seen = new Set<number>();
|
|
@@ -254,14 +331,17 @@ export class WebFonts implements FontFamilies {
|
|
|
254
331
|
order: order++,
|
|
255
332
|
};
|
|
256
333
|
for (const m of members) {
|
|
257
|
-
const ready = registry?.ready.has(faceKey(name, m.rule)) ?? false;
|
|
258
334
|
const face: Face = {
|
|
259
335
|
rule: m.rule,
|
|
260
336
|
element: m.element,
|
|
261
337
|
group,
|
|
262
338
|
family,
|
|
263
|
-
state:
|
|
339
|
+
state: 'idle',
|
|
340
|
+
local: null,
|
|
341
|
+
wght: null,
|
|
264
342
|
};
|
|
343
|
+
if (registry?.ready.has(faceKey(name, m.rule)))
|
|
344
|
+
arrive(face, registry);
|
|
265
345
|
group.faces.push(face);
|
|
266
346
|
made.set(m, face);
|
|
267
347
|
}
|
|
@@ -270,6 +350,10 @@ export class WebFonts implements FontFamilies {
|
|
|
270
350
|
families.set(key, family);
|
|
271
351
|
}
|
|
272
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
|
+
}
|
|
273
357
|
this._faces = declared.map((d) => made.get(d)!);
|
|
274
358
|
// the ranges are new, and so is what they cover
|
|
275
359
|
if (this._text !== null) this._measure(this._text, true);
|
|
@@ -292,7 +376,15 @@ export class WebFonts implements FontFamilies {
|
|
|
292
376
|
}
|
|
293
377
|
used.push(family);
|
|
294
378
|
for (const group of orderedGroups(family)) {
|
|
295
|
-
|
|
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
|
+
}
|
|
296
388
|
}
|
|
297
389
|
}
|
|
298
390
|
let mapped = out.length ? out.join(', ') : this._fallback;
|
|
@@ -322,6 +414,64 @@ export class WebFonts implements FontFamilies {
|
|
|
322
414
|
for (const family of families) family.wants.add(want);
|
|
323
415
|
}
|
|
324
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
|
+
|
|
325
475
|
/**
|
|
326
476
|
* Ask for the faces the styles noted since the last call want, for the
|
|
327
477
|
* characters `text` holds — the document's own. True when the lists the
|
|
@@ -351,6 +501,7 @@ export class WebFonts implements FontFamilies {
|
|
|
351
501
|
destroy(): void {
|
|
352
502
|
this._destroyed = true;
|
|
353
503
|
this._families.clear();
|
|
504
|
+
this._groups.clear();
|
|
354
505
|
this._forget();
|
|
355
506
|
}
|
|
356
507
|
|
|
@@ -359,6 +510,8 @@ export class WebFonts implements FontFamilies {
|
|
|
359
510
|
private _forget(): void {
|
|
360
511
|
this._memo.clear();
|
|
361
512
|
this._sources.clear();
|
|
513
|
+
this._axes.clear();
|
|
514
|
+
this._variable = null;
|
|
362
515
|
}
|
|
363
516
|
|
|
364
517
|
/**
|
|
@@ -451,7 +604,7 @@ export class WebFonts implements FontFamilies {
|
|
|
451
604
|
const registry = registryOf(app);
|
|
452
605
|
const key = faceKey(face.group.name, face.rule);
|
|
453
606
|
if (registry.ready.has(key)) {
|
|
454
|
-
face
|
|
607
|
+
arrive(face, registry);
|
|
455
608
|
return 'ready';
|
|
456
609
|
}
|
|
457
610
|
face.state = 'loading';
|
|
@@ -472,7 +625,7 @@ export class WebFonts implements FontFamilies {
|
|
|
472
625
|
};
|
|
473
626
|
|
|
474
627
|
const sources = face.rule.sources.filter(
|
|
475
|
-
(s) => s.format === null || READABLE.has(s.format),
|
|
628
|
+
(s) => 'local' in s || s.format === null || READABLE.has(s.format),
|
|
476
629
|
);
|
|
477
630
|
let i = 0;
|
|
478
631
|
let sync = true;
|
|
@@ -481,6 +634,18 @@ export class WebFonts implements FontFamilies {
|
|
|
481
634
|
const next = (): 'ready' | 'waiting' | 'none' => {
|
|
482
635
|
while (i < sources.length) {
|
|
483
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
|
+
}
|
|
484
649
|
let answer: ReturnType<Ask>;
|
|
485
650
|
try {
|
|
486
651
|
answer = this._ask({
|
|
@@ -492,15 +657,20 @@ export class WebFonts implements FontFamilies {
|
|
|
492
657
|
continue;
|
|
493
658
|
}
|
|
494
659
|
if (isPromise(answer)) {
|
|
495
|
-
answer
|
|
496
|
-
(result) =>
|
|
497
|
-
() => after(false)
|
|
498
|
-
);
|
|
660
|
+
answer
|
|
661
|
+
.then((result) => this._register(face, result, source.url))
|
|
662
|
+
.then(after, () => after(false));
|
|
499
663
|
return 'waiting';
|
|
500
664
|
}
|
|
501
|
-
|
|
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));
|
|
669
|
+
return 'waiting';
|
|
670
|
+
}
|
|
671
|
+
if (registered) {
|
|
502
672
|
finish(true);
|
|
503
|
-
if (sync) face
|
|
673
|
+
if (sync) arrive(face, registry);
|
|
504
674
|
else this._settled(face, true);
|
|
505
675
|
return 'ready';
|
|
506
676
|
}
|
|
@@ -526,15 +696,47 @@ export class WebFonts implements FontFamilies {
|
|
|
526
696
|
return outcome === 'ready' ? 'ready' : 'loading';
|
|
527
697
|
}
|
|
528
698
|
|
|
529
|
-
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
|
|
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
|
+
*/
|
|
712
|
+
private _register(
|
|
713
|
+
face: Face,
|
|
714
|
+
result: ResourceResult | null,
|
|
715
|
+
url: string,
|
|
716
|
+
): boolean | Promise<boolean> {
|
|
534
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 {
|
|
535
732
|
const { weight, style } = face.rule;
|
|
733
|
+
const app = this._app as Parameters<typeof loadFont>[0];
|
|
536
734
|
try {
|
|
537
|
-
loadFont
|
|
735
|
+
// `loadFont` opens the file first too, and finds this one opened
|
|
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, {
|
|
538
740
|
family: face.group.name,
|
|
539
741
|
// a range is registered at the weight nearest regular in it: the
|
|
540
742
|
// font manager picks among a group's faces by distance from one
|
|
@@ -542,24 +744,157 @@ export class WebFonts implements FontFamilies {
|
|
|
542
744
|
weight: Math.max(weight[0], Math.min(400, weight[1])),
|
|
543
745
|
style,
|
|
544
746
|
});
|
|
545
|
-
|
|
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;
|
|
546
753
|
} catch {
|
|
547
|
-
return
|
|
754
|
+
return '';
|
|
548
755
|
}
|
|
549
756
|
}
|
|
550
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
|
+
|
|
551
764
|
/** A face arrived, or will not. Either way the boxes are built again: an
|
|
552
765
|
* arrival changes the lists, and a failure has the next face asked for,
|
|
553
766
|
* by a family that has to be asked again to ask it. */
|
|
554
767
|
private _settled(face: Face, ok: boolean): void {
|
|
555
768
|
if (this._destroyed) return;
|
|
556
|
-
|
|
769
|
+
if (ok && this._app) arrive(face, registryOf(this._app));
|
|
770
|
+
else face.state = ok ? 'ready' : 'failed';
|
|
557
771
|
if (!ok) face.family.asked.clear();
|
|
558
772
|
this._forget();
|
|
559
773
|
this._changed();
|
|
560
774
|
}
|
|
561
775
|
}
|
|
562
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
|
+
|
|
815
|
+
/** The slice of an opened face `refusal` reads: react-x11's `Font`. */
|
|
816
|
+
interface OpenedFace {
|
|
817
|
+
variationAxes?: Record<string, { min: number; default: number; max: number }>;
|
|
818
|
+
variation?(settings: Record<string, number>): unknown;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** The axes a style moves without naming one: ntk sets `wght` from the
|
|
822
|
+
* weight and `opsz` from the size (its `docs/fonts.md`), and nothing here
|
|
823
|
+
* hands it a `font-variation-settings`. A face with neither is set as its
|
|
824
|
+
* file has it, whatever else varies in it. */
|
|
825
|
+
const DRIVEN_AXES = ['wght', 'opsz'];
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Why the text engine cannot set text in an opened face, or null when it
|
|
829
|
+
* can: the face is asked for the instance a layout will ask it for, at the
|
|
830
|
+
* far end of each axis a style moves, and the answer is the engine's own.
|
|
831
|
+
* What it cost unasked was the document: ntk instantiates inside `match`,
|
|
832
|
+
* so the throw came out of the first layout in the family, at whatever
|
|
833
|
+
* weight was not the file's default.
|
|
834
|
+
*
|
|
835
|
+
* Only an engine that draws through the face is asked. CoreText and
|
|
836
|
+
* DirectWrite move an axis themselves, and the face react-x11 opens there
|
|
837
|
+
* is fontkit's, for an application to read: what it cannot do says nothing
|
|
838
|
+
* about what they draw.
|
|
839
|
+
*/
|
|
840
|
+
export function refusal(
|
|
841
|
+
app: object,
|
|
842
|
+
font: OpenedFace,
|
|
843
|
+
family: string,
|
|
844
|
+
): string | null {
|
|
845
|
+
const axes = font.variationAxes;
|
|
846
|
+
if (!axes || typeof font.variation !== 'function') return null;
|
|
847
|
+
const settings: Record<string, number> = {};
|
|
848
|
+
for (const tag of DRIVEN_AXES) {
|
|
849
|
+
const axis = axes[tag];
|
|
850
|
+
if (!axis) continue;
|
|
851
|
+
const far = axis.max !== axis.default ? axis.max : axis.min;
|
|
852
|
+
if (far !== axis.default) settings[tag] = far;
|
|
853
|
+
}
|
|
854
|
+
if (!Object.keys(settings).length) return null;
|
|
855
|
+
try {
|
|
856
|
+
font.variation(settings);
|
|
857
|
+
return null;
|
|
858
|
+
} catch (error) {
|
|
859
|
+
return instantiates(app, family)
|
|
860
|
+
? String((error as Error)?.message ?? error)
|
|
861
|
+
: null;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Whether the engine's faces are cut into instances as an opened one is:
|
|
866
|
+
* the face it matches a family with has `variation`, as ntk's has and a
|
|
867
|
+
* CoreText or DirectWrite one has not. An engine that cannot say — no
|
|
868
|
+
* family to match — is taken to, since what is at stake is the document. */
|
|
869
|
+
function instantiates(app: object, family: string): boolean {
|
|
870
|
+
try {
|
|
871
|
+
const fonts = (app as { fonts?: { match?(family: string): unknown } })
|
|
872
|
+
.fonts;
|
|
873
|
+
const face = fonts?.match?.(family) as OpenedFace | null | undefined;
|
|
874
|
+
return !face || typeof face.variation === 'function';
|
|
875
|
+
} catch {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Said once a connection, in development: a page set in its fallback
|
|
881
|
+
* family looks like a font that never loaded, and this says it did. */
|
|
882
|
+
function warnRefused(registry: Registry, url: string, why: string): void {
|
|
883
|
+
if (registry.warned) return;
|
|
884
|
+
registry.warned = true;
|
|
885
|
+
const g = globalThis as {
|
|
886
|
+
process?: { env?: Record<string, string | undefined> };
|
|
887
|
+
console?: { warn(message: string): void };
|
|
888
|
+
};
|
|
889
|
+
if (g.process?.env?.NODE_ENV === 'production') return;
|
|
890
|
+
g.console?.warn(
|
|
891
|
+
`@react-x11/components: <Html> loaded the font at ${url} and the text ` +
|
|
892
|
+
'engine cannot set text in it, so its family is left out and the text ' +
|
|
893
|
+
'is set in the next one — as it is for every face the engine refuses. ' +
|
|
894
|
+
`A variable font served as WOFF2 is the usual cause.\n${why}`,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
563
898
|
/** A face's identity within a registered name. */
|
|
564
899
|
function faceKey(name: string, rule: FontFaceRule): string {
|
|
565
900
|
return `${name}\u0000${faceSignature(rule)}`;
|
|
@@ -567,9 +902,67 @@ function faceKey(name: string, rule: FontFaceRule): string {
|
|
|
567
902
|
|
|
568
903
|
function faceSignature(rule: FontFaceRule): string {
|
|
569
904
|
const range = rangeKey(rule.unicodeRange);
|
|
570
|
-
|
|
571
|
-
'
|
|
572
|
-
)
|
|
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
|
+
}
|
|
573
966
|
}
|
|
574
967
|
|
|
575
968
|
function rangeKey(ranges: [number, number][] | null): string {
|