ntk 8.17.6 → 8.17.7
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/text/fontmanager.js +23 -8
- package/lib/widgets/svgview.js +99 -8
- package/package.json +1 -1
package/lib/text/fontmanager.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { weightRank } from '../fontconfig.js';
|
|
1
2
|
import Font from './font.js';
|
|
2
3
|
import {
|
|
3
4
|
createFontSource,
|
|
@@ -227,7 +228,7 @@ export default class FontManager {
|
|
|
227
228
|
this._matches = new Map(); // family|weight|style -> Font
|
|
228
229
|
this._lastMatch = null; // the last of them asked for (`match`)
|
|
229
230
|
this._fallbacks = new Map(); // family|weight|style -> Map(codepoint -> Font|null)
|
|
230
|
-
this._registered = []; // { font, family (lowercase), weight, italic }
|
|
231
|
+
this._registered = []; // { font, family (lowercase), weight: [lo, hi], italic }
|
|
231
232
|
// word-level shaping memo, in two generations (`_shapeCached`): each a
|
|
232
233
|
// map from a style's key to the words shaped in it
|
|
233
234
|
this._shapeCache = new Map();
|
|
@@ -319,7 +320,9 @@ export default class FontManager {
|
|
|
319
320
|
* @param {string|Uint8Array|Buffer} source path to a .ttf/.otf/.woff/.ttc
|
|
320
321
|
* file, or the font file's bytes (environments without a filesystem)
|
|
321
322
|
* @param {object} [opts] { postscriptName (face of a .ttc), family (alias),
|
|
322
|
-
* weight, style } — weight/style override the values detected from the
|
|
323
|
+
* weight, style } — weight/style override the values detected from the
|
|
324
|
+
* file. A `weight` is the face's, as @font-face's descriptor is; without
|
|
325
|
+
* one a variable face holds every weight on its `wght` axis.
|
|
323
326
|
* @returns {Font}
|
|
324
327
|
*/
|
|
325
328
|
load(source, opts = {}) {
|
|
@@ -333,10 +336,12 @@ export default class FontManager {
|
|
|
333
336
|
};
|
|
334
337
|
const font = this._open(candidate);
|
|
335
338
|
const detected = detectStyle(font);
|
|
339
|
+
const axis = font.variationAxes.wght;
|
|
340
|
+
const weight = opts.weight !== undefined ? numWeight(opts.weight) : detected.weight;
|
|
336
341
|
this._registered.push({
|
|
337
342
|
font,
|
|
338
343
|
family: (opts.family || font.familyName || '').toLowerCase(),
|
|
339
|
-
weight: opts.weight
|
|
344
|
+
weight: axis && opts.weight === undefined ? [axis.min, axis.max] : [weight, weight],
|
|
340
345
|
italic: typeof opts.style === 'string' ? opts.style.includes('italic') : detected.italic
|
|
341
346
|
});
|
|
342
347
|
this._matches.clear();
|
|
@@ -347,16 +352,26 @@ export default class FontManager {
|
|
|
347
352
|
return font;
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The registered face of the first of `families` to have one, as the
|
|
357
|
+
* sources pick: the style asked for, then the weight CSS's font matching
|
|
358
|
+
* picks (CSS Fonts 4 §5.2) — up to 500 and then lighter for 400 to 500,
|
|
359
|
+
* lighter first below 400, heavier first above 500 — and of faces alike in
|
|
360
|
+
* both, the one loaded first.
|
|
361
|
+
*/
|
|
350
362
|
_matchRegistered(families, weight, italic) {
|
|
351
|
-
let best = null;
|
|
352
|
-
let bestScore = Infinity;
|
|
353
363
|
for (const family of families) {
|
|
364
|
+
let best = null;
|
|
365
|
+
let bestStyle = Infinity;
|
|
366
|
+
let bestWeight = Infinity;
|
|
354
367
|
for (const r of this._registered) {
|
|
355
368
|
if (r.family !== family || !r.font.drawable) continue;
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
369
|
+
const style = r.italic !== italic ? 1 : 0;
|
|
370
|
+
const rank = weightRank(weight, r.weight, 400, 500);
|
|
371
|
+
if (style < bestStyle || (style === bestStyle && rank < bestWeight)) {
|
|
359
372
|
best = r.font;
|
|
373
|
+
bestStyle = style;
|
|
374
|
+
bestWeight = rank;
|
|
360
375
|
}
|
|
361
376
|
}
|
|
362
377
|
if (best) return best; // earlier families in the list win
|
package/lib/widgets/svgview.js
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
// view.draw(ctx, x, y, width, height);
|
|
16
16
|
//
|
|
17
17
|
// Supported: path/rect/circle/ellipse/line/polyline/polygon, <g>, <defs>,
|
|
18
|
-
// <use>, linear/radial gradients (objectBoundingBox and
|
|
19
|
-
// transform lists, opacity/fill-opacity/stroke-opacity,
|
|
20
|
-
// nested <svg> viewports, fill-rule, basic <text>. See
|
|
21
|
-
// full surface and limitations.
|
|
18
|
+
// <use>, <switch>, linear/radial gradients (objectBoundingBox and
|
|
19
|
+
// userSpaceOnUse), transform lists, opacity/fill-opacity/stroke-opacity,
|
|
20
|
+
// display, visibility, nested <svg> viewports, fill-rule, basic <text>. See
|
|
21
|
+
// docs/svg.md for the full surface and limitations.
|
|
22
22
|
|
|
23
23
|
import { textContent } from 'domutils';
|
|
24
24
|
import { parseDocument } from 'htmlparser2';
|
|
@@ -114,6 +114,79 @@ function displayNone(node) {
|
|
|
114
114
|
return ownProperty(node, 'display', keyword) === 'none';
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// SVG 1.1 features whose elements SvgView draws nothing of: a branch of a
|
|
118
|
+
// <switch> that needs one would come out empty or wrong here, so the
|
|
119
|
+
// author's fallback is what to draw instead
|
|
120
|
+
const UNDRAWN_FEATURES = new Set(
|
|
121
|
+
[
|
|
122
|
+
'Extensibility', 'Image', 'Clip', 'BasicClip', 'Mask', 'Filter', 'BasicFilter',
|
|
123
|
+
'Pattern', 'Marker', 'Font', 'BasicFont', 'Script', 'Animation', 'ColorProfile',
|
|
124
|
+
'Cursor', 'View'
|
|
125
|
+
].map((name) => `http://www.w3.org/TR/SVG11/feature#${name}`)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
let defaultLanguages = null;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The languages a document's `systemLanguage` is matched against where the
|
|
132
|
+
* caller names none: the browser's, or the locale the runtime runs in, each
|
|
133
|
+
* followed by its language alone — `en-AU`, then `en` — as a browser's
|
|
134
|
+
* language settings suggest (SVG 1.1, 5.8.5). Worked out once.
|
|
135
|
+
*/
|
|
136
|
+
function userLanguages() {
|
|
137
|
+
if (defaultLanguages) return defaultLanguages;
|
|
138
|
+
const nav = globalThis.navigator?.languages;
|
|
139
|
+
const tags = nav?.length ? [...nav] : [new Intl.DateTimeFormat().resolvedOptions().locale];
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const t of tags) {
|
|
142
|
+
for (const v of [t, t.split('-')[0]]) {
|
|
143
|
+
const lower = v.toLowerCase();
|
|
144
|
+
if (lower && !out.includes(lower)) out.push(lower);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return (defaultLanguages = out);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether an element's conditional processing attributes hold (SVG 1.1,
|
|
152
|
+
* 5.8): where one does not, the element and all of it are not rendered, and
|
|
153
|
+
* a `<switch>` passes over it to the next. One it does not carry holds.
|
|
154
|
+
*
|
|
155
|
+
* - `requiredExtensions` never holds, as SvgView supports no extension; an
|
|
156
|
+
* empty one does not either.
|
|
157
|
+
* - `requiredFeatures` holds unless it names a feature SvgView draws nothing
|
|
158
|
+
* of. SVG 2 dropped the attribute and browsers hold every one, but the
|
|
159
|
+
* one they would pick, a `foreignObject`, draws nothing here, and the
|
|
160
|
+
* `<text>` an exporter writes after it for renderers like this one does.
|
|
161
|
+
* - `systemLanguage` holds where one of its comma-separated tags is one of
|
|
162
|
+
* `languages`, or that narrowed by a subtag, or the other way round.
|
|
163
|
+
*/
|
|
164
|
+
function conditionsHold(node, languages) {
|
|
165
|
+
if (!node.attribs) return true;
|
|
166
|
+
if (attr(node, 'requiredExtensions') !== undefined) return false;
|
|
167
|
+
const features = attr(node, 'requiredFeatures');
|
|
168
|
+
if (features !== undefined && features.split(/\s+/).some((f) => UNDRAWN_FEATURES.has(f))) return false;
|
|
169
|
+
const system = attr(node, 'systemLanguage');
|
|
170
|
+
if (system !== undefined) {
|
|
171
|
+
const tags = system.split(',').map((t) => t.trim().toLowerCase());
|
|
172
|
+
const matches = (t, u) => t === u || t.startsWith(`${u}-`) || u.startsWith(`${t}-`);
|
|
173
|
+
if (!tags.some((t) => t && languages.some((u) => matches(t, u)))) return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* What a `<switch>` renders: its first child element whose conditions hold
|
|
180
|
+
* (SVG 1.1, 5.8.2), whatever that is — a `display: none` one is chosen, and
|
|
181
|
+
* draws nothing — or null where none does.
|
|
182
|
+
*/
|
|
183
|
+
function switchChoice(node, languages) {
|
|
184
|
+
for (const child of node.children || []) {
|
|
185
|
+
if (child.type === 'tag' && conditionsHold(child, languages)) return child;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
117
190
|
/**
|
|
118
191
|
* An `<alpha-value>` — `opacity`, `fill-opacity`, `stroke-opacity`: a number
|
|
119
192
|
* or a percentage, clamped to 0..1 (CSS Color 4, 5.2). `undefined` for
|
|
@@ -369,7 +442,7 @@ function resolveStyle(node, parent) {
|
|
|
369
442
|
* paint a `mono` document uses — a colour, or the literal `'currentColor'`
|
|
370
443
|
* when the document defers to its caller — and null when nothing paints.
|
|
371
444
|
*/
|
|
372
|
-
function scanPaints(root, ids) {
|
|
445
|
+
function scanPaints(root, ids, languages) {
|
|
373
446
|
const paints = new Set();
|
|
374
447
|
let multi = false;
|
|
375
448
|
|
|
@@ -388,7 +461,7 @@ function scanPaints(root, ids) {
|
|
|
388
461
|
const visit = (node, parent, depth) => {
|
|
389
462
|
if (multi || depth > 32) return;
|
|
390
463
|
const name = tag(node);
|
|
391
|
-
if (NON_RENDERED.has(name) || displayNone(node)) return;
|
|
464
|
+
if (NON_RENDERED.has(name) || displayNone(node) || !conditionsHold(node, languages)) return;
|
|
392
465
|
const style = resolveStyle(node, parent);
|
|
393
466
|
switch (name) {
|
|
394
467
|
case 'svg':
|
|
@@ -396,6 +469,11 @@ function scanPaints(root, ids) {
|
|
|
396
469
|
case 'a':
|
|
397
470
|
kids(node, style, depth);
|
|
398
471
|
return;
|
|
472
|
+
case 'switch': {
|
|
473
|
+
const chosen = switchChoice(node, languages);
|
|
474
|
+
if (chosen) visit(chosen, style, depth + 1);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
399
477
|
case 'use': {
|
|
400
478
|
const href = node.attribs?.href || node.attribs?.['xlink:href'] || '';
|
|
401
479
|
const target = href.startsWith('#') ? ids.get(href.slice(1)) : null;
|
|
@@ -438,6 +516,12 @@ export default class SvgView {
|
|
|
438
516
|
* overrides it; both fall back to the CSS initial value, black.
|
|
439
517
|
*/
|
|
440
518
|
this.color = opts.color ?? INHERITED.color;
|
|
519
|
+
/**
|
|
520
|
+
* The languages `systemLanguage` is matched against, most preferred
|
|
521
|
+
* first, like `navigator.languages`. Read when a document is adopted, for
|
|
522
|
+
* `paintKind`, and on every draw.
|
|
523
|
+
*/
|
|
524
|
+
this.languages = (opts.languages ?? userLanguages()).map((t) => String(t).toLowerCase());
|
|
441
525
|
|
|
442
526
|
this._root = null;
|
|
443
527
|
this._ids = new Map();
|
|
@@ -494,7 +578,7 @@ export default class SvgView {
|
|
|
494
578
|
};
|
|
495
579
|
collect(this._root);
|
|
496
580
|
|
|
497
|
-
const scanned = scanPaints(this._root, this._ids);
|
|
581
|
+
const scanned = scanPaints(this._root, this._ids, this.languages);
|
|
498
582
|
this.paintKind = scanned.kind;
|
|
499
583
|
this.soloPaint = scanned.solo;
|
|
500
584
|
|
|
@@ -738,7 +822,7 @@ export default class SvgView {
|
|
|
738
822
|
_renderNode(node, ctx, parentStyle, alpha, depth) {
|
|
739
823
|
if (depth > 32) return;
|
|
740
824
|
const name = tag(node);
|
|
741
|
-
if (NON_RENDERED.has(name) || displayNone(node)) return;
|
|
825
|
+
if (NON_RENDERED.has(name) || displayNone(node) || !conditionsHold(node, this.languages)) return;
|
|
742
826
|
|
|
743
827
|
const style = this._style(node, parentStyle);
|
|
744
828
|
// not inherited: a group's multiplies down through what is in it
|
|
@@ -760,6 +844,13 @@ export default class SvgView {
|
|
|
760
844
|
case 'a':
|
|
761
845
|
this._renderChildren(node, ctx, style, nodeAlpha, depth + 1);
|
|
762
846
|
break;
|
|
847
|
+
case 'switch': {
|
|
848
|
+
// a group of one: its transform and opacity are a group's, and it
|
|
849
|
+
// draws the first child whose conditions hold and none of the rest
|
|
850
|
+
const chosen = switchChoice(node, this.languages);
|
|
851
|
+
if (chosen) this._renderNode(chosen, ctx, style, nodeAlpha, depth + 1);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
763
854
|
case 'use': {
|
|
764
855
|
const href = node.attribs?.href || node.attribs?.['xlink:href'] || '';
|
|
765
856
|
const target = href.startsWith('#') ? this._ids.get(href.slice(1)) : null;
|