@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.
Files changed (134) hide show
  1. package/dist/html/controls.d.ts +29 -2
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +70 -2
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +42 -1
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +336 -16
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +18 -4
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +66 -28
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/shapes.d.ts +44 -0
  18. package/dist/html/css/shapes.d.ts.map +1 -0
  19. package/dist/html/css/shapes.js +194 -0
  20. package/dist/html/css/shapes.js.map +1 -0
  21. package/dist/html/css/style.d.ts +128 -15
  22. package/dist/html/css/style.d.ts.map +1 -1
  23. package/dist/html/css/style.js +547 -144
  24. package/dist/html/css/style.js.map +1 -1
  25. package/dist/html/css/transform.d.ts +85 -0
  26. package/dist/html/css/transform.d.ts.map +1 -0
  27. package/dist/html/css/transform.js +425 -0
  28. package/dist/html/css/transform.js.map +1 -0
  29. package/dist/html/css/ua.d.ts.map +1 -1
  30. package/dist/html/css/ua.js +14 -2
  31. package/dist/html/css/ua.js.map +1 -1
  32. package/dist/html/css/values.d.ts +4 -0
  33. package/dist/html/css/values.d.ts.map +1 -1
  34. package/dist/html/css/values.js.map +1 -1
  35. package/dist/html/fonts.d.ts +55 -5
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +284 -23
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/layout/axes.d.ts +14 -0
  40. package/dist/html/layout/axes.d.ts.map +1 -0
  41. package/dist/html/layout/axes.js +28 -0
  42. package/dist/html/layout/axes.js.map +1 -0
  43. package/dist/html/layout/block.d.ts +104 -12
  44. package/dist/html/layout/block.d.ts.map +1 -1
  45. package/dist/html/layout/block.js +410 -39
  46. package/dist/html/layout/block.js.map +1 -1
  47. package/dist/html/layout/boxes.d.ts +72 -1
  48. package/dist/html/layout/boxes.d.ts.map +1 -1
  49. package/dist/html/layout/boxes.js +40 -2
  50. package/dist/html/layout/boxes.js.map +1 -1
  51. package/dist/html/layout/cache.d.ts +1 -1
  52. package/dist/html/layout/cache.d.ts.map +1 -1
  53. package/dist/html/layout/cache.js +6 -0
  54. package/dist/html/layout/cache.js.map +1 -1
  55. package/dist/html/layout/css-grid.d.ts.map +1 -1
  56. package/dist/html/layout/css-grid.js +144 -24
  57. package/dist/html/layout/css-grid.js.map +1 -1
  58. package/dist/html/layout/flex.d.ts.map +1 -1
  59. package/dist/html/layout/flex.js +585 -85
  60. package/dist/html/layout/flex.js.map +1 -1
  61. package/dist/html/layout/inline.d.ts +3 -0
  62. package/dist/html/layout/inline.d.ts.map +1 -1
  63. package/dist/html/layout/inline.js +144 -54
  64. package/dist/html/layout/inline.js.map +1 -1
  65. package/dist/html/layout/multicol.d.ts +42 -0
  66. package/dist/html/layout/multicol.d.ts.map +1 -0
  67. package/dist/html/layout/multicol.js +897 -0
  68. package/dist/html/layout/multicol.js.map +1 -0
  69. package/dist/html/layout/shaping.d.ts +7 -0
  70. package/dist/html/layout/shaping.d.ts.map +1 -1
  71. package/dist/html/layout/shaping.js +25 -20
  72. package/dist/html/layout/shaping.js.map +1 -1
  73. package/dist/html/layout/table.d.ts.map +1 -1
  74. package/dist/html/layout/table.js +97 -31
  75. package/dist/html/layout/table.js.map +1 -1
  76. package/dist/html/node.d.ts +4 -0
  77. package/dist/html/node.d.ts.map +1 -1
  78. package/dist/html/node.js +319 -47
  79. package/dist/html/node.js.map +1 -1
  80. package/dist/html/paint.d.ts +83 -5
  81. package/dist/html/paint.d.ts.map +1 -1
  82. package/dist/html/paint.js +1474 -217
  83. package/dist/html/paint.js.map +1 -1
  84. package/dist/html/svg.d.ts +39 -9
  85. package/dist/html/svg.d.ts.map +1 -1
  86. package/dist/html/svg.js +482 -37
  87. package/dist/html/svg.js.map +1 -1
  88. package/dist/html/widgets.js +87 -7
  89. package/dist/html/widgets.js.map +1 -1
  90. package/dist/html/woff2.d.ts +14 -0
  91. package/dist/html/woff2.d.ts.map +1 -0
  92. package/dist/html/woff2.js +608 -0
  93. package/dist/html/woff2.js.map +1 -0
  94. package/dist/maps/controller.d.ts +17 -1
  95. package/dist/maps/controller.d.ts.map +1 -1
  96. package/dist/maps/controller.js +27 -7
  97. package/dist/maps/controller.js.map +1 -1
  98. package/dist/richtext/node.d.ts +8 -1
  99. package/dist/richtext/node.d.ts.map +1 -1
  100. package/dist/richtext/node.js +5 -1
  101. package/dist/richtext/node.js.map +1 -1
  102. package/dist/richtext/runs.d.ts +25 -6
  103. package/dist/richtext/runs.d.ts.map +1 -1
  104. package/dist/richtext/runs.js +240 -24
  105. package/dist/richtext/runs.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/html/controls.ts +85 -2
  108. package/src/html/css/cascade.ts +366 -8
  109. package/src/html/css/color.ts +29 -0
  110. package/src/html/css/parse.ts +71 -31
  111. package/src/html/css/shapes.ts +239 -0
  112. package/src/html/css/style.ts +652 -147
  113. package/src/html/css/transform.ts +459 -0
  114. package/src/html/css/ua.ts +15 -2
  115. package/src/html/css/values.ts +4 -0
  116. package/src/html/fonts.ts +312 -29
  117. package/src/html/layout/axes.ts +62 -0
  118. package/src/html/layout/block.ts +466 -37
  119. package/src/html/layout/boxes.ts +95 -3
  120. package/src/html/layout/cache.ts +6 -0
  121. package/src/html/layout/css-grid.ts +169 -32
  122. package/src/html/layout/flex.ts +686 -79
  123. package/src/html/layout/inline.ts +149 -55
  124. package/src/html/layout/multicol.ts +977 -0
  125. package/src/html/layout/shaping.ts +27 -18
  126. package/src/html/layout/table.ts +105 -35
  127. package/src/html/node.ts +354 -48
  128. package/src/html/paint.ts +1804 -315
  129. package/src/html/svg.ts +573 -36
  130. package/src/html/widgets.ts +95 -9
  131. package/src/html/woff2.ts +612 -0
  132. package/src/maps/controller.ts +28 -7
  133. package/src/richtext/node.ts +13 -2
  134. 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
- _viewFor(width, height) {
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
- const grown = count !== this._seen || last !== this._seenLast;
240
- if (grown || this._percent === null)
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
- const viewport = this._percent ? `${width}x${height}` : '';
243
- if (this._view && !grown && viewport === this._seenViewport) {
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
- view.setSvgDom(this._percent || root.name.includes(':')
254
- ? copyTree(root, this._percent ? [width, height] : null)
255
- : root);
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
- const copy = (el, resolve) => {
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
- for (const child of el.children) {
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