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.
@@ -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 file
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 !== undefined ? numWeight(opts.weight) : detected.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 score = Math.abs(r.weight - weight) + (r.italic !== italic ? 1000 : 0);
357
- if (score < bestScore) {
358
- bestScore = score;
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
@@ -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 userSpaceOnUse),
19
- // transform lists, opacity/fill-opacity/stroke-opacity, display, visibility,
20
- // nested <svg> viewports, fill-rule, basic <text>. See docs/svg.md for the
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.17.6",
3
+ "version": "8.17.7",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",