@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
@@ -21,7 +21,6 @@ import { Element as DomElement, isTag } from 'domhandler';
21
21
  import type { Element } from 'domhandler';
22
22
 
23
23
  import { attr, tagOf } from '../dom.js';
24
- import { svgSizeHint } from '../svg.js';
25
24
  import {
26
25
  asciiLower,
27
26
  escapeEnd,
@@ -54,6 +53,13 @@ import type { UnitContext } from './values.js';
54
53
  import { viewportUnit } from './values.js';
55
54
  import { customProperties, substituteIn } from './vars.js';
56
55
  import type { CustomProps } from './vars.js';
56
+ import {
57
+ ROOT_BOX_PROPS,
58
+ SHAPE_TAGS,
59
+ isShapeProp,
60
+ shapeValue,
61
+ } from './shapes.js';
62
+ import type { ShapeContext, ShapeStyle, ShapeStyles } from './shapes.js';
57
63
 
58
64
  /** Where a declaration came from. Higher wins before specificity is asked. */
59
65
  const enum Origin {
@@ -139,6 +145,14 @@ interface IndexedRule {
139
145
  compiled: boolean;
140
146
  /** Which rule this is, in a sharing key (`Cascade.sharedStyleFor`). */
141
147
  id: number;
148
+ /** For a rule asked of the elements in a drawing by their type, or of
149
+ * them all (`Cascade.shapeStyles`): the ids and classes it asks of its
150
+ * subject's ancestors (`ancestorKeys`). Null until a drawing asks, and
151
+ * in every other rule. */
152
+ needs: readonly string[] | null;
153
+ /** For a rule kept for drawings: whether it declares something a
154
+ * drawing's root does not have from its box's style. Null until asked. */
155
+ root: boolean | null;
142
156
  }
143
157
 
144
158
  /**
@@ -194,6 +208,11 @@ class RuleIndex {
194
208
  */
195
209
  reaches(el: Element): boolean {
196
210
  if (this.universal.length || this.byTag.has(tagOf(el))) return true;
211
+ return this.names(el);
212
+ }
213
+
214
+ /** Whether a bucket here is for `el`'s id, or for a class of its. */
215
+ names(el: Element): boolean {
197
216
  if (this.byId.size) {
198
217
  const id = attr(el, 'id');
199
218
  if (id && this.byId.has(id)) return true;
@@ -209,18 +228,19 @@ class RuleIndex {
209
228
  return false;
210
229
  }
211
230
 
212
- add(rule: StyleRule): void {
231
+ add(rule: StyleRule, key = rightmostKey(rule.selector)): void {
213
232
  this.size += 1;
214
233
  const indexed: IndexedRule = {
215
234
  rule,
216
235
  match: null,
217
236
  compiled: false,
218
237
  id: this._nextId++,
238
+ needs: null,
239
+ root: null,
219
240
  };
220
241
  if (rule.selector.includes(':hover') || rule.selector.includes(':active')) {
221
242
  this.hoverSensitive = true;
222
243
  }
223
- const key = rightmostKey(rule.selector);
224
244
  if (UNSHAREABLE.test(rule.selector)) {
225
245
  if (key.kind === 'id') this.ownStyleIds.add(key.name);
226
246
  else if (key.kind === 'class') this.ownStyleClasses.add(key.name);
@@ -576,11 +596,12 @@ const POINTER_PSEUDO = /:(?:hover|active)(?![\w-])/i;
576
596
  function compileSelector(
577
597
  selector: string,
578
598
  adapter: CssSelectAdapter,
599
+ pseudos: typeof PSEUDOS = PSEUDOS,
579
600
  ): (el: Element) => boolean {
580
601
  return compile(noEmptyWords(selector), {
581
602
  adapter,
582
603
  xmlMode: false,
583
- pseudos: PSEUDOS,
604
+ pseudos,
584
605
  cacheResults: !POINTER_PSEUDO.test(selector),
585
606
  } as unknown as Parameters<typeof compile>[1]) as unknown as (
586
607
  node: Element,
@@ -641,7 +662,14 @@ function rightmostKey(selector: string): {
641
662
  start = i + 1;
642
663
  }
643
664
  }
644
- const compound = selector.slice(start);
665
+ return compoundKey(selector.slice(start));
666
+ }
667
+
668
+ /** A compound selector's most selective key (`rightmostKey`). */
669
+ function compoundKey(compound: string): {
670
+ kind: 'id' | 'class' | 'tag' | 'any';
671
+ name: string;
672
+ } {
645
673
  let id: string | null = null;
646
674
  let cls: string | null = null;
647
675
  let tag: string | null = null;
@@ -675,6 +703,39 @@ function rightmostKey(selector: string): {
675
703
  return { kind: 'any', name: '' };
676
704
  }
677
705
 
706
+ /**
707
+ * The ids and classes a selector asks of its subject's ancestors: for each
708
+ * compound a descendant or a child combinator follows, its id, or else its
709
+ * first class, as `#name` or `.name`. Such a compound is an ancestor of
710
+ * the subject — one a sibling combinator follows is a sibling of one — so
711
+ * an element with no ancestor of some such name is not the rule's, which
712
+ * is known without matching it. A compound with neither asks nothing.
713
+ */
714
+ function ancestorKeys(selector: string): string[] {
715
+ const keys: string[] = [];
716
+ const compounds = compoundsOf(selector);
717
+ for (let i = 0; i < compounds.length - 1; i += 1) {
718
+ const { text, next } = compounds[i];
719
+ if (next !== ' ' && next !== '>') continue;
720
+ const key = compoundKey(text);
721
+ if (key.kind === 'id') keys.push(`#${key.name}`);
722
+ else if (key.kind === 'class') keys.push(`.${key.name}`);
723
+ }
724
+ return keys;
725
+ }
726
+
727
+ /** What the rules asked of every element of a drawing, or of every one of
728
+ * a type, ask of its ancestors (`Cascade.shapeStyles`). */
729
+ interface ShapeNeeds {
730
+ /** The classes and the ids some such rule names in an ancestor. */
731
+ classes: Set<string>;
732
+ ids: Set<string>;
733
+ /** Whether a rule asked of every element asks nothing of its ancestors,
734
+ * and the types with such a rule. */
735
+ universal: boolean;
736
+ tags: Set<string>;
737
+ }
738
+
678
739
  function balancedEnd(
679
740
  text: string,
680
741
  from: number,
@@ -763,6 +824,13 @@ export class Cascade {
763
824
  marker: new RuleIndex(),
764
825
  selection: new RuleIndex(),
765
826
  };
827
+ /** The author's rules that could style a shape in a drawing: the ones
828
+ * that declare a property a shape has (`shapes.ts`) and whose subject
829
+ * is no element of HTML's alone. They are in `_index` too, for the
830
+ * elements that have boxes; these are asked of the ones in an inline
831
+ * `<svg>`, which have none (`shapeStyles`). */
832
+ private _shapes = new RuleIndex();
833
+ private _shapeNeeds: ShapeNeeds | null = null;
766
834
  private _adapter: CssSelectAdapter;
767
835
  /** The compounds of the selectors that test the pointer, each without
768
836
  * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
@@ -837,6 +905,8 @@ export class Cascade {
837
905
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
838
906
  * the line height settled ahead of the declarations that read it. */
839
907
  private _lh = false;
908
+ /** The pseudo-classes css-select is handed, `:root` this cascade's. */
909
+ private _pseudos: typeof PSEUDOS;
840
910
 
841
911
  constructor(
842
912
  sheets: Stylesheet[],
@@ -848,7 +918,13 @@ export class Cascade {
848
918
  zeroWidth: FaceMetric | null = null,
849
919
  normalLine: FaceMetric | null = null,
850
920
  families: FontFamilies | null = null,
921
+ /** The document's element where it is not an `<html>`: an SVG image's
922
+ * `<svg>`, which is `:root` there (Selectors 4, 14.1). */
923
+ documentElement: Element | null = null,
851
924
  ) {
925
+ this._pseudos = documentElement
926
+ ? { ...PSEUDOS, root: (el: Element) => el === documentElement }
927
+ : PSEUDOS;
852
928
  this._xHeightOf = xHeight;
853
929
  this._zeroWidthOf = zeroWidth;
854
930
  this._normalLineOf = normalLine;
@@ -875,7 +951,17 @@ export class Cascade {
875
951
  if (!this._lh && usesLh(rule.declarations)) this._lh = true;
876
952
  const pseudo = splitPseudoElement(rule);
877
953
  if (pseudo) this._pseudo[pseudo.which].add(pseudo.rule);
878
- else this._index.add(rule);
954
+ else {
955
+ const key = rightmostKey(rule.selector);
956
+ this._index.add(rule, key);
957
+ if (
958
+ rule.order >= 0 &&
959
+ (key.kind !== 'tag' || SHAPE_TAGS.has(key.name)) &&
960
+ declaresShape(rule.declarations)
961
+ ) {
962
+ this._shapes.add(rule, key);
963
+ }
964
+ }
879
965
  this._noteHover((pseudo?.rule ?? rule).selector);
880
966
  }
881
967
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
@@ -1052,7 +1138,7 @@ export class Cascade {
1052
1138
  * it, as a rule it refuses drops out of the cascade. */
1053
1139
  private _compile(selector: string): ((el: Element) => boolean) | null {
1054
1140
  try {
1055
- return compileSelector(selector, this._adapter);
1141
+ return compileSelector(selector, this._adapter, this._pseudos);
1056
1142
  } catch {
1057
1143
  return null;
1058
1144
  }
@@ -1743,6 +1829,185 @@ export class Cascade {
1743
1829
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1744
1830
  }
1745
1831
 
1832
+ /** Whether any rule could style a shape in a drawing: a document with
1833
+ * none asks nothing of its drawings (`shapeStyles`). */
1834
+ get stylesShapes(): boolean {
1835
+ return this._shapes.size > 0;
1836
+ }
1837
+
1838
+ /** Whether a pointer move can change what a rule gives a shape in a
1839
+ * drawing. */
1840
+ get shapesFollowPointer(): boolean {
1841
+ return this._shapes.hoverSensitive;
1842
+ }
1843
+
1844
+ /**
1845
+ * What the rules give the elements of an inline drawing, `root` and what
1846
+ * is in it, or null where none reaches one: the narrow cascade of
1847
+ * `shapes.ts`. `style` is the root's computed style — its `color`, its
1848
+ * custom properties and its font size are what a value in the drawing is
1849
+ * read against, as though every element in it had inherited them, which
1850
+ * all but one a rule gives its own has.
1851
+ *
1852
+ * An element's rules and its `style` attribute are put in cascade order,
1853
+ * and each property is the last valid declaration's. The presentation
1854
+ * attributes are below them all (SVG 2, 6.2), and `SvgView` reads those
1855
+ * itself. The root's box has its paint, its opacity and whether it is
1856
+ * shown from its computed style, so those are left out for it.
1857
+ *
1858
+ * Asked as a drawing is first painted (`BoxTree.shapeStyler`), of the
1859
+ * rules in `_shapes` alone: a page of hundreds of icons pays for the ones
1860
+ * it shows, a look in the buckets for each of their elements where no
1861
+ * rule reaches them, and a document with no such rule nothing.
1862
+ */
1863
+ shapeStyles(root: Element, style: ComputedStyle): ShapeStyles | null {
1864
+ const index = this._shapes;
1865
+ if (!index.size) return null;
1866
+ // The rules kept by a class or an id are asked of the elements with
1867
+ // that name. The rest — `.dark .logo path`, `.menu [aria-hidden]` — are
1868
+ // asked of every `<path>`, or of every element, and nearly all of them
1869
+ // are for some other part of the page: each says so by the class or the
1870
+ // id it asks of an ancestor, which is looked for once for the drawing,
1871
+ // around it and in it, where matching the rule would look for it from
1872
+ // each of its elements. On a page with none of them around a drawing,
1873
+ // nothing is matched at all.
1874
+ const needs = (this._shapeNeeds ??= this._needsOfShapes());
1875
+ const found = new Set<string>();
1876
+ const asks = needs.classes.size > 0 || needs.ids.size > 0;
1877
+ const note = (el: Element): void => {
1878
+ if (needs.ids.size) {
1879
+ const id = attr(el, 'id');
1880
+ if (id && needs.ids.has(id)) found.add(`#${id}`);
1881
+ }
1882
+ if (needs.classes.size) {
1883
+ const className = attr(el, 'class');
1884
+ if (className) {
1885
+ for (const name of className.split(/\s+/)) {
1886
+ if (needs.classes.has(name)) found.add(`.${name}`);
1887
+ }
1888
+ }
1889
+ }
1890
+ };
1891
+ if (asks) {
1892
+ let at = root.parent;
1893
+ while (at && at.type === 'tag') {
1894
+ note(at as Element);
1895
+ at = at.parent;
1896
+ }
1897
+ }
1898
+ const live = (indexed: IndexedRule): boolean => {
1899
+ const keys = indexed.needs;
1900
+ if (keys) for (const key of keys) if (!found.has(key)) return false;
1901
+ return true;
1902
+ };
1903
+ // The root's box has its paint from its computed style, so most of the
1904
+ // rules that name a drawing by its class — an icon set's — have
1905
+ // nothing else to say of it.
1906
+ const liveForRoot = (indexed: IndexedRule): boolean =>
1907
+ (indexed.root ??= declaresRootShape(indexed.rule.declarations)) &&
1908
+ live(indexed);
1909
+
1910
+ let of: Map<Element, ShapeStyle> | null = null;
1911
+ let key = '';
1912
+ let ctx: ShapeContext | null = null;
1913
+ // in document order, the root first: an element's place in it is what
1914
+ // the key names it by, and its ancestors in the drawing are noted
1915
+ // before it is asked of
1916
+ const stack: Element[] = [root];
1917
+ let place = -1;
1918
+ while (stack.length) {
1919
+ const el = stack.pop()!;
1920
+ place += 1;
1921
+ // what is in a `<foreignObject>` is not the drawing's to draw
1922
+ if (el.name !== 'foreignobject' && el.name !== 'foreignObject') {
1923
+ const children = el.children;
1924
+ for (let i = children.length - 1; i >= 0; i -= 1) {
1925
+ const child = children[i];
1926
+ if (child.type === 'tag') stack.push(child as Element);
1927
+ }
1928
+ }
1929
+ if (asks) note(el);
1930
+ const tag = tagOf(el);
1931
+ const asked = found.size
1932
+ ? index.universal.length > 0 || index.byTag.has(tag)
1933
+ : needs.universal || needs.tags.has(tag);
1934
+ if (!asked && !index.names(el)) continue;
1935
+ const candidates: Candidate[] = [];
1936
+ this._matchInto(
1937
+ index,
1938
+ el,
1939
+ candidates,
1940
+ undefined,
1941
+ el === root ? liveForRoot : live,
1942
+ );
1943
+ if (!candidates.length) continue;
1944
+ const inline = attr(el, 'style');
1945
+ if (inline) pushInlineShapes(candidates, inline);
1946
+ candidates.sort(byCascade);
1947
+ ctx ??= {
1948
+ color: style.color,
1949
+ scheme: style.colorScheme,
1950
+ units: {
1951
+ em: style.fontSize,
1952
+ rem: (this._root ?? this.initial).fontSize,
1953
+ vw: this.viewportWidth,
1954
+ vh: this.viewportHeight,
1955
+ scale: this.scale,
1956
+ initial: this.initial,
1957
+ },
1958
+ };
1959
+ let own: Record<string, string> | null = null;
1960
+ for (const c of candidates) {
1961
+ for (const d of pick(c)) {
1962
+ if (!isShapeProp(d.prop)) continue;
1963
+ if (el === root && ROOT_BOX_PROPS.has(d.prop)) continue;
1964
+ // a `var()` with nothing to stand for it is invalid at
1965
+ // computed-value time, and the property as though `unset`
1966
+ const value = shapeValue(
1967
+ d.prop,
1968
+ d.vars ? (substituteIn(d.value, style.custom) ?? 'unset') : d.value,
1969
+ ctx,
1970
+ );
1971
+ if (value !== null) (own ??= {})[d.prop] = value;
1972
+ }
1973
+ }
1974
+ if (!own) continue;
1975
+ (of ??= new Map()).set(el, own);
1976
+ key += `${place}{`;
1977
+ for (const prop in own) key += `${prop}:${own[prop]};`;
1978
+ key += '}';
1979
+ }
1980
+ return of ? { of, key } : null;
1981
+ }
1982
+
1983
+ /** `ShapeNeeds`, and each such rule's own (`IndexedRule.needs`). */
1984
+ private _needsOfShapes(): ShapeNeeds {
1985
+ const index = this._shapes;
1986
+ const needs: ShapeNeeds = {
1987
+ classes: new Set(),
1988
+ ids: new Set(),
1989
+ universal: false,
1990
+ tags: new Set(),
1991
+ };
1992
+ const read = (bucket: IndexedRule[]): boolean => {
1993
+ let free = false;
1994
+ for (const indexed of bucket) {
1995
+ const keys = ancestorKeys(indexed.rule.selector);
1996
+ indexed.needs = keys;
1997
+ if (!keys.length) free = true;
1998
+ for (const key of keys) {
1999
+ (key[0] === '#' ? needs.ids : needs.classes).add(key.slice(1));
2000
+ }
2001
+ }
2002
+ return free;
2003
+ };
2004
+ needs.universal = read(index.universal);
2005
+ for (const [tag, bucket] of index.byTag) {
2006
+ if (read(bucket)) needs.tags.add(tag);
2007
+ }
2008
+ return needs;
2009
+ }
2010
+
1746
2011
  /**
1747
2012
  * Whether the pointer's move from `was` to where it is changed which
1748
2013
  * rules match `el`, or its `::before`, `::after` or another of its
@@ -1785,6 +2050,8 @@ export class Cascade {
1785
2050
  el: Element,
1786
2051
  out: Candidate[] | null,
1787
2052
  matched?: number[],
2053
+ /** The rules to try, where some are known not to match without it. */
2054
+ live?: (indexed: IndexedRule) => boolean,
1788
2055
  ): void {
1789
2056
  // A media query's width is CSS pixels; the viewport is kept in device.
1790
2057
  const width = this.viewportWidth / this.scale;
@@ -1793,6 +2060,7 @@ export class Cascade {
1793
2060
  const consider = (bucket: IndexedRule[] | undefined): void => {
1794
2061
  if (!bucket) return;
1795
2062
  for (const indexed of bucket) {
2063
+ if (live !== undefined && !live(indexed)) continue;
1796
2064
  const rule = indexed.rule;
1797
2065
  if (
1798
2066
  !mediaMatches(
@@ -1808,7 +2076,11 @@ export class Cascade {
1808
2076
  if (!indexed.compiled) {
1809
2077
  indexed.compiled = true;
1810
2078
  try {
1811
- indexed.match = compileSelector(rule.selector, this._adapter);
2079
+ indexed.match = compileSelector(
2080
+ rule.selector,
2081
+ this._adapter,
2082
+ this._pseudos,
2083
+ );
1812
2084
  } catch {
1813
2085
  // A selector this matcher does not know (`::-moz-…`, a CSS4 form
1814
2086
  // it has not learnt) drops out of the cascade rather than out of
@@ -2088,6 +2360,51 @@ function setsContent(c: Candidate): boolean {
2088
2360
  /** Whether a rule's declarations set a custom property, by the array. */
2089
2361
  const CUSTOM_IN = new WeakMap<Declaration[], boolean>();
2090
2362
 
2363
+ /** Whether declarations set a property a shape in a drawing has. */
2364
+ function declaresShape(declarations: readonly Declaration[]): boolean {
2365
+ for (const d of declarations) if (isShapeProp(d.prop)) return true;
2366
+ return false;
2367
+ }
2368
+
2369
+ /** Whether declarations set one a drawing's root does not have from its
2370
+ * box's style (`ROOT_BOX_PROPS`). */
2371
+ function declaresRootShape(declarations: readonly Declaration[]): boolean {
2372
+ for (const d of declarations) {
2373
+ if (isShapeProp(d.prop) && !ROOT_BOX_PROPS.has(d.prop)) return true;
2374
+ }
2375
+ return false;
2376
+ }
2377
+
2378
+ /** A `style` attribute's declarations of a shape's properties, as
2379
+ * candidates: over every rule, and its `!important` ones over theirs. */
2380
+ function pushInlineShapes(out: Candidate[], inline: string): void {
2381
+ const declarations = parseDeclarations(inline).filter((d) =>
2382
+ isShapeProp(d.prop),
2383
+ );
2384
+ const normal = declarations.filter((d) => !d.important);
2385
+ const important = declarations.filter((d) => d.important);
2386
+ if (normal.length) {
2387
+ out.push({
2388
+ origin: Origin.Inline,
2389
+ layer: null,
2390
+ specificity: 0,
2391
+ order: 0,
2392
+ declarations: normal,
2393
+ only: -1,
2394
+ });
2395
+ }
2396
+ if (important.length) {
2397
+ out.push({
2398
+ origin: Origin.InlineImportant,
2399
+ layer: null,
2400
+ specificity: 0,
2401
+ order: 0,
2402
+ declarations: important,
2403
+ only: -1,
2404
+ });
2405
+ }
2406
+ }
2407
+
2091
2408
  function pick(c: Candidate): Declaration[] {
2092
2409
  return c.only < 0 ? c.declarations : [c.declarations[c.only]];
2093
2410
  }
@@ -2473,6 +2790,24 @@ function lengthAttr(value: string): string {
2473
2790
  return Number.isFinite(n) ? `${n}px` : v;
2474
2791
  }
2475
2792
 
2793
+ /** A number and a unit, as an SVG's `width` and `height` are written. */
2794
+ const SVG_LENGTH =
2795
+ /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]*)\s*$/i;
2796
+
2797
+ /**
2798
+ * The `width`/`height` of an inline `<svg>` as CSS: they are presentation
2799
+ * attributes for the properties of the same names (SVG 2, 5.1.1), so
2800
+ * `height="50%"` is a percentage of the containing block like a style's.
2801
+ * A bare number is pixels; null for what CSS would not parse.
2802
+ */
2803
+ function svgSizeHint(value: string): string | null {
2804
+ const v = value.trim();
2805
+ if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i.test(v)) return v;
2806
+ const m = SVG_LENGTH.exec(v);
2807
+ if (!m) return null;
2808
+ return m[2] ? v : `${parseFloat(m[1])}px`;
2809
+ }
2810
+
2476
2811
  function closestTable(el: Element): Element | null {
2477
2812
  let node = el.parent;
2478
2813
  while (node) {