@react-x11/components 0.13.0 → 0.14.1
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 +29 -2
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +70 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +42 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +336 -16
- 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 +18 -4
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +66 -28
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +44 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +194 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +128 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +547 -144
- 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.map +1 -1
- package/dist/html/css/ua.js +14 -2
- 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/fonts.d.ts +55 -5
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +284 -23
- package/dist/html/fonts.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 +104 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +410 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +72 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +40 -2
- 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 +144 -24
- 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 +585 -85
- 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 +144 -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 +97 -31
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +4 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +319 -47
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +83 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1474 -217
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +39 -9
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +482 -37
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.js +87 -7
- package/dist/html/widgets.js.map +1 -1
- 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/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 +1 -1
- package/src/html/controls.ts +85 -2
- package/src/html/css/cascade.ts +366 -8
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +71 -31
- package/src/html/css/shapes.ts +239 -0
- package/src/html/css/style.ts +652 -147
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +15 -2
- package/src/html/css/values.ts +4 -0
- package/src/html/fonts.ts +312 -29
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +466 -37
- package/src/html/layout/boxes.ts +95 -3
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +169 -32
- package/src/html/layout/flex.ts +686 -79
- package/src/html/layout/inline.ts +149 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +105 -35
- package/src/html/node.ts +354 -48
- package/src/html/paint.ts +1804 -315
- package/src/html/svg.ts +573 -36
- package/src/html/widgets.ts +95 -9
- package/src/html/woff2.ts +612 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/dist/html/svg.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import { parseDocument } from 'htmlparser2';
|
|
16
16
|
import { Element, Text } from 'domhandler';
|
|
17
17
|
import * as ntk from 'react-x11/ntk';
|
|
18
|
-
import { isSvgRoot } from './dom.js';
|
|
18
|
+
import { isSvgRoot, rawTextOf } from './dom.js';
|
|
19
|
+
import { Cascade } from './css/cascade.js';
|
|
20
|
+
import { parseMediaQuery, parseStylesheet } from './css/parse.js';
|
|
19
21
|
import { inkColor, isTransparent, parseColor } from './css/values.js';
|
|
20
22
|
export { isSvgRoot };
|
|
21
23
|
function svgAttr(el, name) {
|
|
@@ -56,21 +58,6 @@ function absoluteLength(value, fontSize) {
|
|
|
56
58
|
return null;
|
|
57
59
|
return px >= 0 ? px : null;
|
|
58
60
|
}
|
|
59
|
-
/**
|
|
60
|
-
* The `width`/`height` of an inline `<svg>` as CSS: they are presentation
|
|
61
|
-
* attributes for the properties of the same names (SVG 2, 5.1.1), so
|
|
62
|
-
* `height="50%"` is a percentage of the containing block like a style's.
|
|
63
|
-
* A bare number is pixels; null for what CSS would not parse.
|
|
64
|
-
*/
|
|
65
|
-
export function svgSizeHint(value) {
|
|
66
|
-
const v = value.trim();
|
|
67
|
-
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i.test(v))
|
|
68
|
-
return v;
|
|
69
|
-
const m = LENGTH.exec(v);
|
|
70
|
-
if (!m)
|
|
71
|
-
return null;
|
|
72
|
-
return m[2] ? v : `${parseFloat(m[1])}px`;
|
|
73
|
-
}
|
|
74
61
|
function viewBoxParts(el) {
|
|
75
62
|
const parts = (svgAttr(el, 'viewBox') ?? '')
|
|
76
63
|
.trim()
|
|
@@ -137,15 +124,32 @@ export class SvgDrawing {
|
|
|
137
124
|
_seen = -1;
|
|
138
125
|
_seenLast = null;
|
|
139
126
|
_seenViewport = '';
|
|
127
|
+
/** The `fill` and `stroke` the view's tree was last given its root's,
|
|
128
|
+
* and what the document's rules gave the elements in it. */
|
|
129
|
+
_seenPaint = '';
|
|
140
130
|
/** Whether a length in the tree is a percentage of the viewport; null
|
|
141
131
|
* until the tree is first read. */
|
|
142
132
|
_percent = null;
|
|
133
|
+
/** Whether a `<use>` in the tree is one `SvgView` cannot draw as it
|
|
134
|
+
* stands (`needsExpanding`). */
|
|
135
|
+
_uses = false;
|
|
136
|
+
/** The document's last node when a `<use>` was last looked up and its
|
|
137
|
+
* element not found: a streamed document may bring it yet, and has
|
|
138
|
+
* when its last node is another. Undefined where none was missing. */
|
|
139
|
+
_missingAt = undefined;
|
|
143
140
|
_failed = false;
|
|
144
141
|
/** An SVG image's own document, rather than an element of this one. */
|
|
145
142
|
_standalone;
|
|
146
143
|
/** The background an image's root gives its canvas; undefined until it
|
|
147
144
|
* is first read. */
|
|
148
145
|
_canvas = undefined;
|
|
146
|
+
/** An SVG image's own style sheets, its `<style>` elements': read at its
|
|
147
|
+
* first draw, and none for most images. */
|
|
148
|
+
_sheets = null;
|
|
149
|
+
/** A cascade over them for each colour scheme and scale the image is
|
|
150
|
+
* drawn at, and what each gave it: by the viewport it was asked at where
|
|
151
|
+
* a sheet reads the viewport, and under one key where none does. */
|
|
152
|
+
_cascades = new Map();
|
|
149
153
|
constructor(root, intrinsics, standalone = false) {
|
|
150
154
|
this._root = root;
|
|
151
155
|
this.intrinsics = intrinsics;
|
|
@@ -155,9 +159,14 @@ export class SvgDrawing {
|
|
|
155
159
|
* Draw into a rectangle — the box's content box, in device pixels —
|
|
156
160
|
* clipped to it. The `viewBox`, where there is one, is fitted to it as
|
|
157
161
|
* `preserveAspectRatio` says; without one a user unit is a CSS pixel,
|
|
158
|
-
* which is `scale` device pixels. `color` is `currentColor
|
|
162
|
+
* which is `scale` device pixels. `color` is `currentColor`, and `fill`
|
|
163
|
+
* and `stroke` the root's own where the document's styles set them (SVG
|
|
164
|
+
* 2, 13.2): they are properties, which a style sheet's rule sets over
|
|
165
|
+
* the presentation attribute, and what is in the drawing inherits them
|
|
166
|
+
* from its root. `shapes` is what the rules give the elements inside it,
|
|
167
|
+
* the same way, where they give any.
|
|
159
168
|
*/
|
|
160
|
-
draw(ctx, x, y, w, h, scale, color) {
|
|
169
|
+
draw(ctx, x, y, w, h, scale, color, fill = null, stroke = null, shapes = null) {
|
|
161
170
|
if (this._failed || !(w > 0 && h > 0))
|
|
162
171
|
return;
|
|
163
172
|
// the mock backend has no path API, and SvgView draws paths
|
|
@@ -189,9 +198,11 @@ export class SvgDrawing {
|
|
|
189
198
|
}
|
|
190
199
|
const box = viewBoxOf(root);
|
|
191
200
|
// the viewport in user units, which a percentage is of
|
|
201
|
+
const paint = (fill === null ? '' : `fill:${fill};`) +
|
|
202
|
+
(stroke === null ? '' : `stroke:${stroke};`);
|
|
192
203
|
const view = box
|
|
193
|
-
? this._viewFor(box[2], box[3])
|
|
194
|
-
: this._viewFor(w / scale, h / scale);
|
|
204
|
+
? this._viewFor(box[2], box[3], paint, shapes)
|
|
205
|
+
: this._viewFor(w / scale, h / scale, paint, shapes);
|
|
195
206
|
if (!view)
|
|
196
207
|
return;
|
|
197
208
|
ctx.save();
|
|
@@ -232,15 +243,92 @@ export class SvgDrawing {
|
|
|
232
243
|
ctx.restore();
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Draw an SVG image — an `<img>`'s, a background's, a list marker's — as
|
|
248
|
+
* `draw` does, painted as its own style sheets say. An image is a
|
|
249
|
+
* document of its own: no rule of the page's reaches into it, and its
|
|
250
|
+
* `color` is not the page's. What its `<style>` elements say is given
|
|
251
|
+
* its elements as the page's rules are given a drawing inline, and its
|
|
252
|
+
* root its `color`, `fill` and `stroke`, which an inline root has from
|
|
253
|
+
* its box. `scheme` is the colour scheme of the element that embeds it:
|
|
254
|
+
* what `prefers-color-scheme` answers inside it, as Chrome has it.
|
|
255
|
+
*/
|
|
256
|
+
drawImage(ctx, x, y, w, h, scale, scheme = 'light') {
|
|
257
|
+
if (this._failed || !(w > 0 && h > 0))
|
|
258
|
+
return;
|
|
259
|
+
const own = this._ownPaint(w, h, scale, scheme);
|
|
260
|
+
this.draw(ctx, x, y, w, h, scale, own?.color, own?.fill ?? null, own?.stroke ?? null, own?.shapes ?? null);
|
|
261
|
+
}
|
|
262
|
+
/** What an image's own style sheets give it, drawn `w` by `h` device
|
|
263
|
+
* pixels, or null where it has none. */
|
|
264
|
+
_ownPaint(w, h, scale, scheme) {
|
|
265
|
+
const sheets = (this._sheets ??= ownSheets(this._root));
|
|
266
|
+
if (!sheets.length)
|
|
267
|
+
return null;
|
|
268
|
+
const made = `${scheme}|${scale}`;
|
|
269
|
+
let entry = this._cascades.get(made);
|
|
270
|
+
if (!entry) {
|
|
271
|
+
const cascade = new Cascade(sheets, imageLook(scheme, scale), w, h, scale, null, null, null, null, this._root);
|
|
272
|
+
entry = { cascade, own: new Map() };
|
|
273
|
+
this._cascades.set(made, entry);
|
|
274
|
+
}
|
|
275
|
+
const { cascade, own } = entry;
|
|
276
|
+
// An image's viewport is the rectangle it is drawn in, which is what a
|
|
277
|
+
// `@media (min-width)` in it, or a `vw`, is of: the answer is kept by
|
|
278
|
+
// that size only where a sheet asks.
|
|
279
|
+
const sized = cascade.breakpoints.length > 0 ||
|
|
280
|
+
cascade.readsViewportWidth ||
|
|
281
|
+
cascade.readsViewportHeight;
|
|
282
|
+
const key = sized ? `${w}x${h}` : '';
|
|
283
|
+
const kept = own.get(key);
|
|
284
|
+
if (kept !== undefined)
|
|
285
|
+
return kept;
|
|
286
|
+
cascade.viewportWidth = w;
|
|
287
|
+
cascade.viewportHeight = h;
|
|
288
|
+
let paint = null;
|
|
289
|
+
try {
|
|
290
|
+
const root = this._root;
|
|
291
|
+
const style = cascade.styleFor(root, cascade.initial, false);
|
|
292
|
+
const ink = (value) => value === null || value === 'currentColor' || value === 'none'
|
|
293
|
+
? value
|
|
294
|
+
: inkColor(value, style.color);
|
|
295
|
+
paint = {
|
|
296
|
+
color: style.color,
|
|
297
|
+
fill: ink(style.fill),
|
|
298
|
+
stroke: ink(style.stroke),
|
|
299
|
+
shapes: cascade.shapeStyles(root, style),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// a sheet this cannot read leaves the image as its attributes draw it
|
|
304
|
+
}
|
|
305
|
+
// a background drawn at many sizes keeps a few of them
|
|
306
|
+
if (own.size >= 8)
|
|
307
|
+
own.clear();
|
|
308
|
+
own.set(key, paint);
|
|
309
|
+
return paint;
|
|
310
|
+
}
|
|
311
|
+
_viewFor(width, height,
|
|
312
|
+
/** The root's `fill` and `stroke` as declarations, or none. */
|
|
313
|
+
paint = '', shapes = null) {
|
|
236
314
|
const root = this._root;
|
|
237
315
|
const count = root.children.length;
|
|
238
316
|
const last = root.lastChild;
|
|
239
|
-
|
|
240
|
-
|
|
317
|
+
// more of the document has arrived since a `<use>` found nothing
|
|
318
|
+
const arrived = this._missingAt !== undefined &&
|
|
319
|
+
lastNode(documentOf(root)) !== this._missingAt;
|
|
320
|
+
const grown = count !== this._seen || last !== this._seenLast || arrived;
|
|
321
|
+
if (grown || this._percent === null) {
|
|
241
322
|
this._percent = hasPercent(root);
|
|
242
|
-
|
|
243
|
-
|
|
323
|
+
this._uses = needsExpanding(root);
|
|
324
|
+
}
|
|
325
|
+
const sized = this._percent || this._uses;
|
|
326
|
+
const viewport = sized ? `${width}x${height}` : '';
|
|
327
|
+
const painted = shapes ? `${paint}|${shapes.key}` : paint;
|
|
328
|
+
if (this._view &&
|
|
329
|
+
!grown &&
|
|
330
|
+
viewport === this._seenViewport &&
|
|
331
|
+
painted === this._seenPaint) {
|
|
244
332
|
return this._view;
|
|
245
333
|
}
|
|
246
334
|
const View = svgViewClass();
|
|
@@ -250,13 +338,17 @@ export class SvgDrawing {
|
|
|
250
338
|
}
|
|
251
339
|
try {
|
|
252
340
|
const view = this._view ?? new View(null);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
:
|
|
341
|
+
const missing = { any: false };
|
|
342
|
+
const tree = sized || shapes || root.name.includes(':')
|
|
343
|
+
? copyTree(root, sized ? [width, height] : null, missing, shapes)
|
|
344
|
+
: root;
|
|
345
|
+
view.setSvgDom(paint ? withPaint(tree, paint) : tree);
|
|
346
|
+
this._missingAt = missing.any ? lastNode(documentOf(root)) : undefined;
|
|
256
347
|
this._view = view;
|
|
257
348
|
this._seen = count;
|
|
258
349
|
this._seenLast = last;
|
|
259
350
|
this._seenViewport = viewport;
|
|
351
|
+
this._seenPaint = painted;
|
|
260
352
|
return view;
|
|
261
353
|
}
|
|
262
354
|
catch {
|
|
@@ -265,6 +357,16 @@ export class SvgDrawing {
|
|
|
265
357
|
}
|
|
266
358
|
}
|
|
267
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* A drawing's root with its `fill` and `stroke` the document's: a copy of
|
|
362
|
+
* the root, its children the same ones, with the declarations at the end of
|
|
363
|
+
* its `style` — where `SvgView` reads a root's paint, over its presentation
|
|
364
|
+
* attributes, for what is under it to inherit.
|
|
365
|
+
*/
|
|
366
|
+
function withPaint(root, paint) {
|
|
367
|
+
const style = root.attribs.style;
|
|
368
|
+
return new Element(root.name, { ...root.attribs, style: style ? `${style};${paint}` : paint }, root.children);
|
|
369
|
+
}
|
|
268
370
|
/**
|
|
269
371
|
* The colour an SVG image's root element sets as its background, in its
|
|
270
372
|
* `style` attribute — the one place an image can say it, since
|
|
@@ -288,6 +390,59 @@ function canvasBackground(root) {
|
|
|
288
390
|
}
|
|
289
391
|
return found;
|
|
290
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* The style sheets an SVG image's `<style>` elements hold, in order. One
|
|
395
|
+
* that names a type other than CSS's holds none (SVG 2, 6.3), and one with
|
|
396
|
+
* a `media` query holds its rules under it. Its text is its text and CDATA
|
|
397
|
+
* sections, as an XML parse leaves them, and not its comments.
|
|
398
|
+
*/
|
|
399
|
+
function ownSheets(root) {
|
|
400
|
+
const sheets = [];
|
|
401
|
+
const layers = new Map();
|
|
402
|
+
let order = 0;
|
|
403
|
+
const stack = [root];
|
|
404
|
+
for (let el = stack.pop(); el; el = stack.pop()) {
|
|
405
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
406
|
+
const child = el.children[i];
|
|
407
|
+
// an XML parse gives a `<style>` a type of its own, as HTML's does
|
|
408
|
+
if (child.type === 'tag' || child.type === 'style') {
|
|
409
|
+
stack.push(child);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (el === root || localName(el.name) !== 'style')
|
|
413
|
+
continue;
|
|
414
|
+
const type = (el.attribs.type ?? '').trim().toLowerCase();
|
|
415
|
+
if (type && type !== 'text/css')
|
|
416
|
+
continue;
|
|
417
|
+
const media = el.attribs.media?.trim();
|
|
418
|
+
const sheet = parseStylesheet(rawTextOf(el), order, layers, null, media ? [parseMediaQuery(media)] : null);
|
|
419
|
+
order += sheet.rules.length + 1;
|
|
420
|
+
sheets.push(sheet);
|
|
421
|
+
}
|
|
422
|
+
return sheets;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The look an SVG image's own document starts from: CSS's initial values,
|
|
426
|
+
* black at `medium`, since nothing of the page's is inherited into an
|
|
427
|
+
* image — only the colour scheme, which its `prefers-color-scheme` answers.
|
|
428
|
+
*/
|
|
429
|
+
function imageLook(scheme, scale) {
|
|
430
|
+
return {
|
|
431
|
+
color: '#000000',
|
|
432
|
+
fontFamily: 'sans-serif',
|
|
433
|
+
fontSize: 16 * scale,
|
|
434
|
+
monoFamily: 'monospace',
|
|
435
|
+
linkColor: '#0000ee',
|
|
436
|
+
borderColor: '#000000',
|
|
437
|
+
mutedColor: '#808080',
|
|
438
|
+
background: 'transparent',
|
|
439
|
+
colorScheme: scheme,
|
|
440
|
+
surface: '#ffffff',
|
|
441
|
+
controlPadY: 0,
|
|
442
|
+
controlBorder: 0,
|
|
443
|
+
controlRadius: 0,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
291
446
|
/** Drawings of inline `<svg>` elements, per element: the element is the
|
|
292
447
|
* document's, and outlives the box trees built over it. */
|
|
293
448
|
const INLINE = new WeakMap();
|
|
@@ -349,22 +504,248 @@ function hasPercent(root) {
|
|
|
349
504
|
};
|
|
350
505
|
return walk(root);
|
|
351
506
|
}
|
|
507
|
+
/** The element a `<use>` refers to by a fragment, its id: one in another
|
|
508
|
+
* document is none here, where nothing is fetched for a drawing. */
|
|
509
|
+
function useTarget(el) {
|
|
510
|
+
const href = el.attribs.href ?? el.attribs['xlink:href'] ?? '';
|
|
511
|
+
return href.length > 1 && href.startsWith('#') ? href.slice(1) : null;
|
|
512
|
+
}
|
|
513
|
+
/** The elements under a root by their ids, the first of each. */
|
|
514
|
+
function idsUnder(root) {
|
|
515
|
+
const ids = new Map();
|
|
516
|
+
const walk = (el) => {
|
|
517
|
+
for (const child of el.children) {
|
|
518
|
+
if (child.type !== 'tag')
|
|
519
|
+
continue;
|
|
520
|
+
const tag = child;
|
|
521
|
+
const id = tag.attribs.id;
|
|
522
|
+
if (id && !ids.has(id))
|
|
523
|
+
ids.set(id, tag);
|
|
524
|
+
walk(tag);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
walk(root);
|
|
528
|
+
return ids;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Whether a `<use>` under the root is one `SvgView` cannot draw as it
|
|
532
|
+
* stands: it looks a reference up among the root's own elements, and draws
|
|
533
|
+
* a `<symbol>` as its children where they are. So one to an element
|
|
534
|
+
* outside the root — an icon sprite's, a hidden `<svg>` of symbols at the
|
|
535
|
+
* top of the page — and one to a symbol with a `viewBox` of its own are
|
|
536
|
+
* drawn from a copy (`copyTree`).
|
|
537
|
+
*/
|
|
538
|
+
function needsExpanding(root) {
|
|
539
|
+
let ids = null;
|
|
540
|
+
const walk = (el) => {
|
|
541
|
+
for (const child of el.children) {
|
|
542
|
+
if (child.type !== 'tag')
|
|
543
|
+
continue;
|
|
544
|
+
const tag = child;
|
|
545
|
+
if (localName(tag.name) === 'use') {
|
|
546
|
+
const id = useTarget(tag);
|
|
547
|
+
if (id !== null) {
|
|
548
|
+
ids ??= idsUnder(root);
|
|
549
|
+
const target = ids.get(id);
|
|
550
|
+
if (!target)
|
|
551
|
+
return true;
|
|
552
|
+
if (localName(target.name) === 'symbol' && viewBoxOf(target)) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (walk(tag))
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
};
|
|
562
|
+
return walk(root);
|
|
563
|
+
}
|
|
564
|
+
function documentOf(el) {
|
|
565
|
+
let top = el;
|
|
566
|
+
while (top.parent)
|
|
567
|
+
top = top.parent;
|
|
568
|
+
return top;
|
|
569
|
+
}
|
|
570
|
+
/** The last node of a document, in its order: what a document that is
|
|
571
|
+
* still arriving has another of once more of it has. */
|
|
572
|
+
function lastNode(top) {
|
|
573
|
+
let last = top.lastChild;
|
|
574
|
+
while (last && 'lastChild' in last && last.lastChild)
|
|
575
|
+
last = last.lastChild;
|
|
576
|
+
return last;
|
|
577
|
+
}
|
|
578
|
+
/** The first element of a document with an id, in its order. */
|
|
579
|
+
function elementById(top, id) {
|
|
580
|
+
const stack = [...top.children].reverse();
|
|
581
|
+
for (let node = stack.pop(); node; node = stack.pop()) {
|
|
582
|
+
if (node.type !== 'tag')
|
|
583
|
+
continue;
|
|
584
|
+
const el = node;
|
|
585
|
+
if (el.attribs.id === id)
|
|
586
|
+
return el;
|
|
587
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
588
|
+
stack.push(el.children[i]);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
/** How deep a `<use>` may be of a `<use>`: one that reaches itself stops. */
|
|
594
|
+
const USE_DEPTH = 8;
|
|
595
|
+
/** A `<use>`'s `width` or `height`, in user units: a number, or a
|
|
596
|
+
* percentage of the viewport's; null for none, and for `auto`. */
|
|
597
|
+
function useLength(value, of) {
|
|
598
|
+
if (value === undefined)
|
|
599
|
+
return null;
|
|
600
|
+
const n = parseFloat(value);
|
|
601
|
+
if (!Number.isFinite(n) || n < 0)
|
|
602
|
+
return null;
|
|
603
|
+
return value.trim().endsWith('%') ? (n / 100) * of : n;
|
|
604
|
+
}
|
|
605
|
+
/** The transform that fits a `viewBox` into a viewport at `x`, `y`, as
|
|
606
|
+
* `preserveAspectRatio` says (SVG 2, 8.2). */
|
|
607
|
+
function fitTransform(box, x, y, width, height, preserve) {
|
|
608
|
+
const fit = (preserve ?? '').trim().split(/\s+/);
|
|
609
|
+
if (fit[0] === 'defer')
|
|
610
|
+
fit.shift();
|
|
611
|
+
let sx = width / box[2];
|
|
612
|
+
let sy = height / box[3];
|
|
613
|
+
if (fit[0] !== 'none') {
|
|
614
|
+
const align = ALIGN.exec(fit[0] ?? '') ?? ['xMidYMid', 'Mid', 'Mid'];
|
|
615
|
+
const scale = fit[1] === 'slice' ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
616
|
+
x += (width - box[2] * scale) * AT[align[1]];
|
|
617
|
+
y += (height - box[3] * scale) * AT[align[2]];
|
|
618
|
+
sx = scale;
|
|
619
|
+
sy = scale;
|
|
620
|
+
}
|
|
621
|
+
return `translate(${x - box[0] * sx},${y - box[1] * sy}) scale(${sx},${sy})`;
|
|
622
|
+
}
|
|
623
|
+
/** What a `<use>` says of where its element goes, and not of how it looks:
|
|
624
|
+
* the rest is the group's the element is drawn in. */
|
|
625
|
+
const USE_PLACEMENT = new Set([
|
|
626
|
+
'href',
|
|
627
|
+
'xlink:href',
|
|
628
|
+
'x',
|
|
629
|
+
'y',
|
|
630
|
+
'width',
|
|
631
|
+
'height',
|
|
632
|
+
]);
|
|
352
633
|
/**
|
|
353
634
|
* The tree `SvgView` reads, where the document's own will not do: with
|
|
354
635
|
* local names — `SvgView` knows `rect`, not `svg:rect`, and a prefix is
|
|
355
636
|
* dropped only where it is bound to SVG — and with its percentages resolved
|
|
356
|
-
* against a viewport of `[width, height]` user units, where one is given
|
|
637
|
+
* against a viewport of `[width, height]` user units, where one is given;
|
|
638
|
+
* and with each `<use>` that `SvgView` cannot draw (`needsExpanding`) as a
|
|
639
|
+
* group of what it refers to (SVG 2, 5.5): the element, from wherever in
|
|
640
|
+
* the document it is, or a symbol's children, its `viewBox` fitted to the
|
|
641
|
+
* viewport the `<use>` gives it — its `width` and `height`, and all of the
|
|
642
|
+
* drawing's where it has none. `missing.any` is set where one refers to an
|
|
643
|
+
* element the document does not have.
|
|
644
|
+
*
|
|
645
|
+
* And with what the document's rules give each element (`shapes`), where
|
|
646
|
+
* `SvgView` reads an element's own: at the end of its `style`, over its
|
|
647
|
+
* presentation attributes and over what that attribute says, which the
|
|
648
|
+
* cascade that made them has weighed already. An element a rule gives
|
|
649
|
+
* `display: none` is left out, with what is in it, and a shape one hides
|
|
650
|
+
* — `visibility` is inherited, and a shape in a hidden group may be shown
|
|
651
|
+
* again — is left out alone.
|
|
357
652
|
*/
|
|
358
|
-
function copyTree(root, viewport) {
|
|
653
|
+
function copyTree(root, viewport, missing, shapes = null) {
|
|
359
654
|
const colon = root.name.indexOf(':');
|
|
360
655
|
const prefix = colon < 0 ? null : `${root.name.slice(0, colon)}:`;
|
|
361
656
|
const strip = (name) => prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
362
657
|
const diagonal = viewport
|
|
363
658
|
? Math.sqrt((viewport[0] ** 2 + viewport[1] ** 2) / 2)
|
|
364
659
|
: 0;
|
|
365
|
-
|
|
660
|
+
let ids = null;
|
|
661
|
+
const expand = (use, resolve, depth, hidden) => {
|
|
662
|
+
const id = useTarget(use);
|
|
663
|
+
if (id === null || depth >= USE_DEPTH)
|
|
664
|
+
return null;
|
|
665
|
+
ids ??= idsUnder(root);
|
|
666
|
+
const inside = ids.get(id);
|
|
667
|
+
const target = inside ?? elementById(documentOf(root), id);
|
|
668
|
+
if (!target) {
|
|
669
|
+
if (missing)
|
|
670
|
+
missing.any = true;
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const symbol = localName(target.name) === 'symbol';
|
|
674
|
+
const box = symbol ? viewBoxOf(target) : null;
|
|
675
|
+
// one of the root's own with no viewport to fit: `SvgView` draws it
|
|
676
|
+
if (inside && !box)
|
|
677
|
+
return null;
|
|
678
|
+
const attribs = {};
|
|
679
|
+
for (const name in use.attribs) {
|
|
680
|
+
if (!USE_PLACEMENT.has(name))
|
|
681
|
+
attribs[name] = use.attribs[name];
|
|
682
|
+
}
|
|
683
|
+
const own = shapes?.of.get(use);
|
|
684
|
+
if (own)
|
|
685
|
+
restyle(attribs, own);
|
|
686
|
+
const x = parseFloat(use.attribs.x ?? '') || 0;
|
|
687
|
+
const y = parseFloat(use.attribs.y ?? '') || 0;
|
|
688
|
+
let placed = '';
|
|
689
|
+
if (box && viewport) {
|
|
690
|
+
const width = useLength(use.attribs.width, viewport[0]) ??
|
|
691
|
+
useLength(target.attribs.width, viewport[0]) ??
|
|
692
|
+
viewport[0];
|
|
693
|
+
const height = useLength(use.attribs.height, viewport[1]) ??
|
|
694
|
+
useLength(target.attribs.height, viewport[1]) ??
|
|
695
|
+
viewport[1];
|
|
696
|
+
placed = fitTransform(box, x, y, width, height, svgAttr(target, 'preserveAspectRatio'));
|
|
697
|
+
}
|
|
698
|
+
else if (x || y)
|
|
699
|
+
placed = `translate(${x},${y})`;
|
|
700
|
+
const transform = `${attribs.transform ?? ''} ${placed}`.trim();
|
|
701
|
+
if (transform)
|
|
702
|
+
attribs.transform = transform;
|
|
703
|
+
// a length in a symbol with a `viewBox` is of that, and left as it is
|
|
704
|
+
const within = resolve && !box;
|
|
705
|
+
const children = [];
|
|
706
|
+
if (symbol)
|
|
707
|
+
copyInto(children, target, within, depth + 1, hidden);
|
|
708
|
+
else if (shown(target, hidden)) {
|
|
709
|
+
children.push(copy(target, within, depth + 1, hidden));
|
|
710
|
+
}
|
|
711
|
+
return new Element('g', attribs, children);
|
|
712
|
+
};
|
|
713
|
+
/** Whether an element is drawn at all, in a group that is hidden or
|
|
714
|
+
* not: one no rule gives `display: none`, and no shape a rule hides. */
|
|
715
|
+
const shown = (el, hidden) => {
|
|
716
|
+
if (!shapes)
|
|
717
|
+
return true;
|
|
718
|
+
const own = shapes.of.get(el);
|
|
719
|
+
const name = localName(el.name);
|
|
720
|
+
if (own?.display === 'none' && !UNDRAWN.has(name))
|
|
721
|
+
return false;
|
|
722
|
+
return !(hiddenIn(own, hidden) && HIDEABLE.has(name));
|
|
723
|
+
};
|
|
724
|
+
const copyInto = (children, el, resolve, depth, hidden) => {
|
|
725
|
+
for (const child of el.children) {
|
|
726
|
+
if (child.type === 'tag') {
|
|
727
|
+
if (shown(child, hidden)) {
|
|
728
|
+
children.push(copy(child, resolve, depth, hidden));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
else if (child.type === 'text')
|
|
732
|
+
children.push(new Text(child.data));
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const copy = (el, resolve, depth = 0,
|
|
736
|
+
/** Whether the element inherits a `visibility` of `hidden`. */
|
|
737
|
+
hidden = false) => {
|
|
738
|
+
const own = shapes?.of.get(el);
|
|
739
|
+
hidden = hiddenIn(own, hidden);
|
|
740
|
+
if (localName(el.name) === 'use') {
|
|
741
|
+
const group = expand(el, resolve, depth, hidden);
|
|
742
|
+
if (group)
|
|
743
|
+
return group;
|
|
744
|
+
}
|
|
366
745
|
const here = resolve && !OWN_UNITS.has(localName(el.name));
|
|
367
746
|
const attribs = { ...el.attribs };
|
|
747
|
+
if (own)
|
|
748
|
+
restyle(attribs, own);
|
|
368
749
|
if (here && viewport && el !== root) {
|
|
369
750
|
for (const name in attribs) {
|
|
370
751
|
const axis = PERCENT_OF[name];
|
|
@@ -379,16 +760,80 @@ function copyTree(root, viewport) {
|
|
|
379
760
|
}
|
|
380
761
|
}
|
|
381
762
|
const children = [];
|
|
382
|
-
|
|
383
|
-
if (child.type === 'tag')
|
|
384
|
-
children.push(copy(child, here));
|
|
385
|
-
else if (child.type === 'text')
|
|
386
|
-
children.push(new Text(child.data));
|
|
387
|
-
}
|
|
763
|
+
copyInto(children, el, here, depth, hidden);
|
|
388
764
|
return new Element(strip(el.name), attribs, children);
|
|
389
765
|
};
|
|
390
766
|
return copy(root, true);
|
|
391
767
|
}
|
|
768
|
+
/** What `display: none` takes nothing from: `SvgView` draws none of them
|
|
769
|
+
* where they stand, and what refers to one still finds it (SVG 2, 5.3). */
|
|
770
|
+
const UNDRAWN = new Set([
|
|
771
|
+
'defs',
|
|
772
|
+
'symbol',
|
|
773
|
+
'lineargradient',
|
|
774
|
+
'radialgradient',
|
|
775
|
+
'stop',
|
|
776
|
+
'clippath',
|
|
777
|
+
'mask',
|
|
778
|
+
'pattern',
|
|
779
|
+
'marker',
|
|
780
|
+
'style',
|
|
781
|
+
]);
|
|
782
|
+
/** What `visibility: hidden` leaves undrawn: the elements with something
|
|
783
|
+
* of their own to draw, and a `<use>`, since what it draws inherits the
|
|
784
|
+
* property from it. */
|
|
785
|
+
const HIDEABLE = new Set([
|
|
786
|
+
'path',
|
|
787
|
+
'rect',
|
|
788
|
+
'circle',
|
|
789
|
+
'ellipse',
|
|
790
|
+
'line',
|
|
791
|
+
'polyline',
|
|
792
|
+
'polygon',
|
|
793
|
+
'text',
|
|
794
|
+
'image',
|
|
795
|
+
'use',
|
|
796
|
+
]);
|
|
797
|
+
/** Whether an element is hidden, given whether what it is in is. */
|
|
798
|
+
function hiddenIn(own, hidden) {
|
|
799
|
+
const visibility = own?.visibility;
|
|
800
|
+
return visibility === undefined || visibility === 'inherit'
|
|
801
|
+
? hidden
|
|
802
|
+
: visibility === 'hidden';
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* An element's attributes with what the rules give it: each property at
|
|
806
|
+
* the end of its `style`, where `SvgView` reads it over the attribute of
|
|
807
|
+
* its name — but `opacity`, which it reads from the attribute alone. One
|
|
808
|
+
* the rules put back to what the element inherits is taken out of both.
|
|
809
|
+
*/
|
|
810
|
+
function restyle(attribs, own) {
|
|
811
|
+
let style = attribs.style ?? '';
|
|
812
|
+
let added = '';
|
|
813
|
+
for (const prop in own) {
|
|
814
|
+
const value = own[prop];
|
|
815
|
+
if (prop === 'display' || prop === 'visibility')
|
|
816
|
+
continue;
|
|
817
|
+
if (value === 'inherit') {
|
|
818
|
+
delete attribs[prop];
|
|
819
|
+
if (style) {
|
|
820
|
+
style = style
|
|
821
|
+
.split(';')
|
|
822
|
+
.filter((d) => d.slice(0, d.indexOf(':')).trim() !== prop)
|
|
823
|
+
.join(';');
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else if (prop === 'opacity')
|
|
827
|
+
attribs.opacity = value;
|
|
828
|
+
else
|
|
829
|
+
added += `${prop}:${value};`;
|
|
830
|
+
}
|
|
831
|
+
style = style && added ? `${style};${added}` : style || added;
|
|
832
|
+
if (style)
|
|
833
|
+
attribs.style = style;
|
|
834
|
+
else
|
|
835
|
+
delete attribs.style;
|
|
836
|
+
}
|
|
392
837
|
/**
|
|
393
838
|
* An SVG image's bytes as a drawing, or null for bytes that are not SVG —
|
|
394
839
|
* which is how a PNG or a JPEG goes on to the image decoder. Nothing names
|