@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/src/html/svg.ts CHANGED
@@ -14,9 +14,14 @@
14
14
  // as `preserveAspectRatio` says, and clipped to it.
15
15
  import { parseDocument } from 'htmlparser2';
16
16
  import { Element, Text } from 'domhandler';
17
- import type { ChildNode } from 'domhandler';
17
+ import type { ChildNode, ParentNode } from 'domhandler';
18
18
  import * as ntk from 'react-x11/ntk';
19
- import { isSvgRoot } from './dom.js';
19
+ import { isSvgRoot, rawTextOf } from './dom.js';
20
+ import { Cascade } from './css/cascade.js';
21
+ import { parseMediaQuery, parseStylesheet } from './css/parse.js';
22
+ import type { Stylesheet } from './css/parse.js';
23
+ import type { ShapeStyle, ShapeStyles } from './css/shapes.js';
24
+ import type { RootLook } from './css/style.js';
20
25
  import { inkColor, isTransparent, parseColor } from './css/values.js';
21
26
 
22
27
  export { isSvgRoot };
@@ -69,20 +74,6 @@ function absoluteLength(
69
74
  return px >= 0 ? px : null;
70
75
  }
71
76
 
72
- /**
73
- * The `width`/`height` of an inline `<svg>` as CSS: they are presentation
74
- * attributes for the properties of the same names (SVG 2, 5.1.1), so
75
- * `height="50%"` is a percentage of the containing block like a style's.
76
- * A bare number is pixels; null for what CSS would not parse.
77
- */
78
- export function svgSizeHint(value: string): string | null {
79
- const v = value.trim();
80
- if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i.test(v)) return v;
81
- const m = LENGTH.exec(v);
82
- if (!m) return null;
83
- return m[2] ? v : `${parseFloat(m[1])}px`;
84
- }
85
-
86
77
  function viewBoxParts(el: Element): [number, number, number, number] | null {
87
78
  const parts = (svgAttr(el, 'viewBox') ?? '')
88
79
  .trim()
@@ -183,15 +174,35 @@ export class SvgDrawing {
183
174
  private _seen = -1;
184
175
  private _seenLast: ChildNode | null = null;
185
176
  private _seenViewport = '';
177
+ /** The `fill` and `stroke` the view's tree was last given its root's,
178
+ * and what the document's rules gave the elements in it. */
179
+ private _seenPaint = '';
186
180
  /** Whether a length in the tree is a percentage of the viewport; null
187
181
  * until the tree is first read. */
188
182
  private _percent: boolean | null = null;
183
+ /** Whether a `<use>` in the tree is one `SvgView` cannot draw as it
184
+ * stands (`needsExpanding`). */
185
+ private _uses = false;
186
+ /** The document's last node when a `<use>` was last looked up and its
187
+ * element not found: a streamed document may bring it yet, and has
188
+ * when its last node is another. Undefined where none was missing. */
189
+ private _missingAt: ChildNode | null | undefined = undefined;
189
190
  private _failed = false;
190
191
  /** An SVG image's own document, rather than an element of this one. */
191
192
  private readonly _standalone: boolean;
192
193
  /** The background an image's root gives its canvas; undefined until it
193
194
  * is first read. */
194
195
  private _canvas: string | null | undefined = undefined;
196
+ /** An SVG image's own style sheets, its `<style>` elements': read at its
197
+ * first draw, and none for most images. */
198
+ private _sheets: Stylesheet[] | null = null;
199
+ /** A cascade over them for each colour scheme and scale the image is
200
+ * drawn at, and what each gave it: by the viewport it was asked at where
201
+ * a sheet reads the viewport, and under one key where none does. */
202
+ private _cascades = new Map<
203
+ string,
204
+ { cascade: Cascade; own: Map<string, ImagePaint | null> }
205
+ >();
195
206
 
196
207
  constructor(root: Element, intrinsics: IntrinsicSize, standalone = false) {
197
208
  this._root = root;
@@ -203,7 +214,12 @@ export class SvgDrawing {
203
214
  * Draw into a rectangle — the box's content box, in device pixels —
204
215
  * clipped to it. The `viewBox`, where there is one, is fitted to it as
205
216
  * `preserveAspectRatio` says; without one a user unit is a CSS pixel,
206
- * which is `scale` device pixels. `color` is `currentColor`.
217
+ * which is `scale` device pixels. `color` is `currentColor`, and `fill`
218
+ * and `stroke` the root's own where the document's styles set them (SVG
219
+ * 2, 13.2): they are properties, which a style sheet's rule sets over
220
+ * the presentation attribute, and what is in the drawing inherits them
221
+ * from its root. `shapes` is what the rules give the elements inside it,
222
+ * the same way, where they give any.
207
223
  */
208
224
  draw(
209
225
  ctx: ClipContext,
@@ -213,6 +229,9 @@ export class SvgDrawing {
213
229
  h: number,
214
230
  scale: number,
215
231
  color?: string,
232
+ fill: string | null = null,
233
+ stroke: string | null = null,
234
+ shapes: ShapeStyles | null = null,
216
235
  ): void {
217
236
  if (this._failed || !(w > 0 && h > 0)) return;
218
237
  // the mock backend has no path API, and SvgView draws paths
@@ -242,9 +261,12 @@ export class SvgDrawing {
242
261
  }
243
262
  const box = viewBoxOf(root);
244
263
  // the viewport in user units, which a percentage is of
264
+ const paint =
265
+ (fill === null ? '' : `fill:${fill};`) +
266
+ (stroke === null ? '' : `stroke:${stroke};`);
245
267
  const view = box
246
- ? this._viewFor(box[2], box[3])
247
- : this._viewFor(w / scale, h / scale);
268
+ ? this._viewFor(box[2], box[3], paint, shapes)
269
+ : this._viewFor(w / scale, h / scale, paint, shapes);
248
270
  if (!view) return;
249
271
  ctx.save();
250
272
  try {
@@ -294,14 +316,133 @@ export class SvgDrawing {
294
316
  }
295
317
  }
296
318
 
297
- private _viewFor(width: number, height: number): SvgViewLike | null {
319
+ /**
320
+ * Draw an SVG image — an `<img>`'s, a background's, a list marker's — as
321
+ * `draw` does, painted as its own style sheets say. An image is a
322
+ * document of its own: no rule of the page's reaches into it, and its
323
+ * `color` is not the page's. What its `<style>` elements say is given
324
+ * its elements as the page's rules are given a drawing inline, and its
325
+ * root its `color`, `fill` and `stroke`, which an inline root has from
326
+ * its box. `scheme` is the colour scheme of the element that embeds it:
327
+ * what `prefers-color-scheme` answers inside it, as Chrome has it.
328
+ */
329
+ drawImage(
330
+ ctx: ClipContext,
331
+ x: number,
332
+ y: number,
333
+ w: number,
334
+ h: number,
335
+ scale: number,
336
+ scheme: 'light' | 'dark' = 'light',
337
+ ): void {
338
+ if (this._failed || !(w > 0 && h > 0)) return;
339
+ const own = this._ownPaint(w, h, scale, scheme);
340
+ this.draw(
341
+ ctx,
342
+ x,
343
+ y,
344
+ w,
345
+ h,
346
+ scale,
347
+ own?.color,
348
+ own?.fill ?? null,
349
+ own?.stroke ?? null,
350
+ own?.shapes ?? null,
351
+ );
352
+ }
353
+
354
+ /** What an image's own style sheets give it, drawn `w` by `h` device
355
+ * pixels, or null where it has none. */
356
+ private _ownPaint(
357
+ w: number,
358
+ h: number,
359
+ scale: number,
360
+ scheme: 'light' | 'dark',
361
+ ): ImagePaint | null {
362
+ const sheets = (this._sheets ??= ownSheets(this._root));
363
+ if (!sheets.length) return null;
364
+ const made = `${scheme}|${scale}`;
365
+ let entry = this._cascades.get(made);
366
+ if (!entry) {
367
+ const cascade = new Cascade(
368
+ sheets,
369
+ imageLook(scheme, scale),
370
+ w,
371
+ h,
372
+ scale,
373
+ null,
374
+ null,
375
+ null,
376
+ null,
377
+ this._root,
378
+ );
379
+ entry = { cascade, own: new Map() };
380
+ this._cascades.set(made, entry);
381
+ }
382
+ const { cascade, own } = entry;
383
+ // An image's viewport is the rectangle it is drawn in, which is what a
384
+ // `@media (min-width)` in it, or a `vw`, is of: the answer is kept by
385
+ // that size only where a sheet asks.
386
+ const sized =
387
+ cascade.breakpoints.length > 0 ||
388
+ cascade.readsViewportWidth ||
389
+ cascade.readsViewportHeight;
390
+ const key = sized ? `${w}x${h}` : '';
391
+ const kept = own.get(key);
392
+ if (kept !== undefined) return kept;
393
+ cascade.viewportWidth = w;
394
+ cascade.viewportHeight = h;
395
+ let paint: ImagePaint | null = null;
396
+ try {
397
+ const root = this._root;
398
+ const style = cascade.styleFor(root, cascade.initial, false);
399
+ const ink = (value: string | null): string | null =>
400
+ value === null || value === 'currentColor' || value === 'none'
401
+ ? value
402
+ : inkColor(value, style.color);
403
+ paint = {
404
+ color: style.color,
405
+ fill: ink(style.fill),
406
+ stroke: ink(style.stroke),
407
+ shapes: cascade.shapeStyles(root, style),
408
+ };
409
+ } catch {
410
+ // a sheet this cannot read leaves the image as its attributes draw it
411
+ }
412
+ // a background drawn at many sizes keeps a few of them
413
+ if (own.size >= 8) own.clear();
414
+ own.set(key, paint);
415
+ return paint;
416
+ }
417
+
418
+ private _viewFor(
419
+ width: number,
420
+ height: number,
421
+ /** The root's `fill` and `stroke` as declarations, or none. */
422
+ paint = '',
423
+ shapes: ShapeStyles | null = null,
424
+ ): SvgViewLike | null {
298
425
  const root = this._root;
299
426
  const count = root.children.length;
300
427
  const last = root.lastChild;
301
- const grown = count !== this._seen || last !== this._seenLast;
302
- if (grown || this._percent === null) this._percent = hasPercent(root);
303
- const viewport = this._percent ? `${width}x${height}` : '';
304
- if (this._view && !grown && viewport === this._seenViewport) {
428
+ // more of the document has arrived since a `<use>` found nothing
429
+ const arrived =
430
+ this._missingAt !== undefined &&
431
+ lastNode(documentOf(root)) !== this._missingAt;
432
+ const grown = count !== this._seen || last !== this._seenLast || arrived;
433
+ if (grown || this._percent === null) {
434
+ this._percent = hasPercent(root);
435
+ this._uses = needsExpanding(root);
436
+ }
437
+ const sized = this._percent || this._uses;
438
+ const viewport = sized ? `${width}x${height}` : '';
439
+ const painted = shapes ? `${paint}|${shapes.key}` : paint;
440
+ if (
441
+ this._view &&
442
+ !grown &&
443
+ viewport === this._seenViewport &&
444
+ painted === this._seenPaint
445
+ ) {
305
446
  return this._view;
306
447
  }
307
448
  const View = svgViewClass();
@@ -311,15 +452,18 @@ export class SvgDrawing {
311
452
  }
312
453
  try {
313
454
  const view = this._view ?? new View(null);
314
- view.setSvgDom(
315
- this._percent || root.name.includes(':')
316
- ? copyTree(root, this._percent ? [width, height] : null)
317
- : root,
318
- );
455
+ const missing = { any: false };
456
+ const tree =
457
+ sized || shapes || root.name.includes(':')
458
+ ? copyTree(root, sized ? [width, height] : null, missing, shapes)
459
+ : root;
460
+ view.setSvgDom(paint ? withPaint(tree, paint) : tree);
461
+ this._missingAt = missing.any ? lastNode(documentOf(root)) : undefined;
319
462
  this._view = view;
320
463
  this._seen = count;
321
464
  this._seenLast = last;
322
465
  this._seenViewport = viewport;
466
+ this._seenPaint = painted;
323
467
  return view;
324
468
  } catch {
325
469
  this._failed = true;
@@ -328,6 +472,21 @@ export class SvgDrawing {
328
472
  }
329
473
  }
330
474
 
475
+ /**
476
+ * A drawing's root with its `fill` and `stroke` the document's: a copy of
477
+ * the root, its children the same ones, with the declarations at the end of
478
+ * its `style` — where `SvgView` reads a root's paint, over its presentation
479
+ * attributes, for what is under it to inherit.
480
+ */
481
+ function withPaint(root: Element, paint: string): Element {
482
+ const style = root.attribs.style;
483
+ return new Element(
484
+ root.name,
485
+ { ...root.attribs, style: style ? `${style};${paint}` : paint },
486
+ root.children,
487
+ );
488
+ }
489
+
331
490
  /**
332
491
  * The colour an SVG image's root element sets as its background, in its
333
492
  * `style` attribute — the one place an image can say it, since
@@ -349,6 +508,75 @@ function canvasBackground(root: Element): string | null {
349
508
  return found;
350
509
  }
351
510
 
511
+ /** What an SVG image's own style sheets give it (`SvgDrawing.drawImage`):
512
+ * its root's `color`, `fill` and `stroke`, and what they give the
513
+ * elements in it. */
514
+ interface ImagePaint {
515
+ color: string;
516
+ fill: string | null;
517
+ stroke: string | null;
518
+ shapes: ShapeStyles | null;
519
+ }
520
+
521
+ /**
522
+ * The style sheets an SVG image's `<style>` elements hold, in order. One
523
+ * that names a type other than CSS's holds none (SVG 2, 6.3), and one with
524
+ * a `media` query holds its rules under it. Its text is its text and CDATA
525
+ * sections, as an XML parse leaves them, and not its comments.
526
+ */
527
+ function ownSheets(root: Element): Stylesheet[] {
528
+ const sheets: Stylesheet[] = [];
529
+ const layers = new Map<string, number>();
530
+ let order = 0;
531
+ const stack: Element[] = [root];
532
+ for (let el = stack.pop(); el; el = stack.pop()) {
533
+ for (let i = el.children.length - 1; i >= 0; i -= 1) {
534
+ const child = el.children[i];
535
+ // an XML parse gives a `<style>` a type of its own, as HTML's does
536
+ if (child.type === 'tag' || child.type === 'style') {
537
+ stack.push(child as Element);
538
+ }
539
+ }
540
+ if (el === root || localName(el.name) !== 'style') continue;
541
+ const type = (el.attribs.type ?? '').trim().toLowerCase();
542
+ if (type && type !== 'text/css') continue;
543
+ const media = el.attribs.media?.trim();
544
+ const sheet = parseStylesheet(
545
+ rawTextOf(el),
546
+ order,
547
+ layers,
548
+ null,
549
+ media ? [parseMediaQuery(media)] : null,
550
+ );
551
+ order += sheet.rules.length + 1;
552
+ sheets.push(sheet);
553
+ }
554
+ return sheets;
555
+ }
556
+
557
+ /**
558
+ * The look an SVG image's own document starts from: CSS's initial values,
559
+ * black at `medium`, since nothing of the page's is inherited into an
560
+ * image — only the colour scheme, which its `prefers-color-scheme` answers.
561
+ */
562
+ function imageLook(scheme: 'light' | 'dark', scale: number): RootLook {
563
+ return {
564
+ color: '#000000',
565
+ fontFamily: 'sans-serif',
566
+ fontSize: 16 * scale,
567
+ monoFamily: 'monospace',
568
+ linkColor: '#0000ee',
569
+ borderColor: '#000000',
570
+ mutedColor: '#808080',
571
+ background: 'transparent',
572
+ colorScheme: scheme,
573
+ surface: '#ffffff',
574
+ controlPadY: 0,
575
+ controlBorder: 0,
576
+ controlRadius: 0,
577
+ };
578
+ }
579
+
352
580
  /** Drawings of inline `<svg>` elements, per element: the element is the
353
581
  * document's, and outlives the box trees built over it. */
354
582
  const INLINE = new WeakMap<Element, SvgDrawing>();
@@ -413,13 +641,163 @@ function hasPercent(root: Element): boolean {
413
641
  return walk(root);
414
642
  }
415
643
 
644
+ /** The element a `<use>` refers to by a fragment, its id: one in another
645
+ * document is none here, where nothing is fetched for a drawing. */
646
+ function useTarget(el: Element): string | null {
647
+ const href = el.attribs.href ?? el.attribs['xlink:href'] ?? '';
648
+ return href.length > 1 && href.startsWith('#') ? href.slice(1) : null;
649
+ }
650
+
651
+ /** The elements under a root by their ids, the first of each. */
652
+ function idsUnder(root: Element): Map<string, Element> {
653
+ const ids = new Map<string, Element>();
654
+ const walk = (el: Element): void => {
655
+ for (const child of el.children) {
656
+ if (child.type !== 'tag') continue;
657
+ const tag = child as Element;
658
+ const id = tag.attribs.id;
659
+ if (id && !ids.has(id)) ids.set(id, tag);
660
+ walk(tag);
661
+ }
662
+ };
663
+ walk(root);
664
+ return ids;
665
+ }
666
+
667
+ /**
668
+ * Whether a `<use>` under the root is one `SvgView` cannot draw as it
669
+ * stands: it looks a reference up among the root's own elements, and draws
670
+ * a `<symbol>` as its children where they are. So one to an element
671
+ * outside the root — an icon sprite's, a hidden `<svg>` of symbols at the
672
+ * top of the page — and one to a symbol with a `viewBox` of its own are
673
+ * drawn from a copy (`copyTree`).
674
+ */
675
+ function needsExpanding(root: Element): boolean {
676
+ let ids: Map<string, Element> | null = null;
677
+ const walk = (el: Element): boolean => {
678
+ for (const child of el.children) {
679
+ if (child.type !== 'tag') continue;
680
+ const tag = child as Element;
681
+ if (localName(tag.name) === 'use') {
682
+ const id = useTarget(tag);
683
+ if (id !== null) {
684
+ ids ??= idsUnder(root);
685
+ const target = ids.get(id);
686
+ if (!target) return true;
687
+ if (localName(target.name) === 'symbol' && viewBoxOf(target)) {
688
+ return true;
689
+ }
690
+ }
691
+ }
692
+ if (walk(tag)) return true;
693
+ }
694
+ return false;
695
+ };
696
+ return walk(root);
697
+ }
698
+
699
+ function documentOf(el: Element): ParentNode {
700
+ let top: ParentNode = el;
701
+ while (top.parent) top = top.parent;
702
+ return top;
703
+ }
704
+
705
+ /** The last node of a document, in its order: what a document that is
706
+ * still arriving has another of once more of it has. */
707
+ function lastNode(top: ParentNode): ChildNode | null {
708
+ let last: ChildNode | null = top.lastChild;
709
+ while (last && 'lastChild' in last && last.lastChild) last = last.lastChild;
710
+ return last;
711
+ }
712
+
713
+ /** The first element of a document with an id, in its order. */
714
+ function elementById(top: ParentNode, id: string): Element | null {
715
+ const stack: ChildNode[] = [...top.children].reverse();
716
+ for (let node = stack.pop(); node; node = stack.pop()) {
717
+ if (node.type !== 'tag') continue;
718
+ const el = node as Element;
719
+ if (el.attribs.id === id) return el;
720
+ for (let i = el.children.length - 1; i >= 0; i -= 1) {
721
+ stack.push(el.children[i]);
722
+ }
723
+ }
724
+ return null;
725
+ }
726
+
727
+ /** How deep a `<use>` may be of a `<use>`: one that reaches itself stops. */
728
+ const USE_DEPTH = 8;
729
+
730
+ /** A `<use>`'s `width` or `height`, in user units: a number, or a
731
+ * percentage of the viewport's; null for none, and for `auto`. */
732
+ function useLength(value: string | undefined, of: number): number | null {
733
+ if (value === undefined) return null;
734
+ const n = parseFloat(value);
735
+ if (!Number.isFinite(n) || n < 0) return null;
736
+ return value.trim().endsWith('%') ? (n / 100) * of : n;
737
+ }
738
+
739
+ /** The transform that fits a `viewBox` into a viewport at `x`, `y`, as
740
+ * `preserveAspectRatio` says (SVG 2, 8.2). */
741
+ function fitTransform(
742
+ box: [number, number, number, number],
743
+ x: number,
744
+ y: number,
745
+ width: number,
746
+ height: number,
747
+ preserve: string | undefined,
748
+ ): string {
749
+ const fit = (preserve ?? '').trim().split(/\s+/);
750
+ if (fit[0] === 'defer') fit.shift();
751
+ let sx = width / box[2];
752
+ let sy = height / box[3];
753
+ if (fit[0] !== 'none') {
754
+ const align = ALIGN.exec(fit[0] ?? '') ?? ['xMidYMid', 'Mid', 'Mid'];
755
+ const scale = fit[1] === 'slice' ? Math.max(sx, sy) : Math.min(sx, sy);
756
+ x += (width - box[2] * scale) * AT[align[1]];
757
+ y += (height - box[3] * scale) * AT[align[2]];
758
+ sx = scale;
759
+ sy = scale;
760
+ }
761
+ return `translate(${x - box[0] * sx},${y - box[1] * sy}) scale(${sx},${sy})`;
762
+ }
763
+
764
+ /** What a `<use>` says of where its element goes, and not of how it looks:
765
+ * the rest is the group's the element is drawn in. */
766
+ const USE_PLACEMENT = new Set([
767
+ 'href',
768
+ 'xlink:href',
769
+ 'x',
770
+ 'y',
771
+ 'width',
772
+ 'height',
773
+ ]);
774
+
416
775
  /**
417
776
  * The tree `SvgView` reads, where the document's own will not do: with
418
777
  * local names — `SvgView` knows `rect`, not `svg:rect`, and a prefix is
419
778
  * dropped only where it is bound to SVG — and with its percentages resolved
420
- * against a viewport of `[width, height]` user units, where one is given.
779
+ * against a viewport of `[width, height]` user units, where one is given;
780
+ * and with each `<use>` that `SvgView` cannot draw (`needsExpanding`) as a
781
+ * group of what it refers to (SVG 2, 5.5): the element, from wherever in
782
+ * the document it is, or a symbol's children, its `viewBox` fitted to the
783
+ * viewport the `<use>` gives it — its `width` and `height`, and all of the
784
+ * drawing's where it has none. `missing.any` is set where one refers to an
785
+ * element the document does not have.
786
+ *
787
+ * And with what the document's rules give each element (`shapes`), where
788
+ * `SvgView` reads an element's own: at the end of its `style`, over its
789
+ * presentation attributes and over what that attribute says, which the
790
+ * cascade that made them has weighed already. An element a rule gives
791
+ * `display: none` is left out, with what is in it, and a shape one hides
792
+ * — `visibility` is inherited, and a shape in a hidden group may be shown
793
+ * again — is left out alone.
421
794
  */
422
- function copyTree(root: Element, viewport: [number, number] | null): Element {
795
+ function copyTree(
796
+ root: Element,
797
+ viewport: [number, number] | null,
798
+ missing?: { any: boolean },
799
+ shapes: ShapeStyles | null = null,
800
+ ): Element {
423
801
  const colon = root.name.indexOf(':');
424
802
  const prefix = colon < 0 ? null : `${root.name.slice(0, colon)}:`;
425
803
  const strip = (name: string): string =>
@@ -427,9 +805,104 @@ function copyTree(root: Element, viewport: [number, number] | null): Element {
427
805
  const diagonal = viewport
428
806
  ? Math.sqrt((viewport[0] ** 2 + viewport[1] ** 2) / 2)
429
807
  : 0;
430
- const copy = (el: Element, resolve: boolean): Element => {
808
+ let ids: Map<string, Element> | null = null;
809
+ const expand = (
810
+ use: Element,
811
+ resolve: boolean,
812
+ depth: number,
813
+ hidden: boolean,
814
+ ): Element | null => {
815
+ const id = useTarget(use);
816
+ if (id === null || depth >= USE_DEPTH) return null;
817
+ ids ??= idsUnder(root);
818
+ const inside = ids.get(id);
819
+ const target = inside ?? elementById(documentOf(root), id);
820
+ if (!target) {
821
+ if (missing) missing.any = true;
822
+ return null;
823
+ }
824
+ const symbol = localName(target.name) === 'symbol';
825
+ const box = symbol ? viewBoxOf(target) : null;
826
+ // one of the root's own with no viewport to fit: `SvgView` draws it
827
+ if (inside && !box) return null;
828
+ const attribs: Record<string, string> = {};
829
+ for (const name in use.attribs) {
830
+ if (!USE_PLACEMENT.has(name)) attribs[name] = use.attribs[name];
831
+ }
832
+ const own = shapes?.of.get(use);
833
+ if (own) restyle(attribs, own);
834
+ const x = parseFloat(use.attribs.x ?? '') || 0;
835
+ const y = parseFloat(use.attribs.y ?? '') || 0;
836
+ let placed = '';
837
+ if (box && viewport) {
838
+ const width =
839
+ useLength(use.attribs.width, viewport[0]) ??
840
+ useLength(target.attribs.width, viewport[0]) ??
841
+ viewport[0];
842
+ const height =
843
+ useLength(use.attribs.height, viewport[1]) ??
844
+ useLength(target.attribs.height, viewport[1]) ??
845
+ viewport[1];
846
+ placed = fitTransform(
847
+ box,
848
+ x,
849
+ y,
850
+ width,
851
+ height,
852
+ svgAttr(target, 'preserveAspectRatio'),
853
+ );
854
+ } else if (x || y) placed = `translate(${x},${y})`;
855
+ const transform = `${attribs.transform ?? ''} ${placed}`.trim();
856
+ if (transform) attribs.transform = transform;
857
+ // a length in a symbol with a `viewBox` is of that, and left as it is
858
+ const within = resolve && !box;
859
+ const children: ChildNode[] = [];
860
+ if (symbol) copyInto(children, target, within, depth + 1, hidden);
861
+ else if (shown(target, hidden)) {
862
+ children.push(copy(target, within, depth + 1, hidden));
863
+ }
864
+ return new Element('g', attribs, children);
865
+ };
866
+ /** Whether an element is drawn at all, in a group that is hidden or
867
+ * not: one no rule gives `display: none`, and no shape a rule hides. */
868
+ const shown = (el: Element, hidden: boolean): boolean => {
869
+ if (!shapes) return true;
870
+ const own = shapes.of.get(el);
871
+ const name = localName(el.name);
872
+ if (own?.display === 'none' && !UNDRAWN.has(name)) return false;
873
+ return !(hiddenIn(own, hidden) && HIDEABLE.has(name));
874
+ };
875
+ const copyInto = (
876
+ children: ChildNode[],
877
+ el: Element,
878
+ resolve: boolean,
879
+ depth: number,
880
+ hidden: boolean,
881
+ ): void => {
882
+ for (const child of el.children) {
883
+ if (child.type === 'tag') {
884
+ if (shown(child as Element, hidden)) {
885
+ children.push(copy(child as Element, resolve, depth, hidden));
886
+ }
887
+ } else if (child.type === 'text') children.push(new Text(child.data));
888
+ }
889
+ };
890
+ const copy = (
891
+ el: Element,
892
+ resolve: boolean,
893
+ depth = 0,
894
+ /** Whether the element inherits a `visibility` of `hidden`. */
895
+ hidden = false,
896
+ ): Element => {
897
+ const own = shapes?.of.get(el);
898
+ hidden = hiddenIn(own, hidden);
899
+ if (localName(el.name) === 'use') {
900
+ const group = expand(el, resolve, depth, hidden);
901
+ if (group) return group;
902
+ }
431
903
  const here = resolve && !OWN_UNITS.has(localName(el.name));
432
904
  const attribs = { ...el.attribs };
905
+ if (own) restyle(attribs, own);
433
906
  if (here && viewport && el !== root) {
434
907
  for (const name in attribs) {
435
908
  const axis = PERCENT_OF[name];
@@ -443,15 +916,79 @@ function copyTree(root: Element, viewport: [number, number] | null): Element {
443
916
  }
444
917
  }
445
918
  const children: ChildNode[] = [];
446
- for (const child of el.children) {
447
- if (child.type === 'tag') children.push(copy(child as Element, here));
448
- else if (child.type === 'text') children.push(new Text(child.data));
449
- }
919
+ copyInto(children, el, here, depth, hidden);
450
920
  return new Element(strip(el.name), attribs, children);
451
921
  };
452
922
  return copy(root, true);
453
923
  }
454
924
 
925
+ /** What `display: none` takes nothing from: `SvgView` draws none of them
926
+ * where they stand, and what refers to one still finds it (SVG 2, 5.3). */
927
+ const UNDRAWN = new Set([
928
+ 'defs',
929
+ 'symbol',
930
+ 'lineargradient',
931
+ 'radialgradient',
932
+ 'stop',
933
+ 'clippath',
934
+ 'mask',
935
+ 'pattern',
936
+ 'marker',
937
+ 'style',
938
+ ]);
939
+
940
+ /** What `visibility: hidden` leaves undrawn: the elements with something
941
+ * of their own to draw, and a `<use>`, since what it draws inherits the
942
+ * property from it. */
943
+ const HIDEABLE = new Set([
944
+ 'path',
945
+ 'rect',
946
+ 'circle',
947
+ 'ellipse',
948
+ 'line',
949
+ 'polyline',
950
+ 'polygon',
951
+ 'text',
952
+ 'image',
953
+ 'use',
954
+ ]);
955
+
956
+ /** Whether an element is hidden, given whether what it is in is. */
957
+ function hiddenIn(own: ShapeStyle | undefined, hidden: boolean): boolean {
958
+ const visibility = own?.visibility;
959
+ return visibility === undefined || visibility === 'inherit'
960
+ ? hidden
961
+ : visibility === 'hidden';
962
+ }
963
+
964
+ /**
965
+ * An element's attributes with what the rules give it: each property at
966
+ * the end of its `style`, where `SvgView` reads it over the attribute of
967
+ * its name — but `opacity`, which it reads from the attribute alone. One
968
+ * the rules put back to what the element inherits is taken out of both.
969
+ */
970
+ function restyle(attribs: Record<string, string>, own: ShapeStyle): void {
971
+ let style = attribs.style ?? '';
972
+ let added = '';
973
+ for (const prop in own) {
974
+ const value = own[prop];
975
+ if (prop === 'display' || prop === 'visibility') continue;
976
+ if (value === 'inherit') {
977
+ delete attribs[prop];
978
+ if (style) {
979
+ style = style
980
+ .split(';')
981
+ .filter((d) => d.slice(0, d.indexOf(':')).trim() !== prop)
982
+ .join(';');
983
+ }
984
+ } else if (prop === 'opacity') attribs.opacity = value;
985
+ else added += `${prop}:${value};`;
986
+ }
987
+ style = style && added ? `${style};${added}` : style || added;
988
+ if (style) attribs.style = style;
989
+ else delete attribs.style;
990
+ }
991
+
455
992
  /**
456
993
  * An SVG image's bytes as a drawing, or null for bytes that are not SVG —
457
994
  * which is how a PNG or a JPEG goes on to the image decoder. Nothing names