ntk 8.24.0 → 8.26.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/lib/widgets/svgview.js +146 -11
- package/package.json +2 -2
package/lib/widgets/svgview.js
CHANGED
|
@@ -33,6 +33,8 @@ const INHERITED = {
|
|
|
33
33
|
lineCap: 'butt',
|
|
34
34
|
lineJoin: 'miter',
|
|
35
35
|
miterLimit: 4,
|
|
36
|
+
dashArray: 'none',
|
|
37
|
+
dashOffset: 0,
|
|
36
38
|
fillRule: 'nonzero',
|
|
37
39
|
clipRule: 'nonzero',
|
|
38
40
|
fillOpacity: 1,
|
|
@@ -61,6 +63,8 @@ const STYLE_ATTRS = {
|
|
|
61
63
|
'stroke-linecap': 'lineCap',
|
|
62
64
|
'stroke-linejoin': 'lineJoin',
|
|
63
65
|
'stroke-miterlimit': 'miterLimit',
|
|
66
|
+
'stroke-dasharray': 'dashArray',
|
|
67
|
+
'stroke-dashoffset': 'dashOffset',
|
|
64
68
|
'fill-rule': 'fillRule',
|
|
65
69
|
'clip-rule': 'clipRule',
|
|
66
70
|
'fill-opacity': 'fillOpacity',
|
|
@@ -175,6 +179,8 @@ function userLanguages() {
|
|
|
175
179
|
return (defaultLanguages = out);
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
const EXTENSIBILITY = 'http://www.w3.org/TR/SVG11/feature#Extensibility';
|
|
183
|
+
|
|
178
184
|
/**
|
|
179
185
|
* Whether an element's conditional processing attributes hold (SVG 1.1,
|
|
180
186
|
* 5.8): where one does not, the element and all of it are not rendered, and
|
|
@@ -186,14 +192,21 @@ function userLanguages() {
|
|
|
186
192
|
* of. SVG 2 dropped the attribute and browsers hold every one, but the
|
|
187
193
|
* one they would pick, a `foreignObject`, draws nothing here, and the
|
|
188
194
|
* `<text>` an exporter writes after it for renderers like this one does.
|
|
195
|
+
* `extensible` is whether a `foreignObject` is drawn — the draw was handed
|
|
196
|
+
* `opts.foreignObject` — and then `#Extensibility` holds.
|
|
189
197
|
* - `systemLanguage` holds where one of its comma-separated tags is one of
|
|
190
198
|
* `languages`, or that narrowed by a subtag, or the other way round.
|
|
191
199
|
*/
|
|
192
|
-
function conditionsHold(node, languages) {
|
|
200
|
+
function conditionsHold(node, languages, extensible = false) {
|
|
193
201
|
if (!node.attribs) return true;
|
|
194
202
|
if (attr(node, 'requiredExtensions') !== undefined) return false;
|
|
195
203
|
const features = attr(node, 'requiredFeatures');
|
|
196
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
features !== undefined &&
|
|
206
|
+
features.split(/\s+/).some((f) => UNDRAWN_FEATURES.has(f) && !(extensible && f === EXTENSIBILITY))
|
|
207
|
+
) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
197
210
|
const system = attr(node, 'systemLanguage');
|
|
198
211
|
if (system !== undefined) {
|
|
199
212
|
const tags = system.split(',').map((t) => t.trim().toLowerCase());
|
|
@@ -208,9 +221,9 @@ function conditionsHold(node, languages) {
|
|
|
208
221
|
* (SVG 1.1, 5.8.2), whatever that is — a `display: none` one is chosen, and
|
|
209
222
|
* draws nothing — or null where none does.
|
|
210
223
|
*/
|
|
211
|
-
function switchChoice(node, languages) {
|
|
224
|
+
function switchChoice(node, languages, extensible = false) {
|
|
212
225
|
for (const child of node.children || []) {
|
|
213
|
-
if (child.type === 'tag' && conditionsHold(child, languages)) return child;
|
|
226
|
+
if (child.type === 'tag' && conditionsHold(child, languages, extensible)) return child;
|
|
214
227
|
}
|
|
215
228
|
return null;
|
|
216
229
|
}
|
|
@@ -250,6 +263,24 @@ function viewportLength(node, name, size, fallback) {
|
|
|
250
263
|
return v.endsWith('%') ? (n / 100) * size : n;
|
|
251
264
|
}
|
|
252
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Where a `<foreignObject>` is in the user space it is drawn in (SVG 2,
|
|
268
|
+
* 23.2): its `x` and `y`, 0 where unset, and its `width` and `height`, 0
|
|
269
|
+
* where unset or `auto` — which draws nothing — each a length or a
|
|
270
|
+
* percentage of the viewport, `[width, height]`. A negative size is as
|
|
271
|
+
* none. Its bounding box, which `objectBoundingBox` units are of.
|
|
272
|
+
*/
|
|
273
|
+
function foreignBox(node, [pw, ph]) {
|
|
274
|
+
const width = viewportLength(node, 'width', pw, 0);
|
|
275
|
+
const height = viewportLength(node, 'height', ph, 0);
|
|
276
|
+
return {
|
|
277
|
+
x: viewportLength(node, 'x', pw, 0),
|
|
278
|
+
y: viewportLength(node, 'y', ph, 0),
|
|
279
|
+
width: width > 0 ? width : 0,
|
|
280
|
+
height: height > 0 ? height : 0
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
253
284
|
const ALIGN = /^x(Min|Mid|Max)Y(Min|Mid|Max)$/;
|
|
254
285
|
const ALONG = { Min: 0, Mid: 0.5, Max: 1 };
|
|
255
286
|
|
|
@@ -578,6 +609,12 @@ function resolveStyle(node, parent) {
|
|
|
578
609
|
case 'textAnchor':
|
|
579
610
|
value = v.toLowerCase();
|
|
580
611
|
break;
|
|
612
|
+
case 'dashArray':
|
|
613
|
+
value = dashArrayValue(v);
|
|
614
|
+
break;
|
|
615
|
+
case 'dashOffset':
|
|
616
|
+
value = dashLength(v, true) ?? undefined;
|
|
617
|
+
break;
|
|
581
618
|
case 'markerStart':
|
|
582
619
|
case 'markerMid':
|
|
583
620
|
case 'markerEnd':
|
|
@@ -596,6 +633,42 @@ function resolveStyle(node, parent) {
|
|
|
596
633
|
return style;
|
|
597
634
|
}
|
|
598
635
|
|
|
636
|
+
/**
|
|
637
|
+
* A length in a dash pattern or its offset, in user units: a number, or one
|
|
638
|
+
* in `px` (SVG 2, 13.5.6). A percentage of the viewport's diagonal, or a
|
|
639
|
+
* length in another unit, is none this reads. A negative one is an error in
|
|
640
|
+
* a pattern, and fine in an offset (`signed`).
|
|
641
|
+
*/
|
|
642
|
+
function dashLength(value, signed = false) {
|
|
643
|
+
const m = /^([-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(px)?$/i.exec(value.trim());
|
|
644
|
+
if (!m) return null;
|
|
645
|
+
const n = Number(m[1]);
|
|
646
|
+
if (!Number.isFinite(n) || (!signed && n < 0)) return null;
|
|
647
|
+
return n;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* `stroke-dasharray` as `setLineDash` takes it (SVG 2, 13.5.5): `none`, or
|
|
652
|
+
* its lengths, split on commas and white space. One that is not a length,
|
|
653
|
+
* or is negative, makes the value an error, which leaves the property as it
|
|
654
|
+
* was (undefined); lengths that come to nothing draw the stroke solid, as
|
|
655
|
+
* `none` does. An odd count is repeated by `setLineDash`, as the property
|
|
656
|
+
* has it.
|
|
657
|
+
*/
|
|
658
|
+
function dashArrayValue(value) {
|
|
659
|
+
const v = value.trim();
|
|
660
|
+
if (v.toLowerCase() === 'none') return 'none';
|
|
661
|
+
const parts = v.split(/[\s,]+/).filter(Boolean);
|
|
662
|
+
if (!parts.length) return undefined;
|
|
663
|
+
const out = [];
|
|
664
|
+
for (const part of parts) {
|
|
665
|
+
const n = dashLength(part);
|
|
666
|
+
if (n === null) return undefined;
|
|
667
|
+
out.push(n);
|
|
668
|
+
}
|
|
669
|
+
return out.some((n) => n > 0) ? out : 'none';
|
|
670
|
+
}
|
|
671
|
+
|
|
599
672
|
/**
|
|
600
673
|
* How many distinct colours a document actually commits to, decided once at
|
|
601
674
|
* parse time.
|
|
@@ -658,6 +731,10 @@ function scanPaints(root, ids, languages) {
|
|
|
658
731
|
if (chosen) visit(chosen, style, depth + 1);
|
|
659
732
|
return;
|
|
660
733
|
}
|
|
734
|
+
case 'foreignobject':
|
|
735
|
+
// it paints nothing of its own: what is in it is the caller's to
|
|
736
|
+
// draw (`opts.foreignObject`), in the caller's colours
|
|
737
|
+
return;
|
|
661
738
|
case 'use': {
|
|
662
739
|
const href = node.attribs?.href || node.attribs?.['xlink:href'] || '';
|
|
663
740
|
const target = href.startsWith('#') ? ids.get(href.slice(1)) : null;
|
|
@@ -736,6 +813,10 @@ export default class SvgView {
|
|
|
736
813
|
this._root = null;
|
|
737
814
|
this._ids = new Map();
|
|
738
815
|
this._viewport = [0, 0];
|
|
816
|
+
/** what draws a `<foreignObject>` in this draw (`opts.foreignObject`),
|
|
817
|
+
* and whether there is one, which `#Extensibility` holds where */
|
|
818
|
+
this._foreignObject = null;
|
|
819
|
+
this._extensible = false;
|
|
739
820
|
this.naturalWidth = 0;
|
|
740
821
|
this.naturalHeight = 0;
|
|
741
822
|
this.viewBox = null;
|
|
@@ -851,9 +932,23 @@ export default class SvgView {
|
|
|
851
932
|
* user units: an inline `<svg>` takes its font from the page around it,
|
|
852
933
|
* as it does in a browser, where on its own it starts from 16px
|
|
853
934
|
* sans-serif.
|
|
935
|
+
*
|
|
936
|
+
* `opts.foreignObject(element, ctx, box)` draws what a `<foreignObject>`
|
|
937
|
+
* holds, which is the caller's — HTML, in a host document's inline
|
|
938
|
+
* `<svg>` — where the drawing reaches the object in its order: over what
|
|
939
|
+
* is drawn before it and under what is drawn after. `ctx` is in the
|
|
940
|
+
* object's user space, its `transform` and every one around it in force
|
|
941
|
+
* and cut to the `clip-path` on it and on what it is in, its `globalAlpha`
|
|
942
|
+
* what the opacities down to it come to. `box` is `{ x, y, width, height,
|
|
943
|
+
* opacity }`: where the object is in that space, its bounding box, and
|
|
944
|
+
* that opacity. The context is restored after it. A hidden object, or one
|
|
945
|
+
* of no width or height, is not handed over; and where it is given,
|
|
946
|
+
* `#Extensibility` holds in a `requiredFeatures`.
|
|
854
947
|
*/
|
|
855
948
|
draw(ctx, x = 0, y = 0, w = this.naturalWidth, h = this.naturalHeight, opts = {}) {
|
|
856
949
|
if (!this._root) return;
|
|
950
|
+
this._foreignObject = typeof opts.foreignObject === 'function' ? opts.foreignObject : null;
|
|
951
|
+
this._extensible = this._foreignObject !== null;
|
|
857
952
|
// what a masked element's surface is made with, and where it can show
|
|
858
953
|
this._surface = opts.surface ?? surfaceMaker(ctx) ?? surfaceMaker(this._ctx);
|
|
859
954
|
this._bounds = deviceBox(ctx, x, y, w, h);
|
|
@@ -879,6 +974,8 @@ export default class SvgView {
|
|
|
879
974
|
this._renderChildren(this._root, ctx, this._style(this._root, this._initial), 1, 0);
|
|
880
975
|
} finally {
|
|
881
976
|
this._surface = null;
|
|
977
|
+
this._foreignObject = null;
|
|
978
|
+
this._extensible = false;
|
|
882
979
|
ctx.restore();
|
|
883
980
|
}
|
|
884
981
|
}
|
|
@@ -1106,7 +1203,7 @@ export default class SvgView {
|
|
|
1106
1203
|
_renderNode(node, ctx, parentStyle, alpha, depth) {
|
|
1107
1204
|
if (depth > 32) return;
|
|
1108
1205
|
const name = tag(node);
|
|
1109
|
-
if (NON_RENDERED.has(name) || displayNone(node) || !conditionsHold(node, this.languages)) return;
|
|
1206
|
+
if (NON_RENDERED.has(name) || displayNone(node) || !conditionsHold(node, this.languages, this._extensible)) return;
|
|
1110
1207
|
|
|
1111
1208
|
const style = this._style(node, parentStyle);
|
|
1112
1209
|
// not inherited: a group's multiplies down through what is in it
|
|
@@ -1145,7 +1242,7 @@ export default class SvgView {
|
|
|
1145
1242
|
case 'switch': {
|
|
1146
1243
|
// a group of one: its transform and opacity are a group's, and it
|
|
1147
1244
|
// draws the first child whose conditions hold and none of the rest
|
|
1148
|
-
const chosen = switchChoice(node, this.languages);
|
|
1245
|
+
const chosen = switchChoice(node, this.languages, this._extensible);
|
|
1149
1246
|
if (chosen) this._renderNode(chosen, ctx, style, nodeAlpha, depth + 1);
|
|
1150
1247
|
break;
|
|
1151
1248
|
}
|
|
@@ -1163,6 +1260,9 @@ export default class SvgView {
|
|
|
1163
1260
|
// `<text>` may be shown again
|
|
1164
1261
|
this._renderText(node, ctx, style, nodeAlpha);
|
|
1165
1262
|
break;
|
|
1263
|
+
case 'foreignobject':
|
|
1264
|
+
this._renderForeign(node, ctx, style, nodeAlpha);
|
|
1265
|
+
break;
|
|
1166
1266
|
default: {
|
|
1167
1267
|
// `visibility` hides what an element paints itself, and is
|
|
1168
1268
|
// inherited: a group walks on, since what is in it may say
|
|
@@ -1179,7 +1279,17 @@ export default class SvgView {
|
|
|
1179
1279
|
ctx.lineCap = style.lineCap;
|
|
1180
1280
|
ctx.lineJoin = style.lineJoin;
|
|
1181
1281
|
ctx.miterLimit = style.miterLimit;
|
|
1282
|
+
// dashed where it says so, and nothing after it is
|
|
1283
|
+
const dashed = style.dashArray !== 'none' && typeof ctx.setLineDash === 'function';
|
|
1284
|
+
if (dashed) {
|
|
1285
|
+
ctx.setLineDash(style.dashArray);
|
|
1286
|
+
ctx.lineDashOffset = style.dashOffset;
|
|
1287
|
+
}
|
|
1182
1288
|
this._ink(ctx, 'stroke', style.stroke, style, path, nodeAlpha * style.strokeOpacity, () => ctx.stroke(path));
|
|
1289
|
+
if (dashed) {
|
|
1290
|
+
ctx.setLineDash([]);
|
|
1291
|
+
ctx.lineDashOffset = 0;
|
|
1292
|
+
}
|
|
1183
1293
|
}
|
|
1184
1294
|
ctx.globalAlpha = 1;
|
|
1185
1295
|
// over its fill and its stroke, which is the default paint order
|
|
@@ -1189,6 +1299,26 @@ export default class SvgView {
|
|
|
1189
1299
|
}
|
|
1190
1300
|
}
|
|
1191
1301
|
|
|
1302
|
+
/**
|
|
1303
|
+
* A `<foreignObject>` (SVG 2, 23.2), handed to `opts.foreignObject` to
|
|
1304
|
+
* draw what it holds, in its user space and at its opacity — the caller's
|
|
1305
|
+
* own drawing, so the context is put back as it was after it, whatever
|
|
1306
|
+
* it set. Nothing is drawn of one where the draw was handed none.
|
|
1307
|
+
*/
|
|
1308
|
+
_renderForeign(node, ctx, style, alpha) {
|
|
1309
|
+
const draw = this._foreignObject;
|
|
1310
|
+
if (!draw || style.visibility !== 'visible') return;
|
|
1311
|
+
const box = foreignBox(node, this._viewport);
|
|
1312
|
+
if (!(box.width > 0 && box.height > 0)) return;
|
|
1313
|
+
ctx.save();
|
|
1314
|
+
try {
|
|
1315
|
+
ctx.globalAlpha = alpha;
|
|
1316
|
+
draw(node, ctx, { ...box, opacity: alpha });
|
|
1317
|
+
} finally {
|
|
1318
|
+
ctx.restore();
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1192
1322
|
/**
|
|
1193
1323
|
* The markers a shape's `marker-start`, `marker-mid` and `marker-end`
|
|
1194
1324
|
* name, drawn at its vertices (SVG 2, 11.6): the first vertex of the
|
|
@@ -1478,7 +1608,7 @@ export default class SvgView {
|
|
|
1478
1608
|
* or that is not visible.
|
|
1479
1609
|
*/
|
|
1480
1610
|
_clipPart(child, inherited, base, ctx, depth, nest) {
|
|
1481
|
-
if (!CLIPPING.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages)) return null;
|
|
1611
|
+
if (!CLIPPING.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages, this._extensible)) return null;
|
|
1482
1612
|
let style = this._style(child, inherited);
|
|
1483
1613
|
let m = matMultiply(base, matrixOf(ownTransforms(child)));
|
|
1484
1614
|
const clips = [];
|
|
@@ -1492,7 +1622,7 @@ export default class SvgView {
|
|
|
1492
1622
|
if (tag(child) === 'use') {
|
|
1493
1623
|
shape = this._useTarget(child);
|
|
1494
1624
|
if (!shape || tag(shape) === 'use' || !CLIPPING.has(tag(shape))) return null;
|
|
1495
|
-
if (displayNone(shape) || !conditionsHold(shape, this.languages)) return null;
|
|
1625
|
+
if (displayNone(shape) || !conditionsHold(shape, this.languages, this._extensible)) return null;
|
|
1496
1626
|
style = this._style(shape, style);
|
|
1497
1627
|
m = matMultiply(m, matrixOf(ownTransforms(shape)));
|
|
1498
1628
|
cutBy(shape, style, m);
|
|
@@ -1689,11 +1819,11 @@ export default class SvgView {
|
|
|
1689
1819
|
case 'g':
|
|
1690
1820
|
case 'a':
|
|
1691
1821
|
case 'switch': {
|
|
1692
|
-
const kids = name === 'switch' ? [switchChoice(node, this.languages)] : node.children || [];
|
|
1822
|
+
const kids = name === 'switch' ? [switchChoice(node, this.languages, this._extensible)] : node.children || [];
|
|
1693
1823
|
let box = null;
|
|
1694
1824
|
for (const child of kids) {
|
|
1695
1825
|
if (!child || child.type !== 'tag') continue;
|
|
1696
|
-
if (NON_RENDERED.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages)) continue;
|
|
1826
|
+
if (NON_RENDERED.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages, this._extensible)) continue;
|
|
1697
1827
|
const inner = this._bbox(child, ctx, this._style(child, style), depth + 1);
|
|
1698
1828
|
if (inner) box = unionBox(box, transformedBox(inner, ownTransforms(child)));
|
|
1699
1829
|
}
|
|
@@ -1724,6 +1854,11 @@ export default class SvgView {
|
|
|
1724
1854
|
ctx.restore();
|
|
1725
1855
|
}
|
|
1726
1856
|
}
|
|
1857
|
+
case 'foreignobject': {
|
|
1858
|
+
// its rectangle, whether or not anything draws what is in it
|
|
1859
|
+
const { x, y, width, height } = foreignBox(node, this._viewport);
|
|
1860
|
+
return { x, y, w: width, h: height };
|
|
1861
|
+
}
|
|
1727
1862
|
default: {
|
|
1728
1863
|
const path = shapePath(node);
|
|
1729
1864
|
return path ? pathBBox(path) : null;
|
|
@@ -1775,7 +1910,7 @@ export default class SvgView {
|
|
|
1775
1910
|
} else if (child.type === 'cdata') {
|
|
1776
1911
|
walk(child.children);
|
|
1777
1912
|
} else if (child.type === 'tag' && depth < 32) {
|
|
1778
|
-
if (!TEXT_SPANS.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages)) continue;
|
|
1913
|
+
if (!TEXT_SPANS.has(tag(child)) || displayNone(child) || !conditionsHold(child, this.languages, this._extensible)) continue;
|
|
1779
1914
|
const ca = a * (ownProperty(child, 'opacity', alphaValue) ?? 1);
|
|
1780
1915
|
collect(child, this._style(child, st), ca, depth + 1);
|
|
1781
1916
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.26.0",
|
|
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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"x11-dri": ">=0.5.0 <1"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
|
-
"test": "NTK_STRICT_COLORS=1 node --test",
|
|
71
|
+
"test": "NTK_STRICT_COLORS=1 node --import ./test/helpers/exit-watchdog.js --test",
|
|
72
72
|
"check-release-message": "node scripts/check-release-message.mjs"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|