@react-x11/components 0.14.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 (51) hide show
  1. package/dist/html/css/cascade.d.ts +42 -1
  2. package/dist/html/css/cascade.d.ts.map +1 -1
  3. package/dist/html/css/cascade.js +310 -12
  4. package/dist/html/css/cascade.js.map +1 -1
  5. package/dist/html/css/shapes.d.ts +44 -0
  6. package/dist/html/css/shapes.d.ts.map +1 -0
  7. package/dist/html/css/shapes.js +194 -0
  8. package/dist/html/css/shapes.js.map +1 -0
  9. package/dist/html/css/style.d.ts.map +1 -1
  10. package/dist/html/css/style.js +1 -15
  11. package/dist/html/css/style.js.map +1 -1
  12. package/dist/html/layout/block.d.ts +16 -5
  13. package/dist/html/layout/block.d.ts.map +1 -1
  14. package/dist/html/layout/block.js +26 -5
  15. package/dist/html/layout/block.js.map +1 -1
  16. package/dist/html/layout/boxes.d.ts +13 -0
  17. package/dist/html/layout/boxes.d.ts.map +1 -1
  18. package/dist/html/layout/boxes.js +18 -0
  19. package/dist/html/layout/boxes.js.map +1 -1
  20. package/dist/html/layout/css-grid.d.ts.map +1 -1
  21. package/dist/html/layout/css-grid.js +58 -10
  22. package/dist/html/layout/css-grid.js.map +1 -1
  23. package/dist/html/layout/flex.js +19 -6
  24. package/dist/html/layout/flex.js.map +1 -1
  25. package/dist/html/layout/table.d.ts.map +1 -1
  26. package/dist/html/layout/table.js +38 -16
  27. package/dist/html/layout/table.js.map +1 -1
  28. package/dist/html/node.d.ts +1 -0
  29. package/dist/html/node.d.ts.map +1 -1
  30. package/dist/html/node.js +57 -12
  31. package/dist/html/node.js.map +1 -1
  32. package/dist/html/paint.d.ts +4 -1
  33. package/dist/html/paint.d.ts.map +1 -1
  34. package/dist/html/paint.js +269 -84
  35. package/dist/html/paint.js.map +1 -1
  36. package/dist/html/svg.d.ts +27 -10
  37. package/dist/html/svg.d.ts.map +1 -1
  38. package/dist/html/svg.js +259 -46
  39. package/dist/html/svg.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/html/css/cascade.ts +343 -8
  42. package/src/html/css/shapes.ts +239 -0
  43. package/src/html/css/style.ts +1 -13
  44. package/src/html/layout/block.ts +27 -5
  45. package/src/html/layout/boxes.ts +30 -0
  46. package/src/html/layout/css-grid.ts +77 -13
  47. package/src/html/layout/flex.ts +22 -9
  48. package/src/html/layout/table.ts +45 -18
  49. package/src/html/node.ts +54 -11
  50. package/src/html/paint.ts +379 -90
  51. package/src/html/svg.ts +318 -39
package/src/html/svg.ts CHANGED
@@ -16,7 +16,12 @@ import { parseDocument } from 'htmlparser2';
16
16
  import { Element, Text } from 'domhandler';
