ntk 8.12.7 → 8.12.8
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/lib/fontconfig.js +36 -2
- package/lib/text/fontmanager.js +29 -10
- package/lib/text/layout.js +4 -1
- package/lib/text/paragraphs.js +23 -0
- package/package.json +1 -1
package/lib/fontconfig.js
CHANGED
|
@@ -16,6 +16,40 @@ function execFileSync(...args) {
|
|
|
16
16
|
return cp.execFileSync(...args);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
let fcMatchFound = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* fc-match's path, looked up once per PATH. Spawned by its bare name, the
|
|
23
|
+
* system searches PATH for it, and on macOS that is `posix_spawnp`, which
|
|
24
|
+
* attempts a spawn in every directory ahead of the one that has it: with
|
|
25
|
+
* fc-match seventh on a developer's PATH, each spawn took 45 ms where the
|
|
26
|
+
* path itself takes 15 — and the synchronous ones stall the first text
|
|
27
|
+
* layout. A directory search here is a few stats. Not found, the bare name
|
|
28
|
+
* is spawned as before, so a missing fontconfig reports exactly as it did.
|
|
29
|
+
*/
|
|
30
|
+
export function fcMatchFile() {
|
|
31
|
+
const p = globalThis.process;
|
|
32
|
+
const path = p?.env?.PATH ?? '';
|
|
33
|
+
if (fcMatchFound !== null && fcMatchFound.path === path) return fcMatchFound.file;
|
|
34
|
+
let file = 'fc-match';
|
|
35
|
+
const fs = p?.platform === 'win32' ? undefined : builtin('node:fs');
|
|
36
|
+
if (fs) {
|
|
37
|
+
for (const dir of path.split(':')) {
|
|
38
|
+
const candidate = `${dir || '.'}/fc-match`;
|
|
39
|
+
try {
|
|
40
|
+
if (!fs.statSync(candidate).isFile()) continue;
|
|
41
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
42
|
+
file = candidate;
|
|
43
|
+
break;
|
|
44
|
+
} catch {
|
|
45
|
+
// not here, or not ours to run: the next directory
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
fcMatchFound = { path, file };
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
const DOCS = 'https://github.com/sidorares/ntk/blob/master/docs/fonts.md#environments-without-fontconfig';
|
|
20
54
|
|
|
21
55
|
/**
|
|
@@ -279,7 +313,7 @@ function runFcMatch(fc) {
|
|
|
279
313
|
|
|
280
314
|
const entry = { promise: null, base: null };
|
|
281
315
|
entry.promise = new Promise((resolve, reject) => {
|
|
282
|
-
cp.execFile(
|
|
316
|
+
cp.execFile(fcMatchFile(), [...fcMatchArgs, fc], fcMatchOpts, (err, out, stderr) => {
|
|
283
317
|
if (inflight.get(fc) === entry) inflight.delete(fc);
|
|
284
318
|
if (!err) return resolve(out);
|
|
285
319
|
// execFile hands stderr to the callback; execFileSync hangs it on the
|
|
@@ -576,7 +610,7 @@ export function matchSortedSync(pattern) {
|
|
|
576
610
|
}
|
|
577
611
|
let out;
|
|
578
612
|
try {
|
|
579
|
-
out = execFileSync(
|
|
613
|
+
out = execFileSync(fcMatchFile(), [...fcMatchArgs, fc], fcMatchOpts);
|
|
580
614
|
} catch (err) {
|
|
581
615
|
throw fcMatchError(err);
|
|
582
616
|
}
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -182,6 +182,27 @@ function instantiate(font, weight, variations, opticalSize) {
|
|
|
182
182
|
* fontconfig, e.g. a StaticFontSource in a browser bundle. `source` also
|
|
183
183
|
* takes a font spec: a path, a directory of faces, or font bytes.
|
|
184
184
|
*/
|
|
185
|
+
/** A CSS family list's names, unquoted. */
|
|
186
|
+
function familiesOf(family) {
|
|
187
|
+
return String(family)
|
|
188
|
+
.split(',')
|
|
189
|
+
.map((f) => f.trim().replace(/^["']|["']$/g, ''))
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The pattern a family and face are matched with — one spelling for both
|
|
195
|
+
* `match` and `fallbackFor`, since a source caches its lists by pattern and
|
|
196
|
+
* the two ask about the same face.
|
|
197
|
+
*/
|
|
198
|
+
function patternOf(family, weight, italic) {
|
|
199
|
+
return {
|
|
200
|
+
family: familiesOf(family).join(','),
|
|
201
|
+
weight,
|
|
202
|
+
style: italic ? 'italic' : 'normal'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
export default class FontManager {
|
|
186
207
|
constructor({ source } = {}) {
|
|
187
208
|
// coerced here rather than in the getter, which is on the match path;
|
|
@@ -304,10 +325,7 @@ export default class FontManager {
|
|
|
304
325
|
return instantiate(face, weight, opts.variations, opticalSizeFor(opts));
|
|
305
326
|
}
|
|
306
327
|
|
|
307
|
-
const families =
|
|
308
|
-
.split(',')
|
|
309
|
-
.map((f) => f.trim().replace(/^["']|["']$/g, ''))
|
|
310
|
-
.filter(Boolean);
|
|
328
|
+
const families = familiesOf(family);
|
|
311
329
|
|
|
312
330
|
face = this._matchRegistered(
|
|
313
331
|
families.map((f) => f.toLowerCase()),
|
|
@@ -316,11 +334,7 @@ export default class FontManager {
|
|
|
316
334
|
);
|
|
317
335
|
if (!face) {
|
|
318
336
|
// sources understand comma-separated family lists natively
|
|
319
|
-
const candidates = this.source.matchSorted(
|
|
320
|
-
family: families.join(','),
|
|
321
|
-
weight,
|
|
322
|
-
style: italic ? 'italic' : 'normal'
|
|
323
|
-
});
|
|
337
|
+
const candidates = this.source.matchSorted(patternOf(family, weight, italic));
|
|
324
338
|
face = this._open(candidates[0]);
|
|
325
339
|
}
|
|
326
340
|
this._matches.set(cacheKey, face);
|
|
@@ -369,7 +383,12 @@ export default class FontManager {
|
|
|
369
383
|
const source = this.source;
|
|
370
384
|
let candidates;
|
|
371
385
|
try {
|
|
372
|
-
|
|
386
|
+
// The pattern `match` asks for this family and face, so the list the
|
|
387
|
+
// match or a prewarm already fetched answers: asked with the span's
|
|
388
|
+
// raw weight — undefined for regular text — it was another pattern,
|
|
389
|
+
// and the first character to need a fallback waited on a fc-match
|
|
390
|
+
// of its own.
|
|
391
|
+
candidates = source.matchSorted(patternOf(family, numWeight(opts.weight), !!opts.style?.includes('italic')));
|
|
373
392
|
} catch (err) {
|
|
374
393
|
// no system fonts to fall back to is an answer, not a crash; a source
|
|
375
394
|
// that failed for any other reason is a real bug and still propagates
|
package/lib/text/layout.js
CHANGED
|
@@ -3,6 +3,7 @@ import LineBreaker from 'linebreak';
|
|
|
3
3
|
import { rasterizePolys } from '../rasterize.js';
|
|
4
4
|
import { drawGlyphRuns } from './glyphs.js';
|
|
5
5
|
import { embeddingLevels, normalizedLevels, reorderRuns } from './shape.js';
|
|
6
|
+
import { worthKeeping } from './paragraphs.js';
|
|
6
7
|
|
|
7
8
|
const WS = new Set([0x20, 0x09, 0xa0]);
|
|
8
9
|
const HARD_BREAKS = /[\n\r\u2028\u2029]+$/;
|
|
@@ -55,7 +56,9 @@ export class TextLayout {
|
|
|
55
56
|
// Everything up to the line fill is the same at every width, and a
|
|
56
57
|
// font manager keeps it (text/paragraphs.js): a relayout at a new width
|
|
57
58
|
// starts at the fill.
|
|
58
|
-
|
|
59
|
+
// A short text skips it (`PARAGRAPH_MIN_CHARS`): a label is cheap to
|
|
60
|
+
// prepare and seldom laid out at another width.
|
|
61
|
+
const kept = worthKeeping(content) ? fonts._paragraphs : undefined;
|
|
59
62
|
const key = kept ? kept.keyOf(content) : null;
|
|
60
63
|
let paragraph = kept?.find(key, options.direction, content, style);
|
|
61
64
|
if (!paragraph) {
|
package/lib/text/paragraphs.js
CHANGED
|
@@ -21,6 +21,29 @@
|
|
|
21
21
|
/** How many characters of paragraphs a generation holds. */
|
|
22
22
|
export const PARAGRAPH_GENERATION = 1 << 19;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The shortest paragraph worth keeping, in code units. Shorter text does not
|
|
26
|
+
* ask the cache at all: a label's preparation is a few microseconds and the
|
|
27
|
+
* lookup and copies about one of them, while a label is seldom laid out at
|
|
28
|
+
* a second width. Kept, a UI's labels mounted a quarter slower and a ticking
|
|
29
|
+
* counter redrew a quarter slower, to save nothing; the paragraphs a resize
|
|
30
|
+
* lays out again are a document's, and those are longer than this.
|
|
31
|
+
*/
|
|
32
|
+
export const PARAGRAPH_MIN_CHARS = 64;
|
|
33
|
+
|
|
34
|
+
/** Whether `content` is long enough to keep (`PARAGRAPH_MIN_CHARS`), counted
|
|
35
|
+
* without building anything. */
|
|
36
|
+
export function worthKeeping(content) {
|
|
37
|
+
if (typeof content === 'string') return content.length >= PARAGRAPH_MIN_CHARS;
|
|
38
|
+
let chars = 0;
|
|
39
|
+
for (const span of content) {
|
|
40
|
+
const text = span.text;
|
|
41
|
+
chars += typeof text === 'string' ? text.length : String(text ?? '').length;
|
|
42
|
+
if (chars >= PARAGRAPH_MIN_CHARS) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
export class ParagraphCache {
|
|
25
48
|
constructor(limit = PARAGRAPH_GENERATION) {
|
|
26
49
|
this._limit = limit;
|