17
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,7 +174,8 @@ export class SvgDrawing {
183
174
  private _seen = -1;
184
175
  private _seenLast: ChildNode | null = null;
185
176
  private _seenViewport = '';
186
- /** The `fill` and `stroke` the view's tree was last given its root's. */
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. */
187
179
  private _seenPaint = '';
188
180
  /** Whether a length in the tree is a percentage of the viewport; null
189
181
  * until the tree is first read. */
@@ -201,6 +193,16 @@ export class SvgDrawing {
201
193
  /** The background an image's root gives its canvas; undefined until it
202
194
  * is first read. */
203
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
+ >();
204
206
 
205
207
  constructor(root: Element, intrinsics: IntrinsicSize, standalone = false) {
206
208
  this._root = root;
@@ -216,7 +218,8 @@ export class SvgDrawing {
216
218
  * and `stroke` the root's own where the document's styles set them (SVG
217
219
  * 2, 13.2): they are properties, which a style sheet's rule sets over
218
220
  * the presentation attribute, and what is in the drawing inherits them
219
- * from its root.
221
+ * from its root. `shapes` is what the rules give the elements inside it,
222
+ * the same way, where they give any.
220
223
  */
221
224
  draw(
222
225
  ctx: ClipContext,
@@ -228,6 +231,7 @@ export class SvgDrawing {
228
231
  color?: string,
229
232
  fill: string | null = null,
230
233
  stroke: string | null = null,
234
+ shapes: ShapeStyles | null = null,
231
235
  ): void {
232
236
  if (this._failed || !(w > 0 && h > 0)) return;
233
237
  // the mock backend has no path API, and SvgView draws paths
@@ -261,8 +265,8 @@ export class SvgDrawing {
261
265
  (fill === null ? '' : `fill:${fill};`) +
262
266
  (stroke === null ? '' : `stroke:${stroke};`);
263
267
  const view = box
264
- ? this._viewFor(box[2], box[3], paint)
265
- : this._viewFor(w / scale, h / scale, paint);
268
+ ? this._viewFor(box[2], box[3], paint, shapes)
269
+ : this._viewFor(w / scale, h / scale, paint, shapes);
266
270
  if (!view) return;
267
271
  ctx.save();
268
272
  try {
@@ -312,11 +316,111 @@ export class SvgDrawing {
312
316
  }
313
317
  }
314
318
 
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
+
315
418
  private _viewFor(
316
419
  width: number,
317
420
  height: number,
318
421
  /** The root's `fill` and `stroke` as declarations, or none. */
319
422
  paint = '',
423
+ shapes: ShapeStyles | null = null,
320
424
  ): SvgViewLike | null {
321
425
  const root = this._root;
322
426
  const count = root.children.length;
@@ -332,11 +436,12 @@ export class SvgDrawing {
332
436
  }
333
437
  const sized = this._percent || this._uses;
334
438
  const viewport = sized ? `${width}x${height}` : '';
439
+ const painted = shapes ? `${paint}|${shapes.key}` : paint;
335
440
  if (
336
441
  this._view &&
337
442
  !grown &&
338
443
  viewport === this._seenViewport &&
339
- paint === this._seenPaint
444
+ painted === this._seenPaint
340
445
  ) {
341
446
  return this._view;
342
447
  }
@@ -349,16 +454,16 @@ export class SvgDrawing {
349
454
  const view = this._view ?? new View(null);
350
455
  const missing = { any: false };
351
456
  const tree =
352
- sized || root.name.includes(':')
353
- ? copyTree(root, sized ? [width, height] : null, missing)
457
+ sized || shapes || root.name.includes(':')
458
+ ? copyTree(root, sized ? [width, height] : null, missing, shapes)
354
459
  : root;
355
- view.setSvgDom(paint ? painted(tree, paint) : tree);
460
+ view.setSvgDom(paint ? withPaint(tree, paint) : tree);
356
461
  this._missingAt = missing.any ? lastNode(documentOf(root)) : undefined;
357
462
  this._view = view;
358
463
  this._seen = count;
359
464
  this._seenLast = last;
360
465
  this._seenViewport = viewport;
361
- this._seenPaint = paint;
466
+ this._seenPaint = painted;
362
467
  return view;
363
468
  } catch {
364
469
  this._failed = true;
@@ -373,7 +478,7 @@ export class SvgDrawing {
373
478
  * its `style` — where `SvgView` reads a root's paint, over its presentation
374
479
  * attributes, for what is under it to inherit.
375
480
  */
376
- function painted(root: Element, paint: string): Element {
481
+ function withPaint(root: Element, paint: string): Element {
377
482
  const style = root.attribs.style;
378
483
  return new Element(
379
484
  root.name,
@@ -403,6 +508,75 @@ function canvasBackground(root: Element): string | null {
403
508
  return found;
404
509
  }
405
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
+
406
580
  /** Drawings of inline `<svg>` elements, per element: the element is the
407
581
  * document's, and outlives the box trees built over it. */
408
582
  const INLINE = new WeakMap<Element, SvgDrawing>();
@@ -609,11 +783,20 @@ const USE_PLACEMENT = new Set([
609
783
  * viewport the `<use>` gives it — its `width` and `height`, and all of the
610
784
  * drawing's where it has none. `missing.any` is set where one refers to an
611
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.
612
794
  */
613
795
  function copyTree(
614
796
  root: Element,
615
797
  viewport: [number, number] | null,
616
798
  missing?: { any: boolean },
799
+ shapes: ShapeStyles | null = null,
617
800
  ): Element {
618
801
  const colon = root.name.indexOf(':');
619
802
  const prefix = colon < 0 ? null : `${root.name.slice(0, colon)}:`;
@@ -627,6 +810,7 @@ function copyTree(
627
810
  use: Element,
628
811
  resolve: boolean,
629
812
  depth: number,
813
+ hidden: boolean,
630
814
  ): Element | null => {
631
815
  const id = useTarget(use);
632
816
  if (id === null || depth >= USE_DEPTH) return null;
@@ -645,6 +829,8 @@ function copyTree(
645
829
  for (const name in use.attribs) {
646
830
  if (!USE_PLACEMENT.has(name)) attribs[name] = use.attribs[name];
647
831
  }
832
+ const own = shapes?.of.get(use);
833
+ if (own) restyle(attribs, own);
648
834
  const x = parseFloat(use.attribs.x ?? '') || 0;
649
835
  const y = parseFloat(use.attribs.y ?? '') || 0;
650
836
  let placed = '';
@@ -671,22 +857,52 @@ function copyTree(
671
857
  // a length in a symbol with a `viewBox` is of that, and left as it is
672
858
  const within = resolve && !box;
673
859
  const children: ChildNode[] = [];
674
- if (symbol) {
675
- for (const child of target.children) {
676
- if (child.type === 'tag') {
677
- children.push(copy(child as Element, within, depth + 1));
678
- }
679
- }
680
- } else children.push(copy(target, within, depth + 1));
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
+ }
681
864
  return new Element('g', attribs, children);
682
865
  };
683
- const copy = (el: Element, resolve: boolean, depth = 0): Element => {
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);
684
899
  if (localName(el.name) === 'use') {
685
- const group = expand(el, resolve, depth);
900
+ const group = expand(el, resolve, depth, hidden);
686
901
  if (group) return group;
687
902
  }
688
903
  const here = resolve && !OWN_UNITS.has(localName(el.name));
689
904
  const attribs = { ...el.attribs };
905
+ if (own) restyle(attribs, own);
690
906
  if (here && viewport && el !== root) {
691
907
  for (const name in attribs) {
692
908
  const axis = PERCENT_OF[name];
@@ -700,16 +916,79 @@ function copyTree(
700
916
  }
701
917
  }
702
918
  const children: ChildNode[] = [];
703
- for (const child of el.children) {
704
- if (child.type === 'tag') {
705
- children.push(copy(child as Element, here, depth));
706
- } else if (child.type === 'text') children.push(new Text(child.data));
707
- }
919
+ copyInto(children, el, here, depth, hidden);
708
920
  return new Element(strip(el.name), attribs, children);
709
921
  };
710
922
  return copy(root, true);
711
923
  }
712
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
+
713
992
  /**
714
993
  * An SVG image's bytes as a drawing, or null for bytes that are not SVG —
715
994
  * which is how a PNG or a JPEG goes on to the image decoder. Nothing names