@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
@@ -56,6 +56,10 @@ export interface Stylesheet {
56
56
  * resource seam and splices the result in ahead of this sheet. Absolute
57
57
  * when the sheet was parsed with a base. */
58
58
  imports: string[];
59
+ /** Each import's media query list, where it has one: the condition the
60
+ * sheet it brings in is under, as though an `@media` block were around
61
+ * all of it (CSS Cascade 4, 2). Null for an import with none. */
62
+ importConditions: (MediaCondition[] | null)[];
59
63
  /** Every width a `@media` rule in this sheet switches on. The renderer
60
64
  * keeps these so a resize can tell "the layout changed" from "the
61
65
  * *cascade* changed", and restyle only when it crossed one. */
@@ -84,8 +88,9 @@ export interface Stylesheet {
84
88
  export interface FontFaceRule {
85
89
  /** The family as the document names it, unquoted. */
86
90
  family: string;
87
- /** The `url()`s of `src`, in order, each with its `format()` hint
88
- * lowercased; `local()` entries are left out. */
91
+ /** The entries of `src`, in order, which is the order they are tried in:
92
+ * a `url()` with its `format()` hint lowercased, or the name a `local()`
93
+ * asks the system for. */
89
94
  sources: FontFaceSource[];
90
95
  /** The weights the face covers: `[400, 400]` for a static regular, a
91
96
  * range for a variable face. */
@@ -97,10 +102,10 @@ export interface FontFaceRule {
97
102
  media: MediaCondition[][] | null;
98
103
  }
99
104
 
100
- export interface FontFaceSource {
101
- url: string;
102
- format: string | null;
103
- }
105
+ export type FontFaceSource =
106
+ | { url: string; format: string | null }
107
+ /** `local(Arial)`: a face the system has, by name and unquoted. */
108
+ | { local: string };
104
109
 
105
110
  /** The tests this evaluates live: a width, a height, a colour scheme.
106
111
  * Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
@@ -176,16 +181,38 @@ export function parseStylesheet(
176
181
  * the document's for a `<style>` — or null to leave them as written.
177
182
  * Every `url()`, `@import` and `@font-face` source comes out absolute. */
178
183
  base: string | null = null,
184
+ /** The media queries the whole sheet is under, outermost first: the ones
185
+ * of the `@import` that brought it in, and of the imports that brought
186
+ * that sheet in. Every rule and face in it is under them, as one inside
187
+ * an `@media` block is under the block's. */
188
+ under: MediaCondition[][] | null = null,
179
189
  ): Stylesheet {
180
190
  text = withoutComments(text);
181
191
  const sheet: Stylesheet = {
182
192
  rules: [],
183
193
  imports: [],
194
+ importConditions: [],
184
195
  breakpoints: [],
185
196
  fontFaces: [],
186
197
  };
187
198
  let order = startOrder;
188
199
  const breakpoints = new Set<number>();
200
+ /** What a media query list tests that a resize has to restyle for: the
201
+ * widths it changes its mind at, and whether it reads the height. */
202
+ const note = (conditions: MediaCondition[]): void => {
203
+ for (const c of conditions) {
204
+ if (c.min !== undefined) breakpoints.add(c.min);
205
+ if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
206
+ if (c.minHeight !== undefined || c.maxHeight !== undefined) {
207
+ sheet.readsHeight = true;
208
+ }
209
+ if (c.minAspect !== undefined || c.maxAspect !== undefined) {
210
+ sheet.readsHeight = true;
211
+ sheet.readsWidth = true;
212
+ }
213
+ }
214
+ };
215
+ for (const conditions of under ?? []) note(conditions);
189
216
  // `@import` counts only ahead of every other rule, `@charset` aside
190
217
  let importsAllowed = true;
191
218
 
@@ -233,10 +260,12 @@ export function parseStylesheet(
233
260
  const name = at.name.toLowerCase();
234
261
  if (name === 'charset') continue;
235
262
  if (name === 'import') {
236
- if (importsAllowed && media === null) {
263
+ if (importsAllowed && media === under) {
237
264
  const url = importUrl(at.prelude);
238
- if (url && importApplies(at.prelude)) {
265
+ const conditions = importConditions(at.prelude);
266
+ if (url && conditions !== false) {
239
267
  sheet.imports.push(resolveUrl(url, base));
268
+ sheet.importConditions.push(conditions);
240
269
  }
241
270
  }
242
271
  continue;
@@ -248,17 +277,7 @@ export function parseStylesheet(
248
277
  }
249
278
  if (name === 'media' && at.block !== null) {
250
279
  const conditions = parseMediaQuery(at.prelude);
251
- for (const c of conditions) {
252
- if (c.min !== undefined) breakpoints.add(c.min);
253
- if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
254
- if (c.minHeight !== undefined || c.maxHeight !== undefined) {
255
- sheet.readsHeight = true;
256
- }
257
- if (c.minAspect !== undefined || c.maxAspect !== undefined) {
258
- sheet.readsHeight = true;
259
- sheet.readsWidth = true;
260
- }
261
- }
280
+ note(conditions);
262
281
  // A nested `@media` intersects with the one above it; pushing a
263
282
  // level rather than merging keeps "all of these blocks hold" exact
264
283
  // when two of them overlap.
@@ -414,7 +433,7 @@ export function parseStylesheet(
414
433
  }
415
434
  };
416
435
 
417
- walk(text, null);
436
+ walk(text, under);
418
437
  sheet.breakpoints = [...breakpoints].sort((a, b) => a - b);
419
438
  return sheet;
420
439
  }
@@ -1591,18 +1610,28 @@ function importUrl(prelude: string): string | null {
1591
1610
  return m ? m[1] : null;
1592
1611
  }
1593
1612
 
1594
- /** Whether an `@import`'s media list, the rest of its prelude after the
1595
- * URL, lets it apply here: a list that only names other media, `print` or
1596
- * `braille`, leaves the sheet out (CSS 2.1 6.3). A list that depends on the
1597
- * width is taken to apply; an import is fetched once, not per width. */
1598
- function importApplies(prelude: string): boolean {
1613
+ /**
1614
+ * An `@import`'s media query list, the rest of its prelude after the URL:
1615
+ * null where it has none, and false where no query in it can ever hold
1616
+ * here — a list that only names other media, `print` or `braille` — which
1617
+ * leaves the sheet out, unasked for (CSS 2.1 6.3). Any other list is the
1618
+ * condition the sheet it imports is under (CSS Cascade 4, 2): the sheet is
1619
+ * fetched once, and its rules hold at the widths the list does. Taken to
1620
+ * hold at every width, as it was, a design that imports a sheet a
1621
+ * breakpoint — the Zen Garden's 219, `@import url("219-1367.css") all and
1622
+ * (min-width: 1367px)` — had all of them at once.
1623
+ */
1624
+ function importConditions(prelude: string): MediaCondition[] | null | false {
1599
1625
  const m =
1600
1626
  /^\s*(?:url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)|"[^"]*"|'[^']*')(.*)$/s.exec(
1601
1627
  prelude,
1602
1628
  );
1603
1629
  const list = m?.[1].trim();
1604
- if (!list) return true;
1605
- return parseMediaQuery(list).some((c) => c.staticPass !== false);
1630
+ if (!list) return null;
1631
+ const conditions = parseMediaQuery(list);
1632
+ if (!conditions.some((c) => c.staticPass !== false)) return false;
1633
+ // one that always holds is no condition
1634
+ return conditions.some((c) => c.staticPass === true) ? null : conditions;
1606
1635
  }
1607
1636
 
1608
1637
  /**
@@ -1684,10 +1713,13 @@ const GENERIC_FAMILIES = new Set([
1684
1713
  ]);
1685
1714
 
1686
1715
  /**
1687
- * A `@font-face` block, or null when it names no family, or no source this
1688
- * can ask for: `local()` names a face the system has, which this does not
1689
- * look up, and a family that is only `local()`s is left to the system as it
1690
- * would be anyway.
1716
+ * A `@font-face` block, or null when it names no family or no source — a
1717
+ * rule CSS Fonts 4 (4.3) drops whole. A rule whose sources are all
1718
+ * `local()`s is a rule like any other: its family is the document's, and
1719
+ * left out here, `"GeistSans Fallback"` — next/font's `src: local("Arial")`
1720
+ * with the metrics adjusted — reached the text engine as a name to look up,
1721
+ * where fontconfig's nearest guess for a family nobody has set a site's
1722
+ * headings in Gill Sans Ultra Bold.
1691
1723
  */
1692
1724
  function parseFontFace(
1693
1725
  block: string,
@@ -1720,6 +1752,14 @@ function parseFontFace(
1720
1752
  const sources: FontFaceSource[] = [];
1721
1753
  for (const part of splitTopLevel(src, ',')) {
1722
1754
  const item = part.trim();
1755
+ // `local(Gentium Bold)`, `local("Gentium Bold")`: a name as a family's
1756
+ // is written, and like one never a keyword (`local(inherit)`)
1757
+ const local = /^local\(([\s\S]*)\)$/i.exec(item);
1758
+ if (local) {
1759
+ const name = faceFamily(local[1]);
1760
+ if (name) sources.push({ local: name });
1761
+ continue;
1762
+ }
1723
1763
  if (!/^url\(/i.test(item)) continue;
1724
1764
  const end = urlEnd(item, 3);
1725
1765
  const url = parseUrl(item.slice(0, end));
@@ -0,0 +1,239 @@
1
+ // What a style sheet says of the shapes in a drawing.
2
+ //
3
+ // An inline `<svg>` is a replaced box: the box builder stops at it, and no
4
+ // computed style is made for what is inside. `fill`, `stroke` and their kin
5
+ // are properties all the same (SVG 2, 6.2 and 13.2), which a rule of the
6
+ // document's sets on a `<path>` as it would on a paragraph — a logo's
7
+ // `.dark .logo path { fill: #fff }` — and a drawing exported with a style
8
+ // sheet of its own, `.st0 { fill: #fff }` on `<path class="st0">`, has all
9
+ // its colours in one. So the cascade is run for them narrowly
10
+ // (`Cascade.shapeStyles`): over the rules that declare one of the
11
+ // properties here, and down to values `SvgView` reads from a `style`
12
+ // attribute, which is where the drawing's copy of an element carries them
13
+ // (`svg.ts`). A `ComputedStyle` per `<path>` would be a few hundred
14
+ // properties for the dozen a shape has, on a page of hundreds of icons.
15
+ import type { Element } from 'domhandler';
16
+ import { LIGHT_DARK, lightDark } from './color.js';
17
+ import { inkColor, parseAlpha, parseColor, parseLength } from './values.js';
18
+ import type { UnitContext } from './values.js';
19
+
20
+ /** What the rules give one element of a drawing: each property they set,
21
+ * as `SvgView` reads it — `inherit` for one they put back to what the
22
+ * element inherits, over its own attribute. */
23
+ export type ShapeStyle = Readonly<Record<string, string>>;
24
+
25
+ /** What the rules give the elements of one drawing, and all of it as a
26
+ * string: what a drawing's tree was last made with (`SvgDrawing`). */
27
+ export interface ShapeStyles {
28
+ readonly of: ReadonlyMap<Element, ShapeStyle>;
29
+ readonly key: string;
30
+ }
31
+
32
+ /** Each property read, and whether it is inherited. */
33
+ const PROPS: Record<string, boolean> = {
34
+ fill: true,
35
+ stroke: true,
36
+ 'stroke-width': true,
37
+ 'stroke-linecap': true,
38
+ 'stroke-linejoin': true,
39
+ 'stroke-miterlimit': true,
40
+ 'fill-rule': true,
41
+ 'fill-opacity': true,
42
+ 'stroke-opacity': true,
43
+ color: true,
44
+ visibility: true,
45
+ opacity: false,
46
+ display: false,
47
+ 'stop-color': false,
48
+ 'stop-opacity': false,
49
+ };
50
+
51
+ /** Whether a rule with this declaration is one to keep for drawings. */
52
+ export function isShapeProp(prop: string): boolean {
53
+ return PROPS[prop] !== undefined;
54
+ }
55
+
56
+ /** The properties the drawing's box has from its computed style already:
57
+ * its paint, which `paintSvg` hands the drawing, and what makes it a box,
58
+ * shown and see-through as one. */
59
+ export const ROOT_BOX_PROPS: ReadonlySet<string> = new Set([
60
+ 'fill',
61
+ 'stroke',
62
+ 'color',
63
+ 'display',
64
+ 'visibility',
65
+ 'opacity',
66
+ ]);
67
+
68
+ /** Each at its initial value (SVG 2, 13; CSS Color 4; CSS Display 3). */
69
+ const INITIAL: Record<string, string> = {
70
+ fill: '#000000',
71
+ stroke: 'none',
72
+ 'stroke-width': '1',
73
+ 'stroke-linecap': 'butt',
74
+ 'stroke-linejoin': 'miter',
75
+ 'stroke-miterlimit': '4',
76
+ 'fill-rule': 'nonzero',
77
+ 'fill-opacity': '1',
78
+ 'stroke-opacity': '1',
79
+ visibility: 'visible',
80
+ opacity: '1',
81
+ display: 'inline',
82
+ 'stop-color': '#000000',
83
+ 'stop-opacity': '1',
84
+ };
85
+
86
+ /** The elements a rule's subject can be, where it names one by its type,
87
+ * for the rule to reach into a drawing: the ones `SvgView` draws, or
88
+ * reads a paint from. `a` is both languages'. */
89
+ export const SHAPE_TAGS: ReadonlySet<string> = new Set([
90
+ 'svg',
91
+ 'g',
92
+ 'a',
93
+ 'path',
94
+ 'rect',
95
+ 'circle',
96
+ 'ellipse',
97
+ 'line',
98
+ 'polyline',
99
+ 'polygon',
100
+ 'text',
101
+ 'tspan',
102
+ 'use',
103
+ 'symbol',
104
+ 'switch',
105
+ 'image',
106
+ 'stop',
107
+ ]);
108
+
109
+ /** What a shape's values are read against: the drawing's own `color` and
110
+ * colour scheme, and its font size for a length in `em`. */
111
+ export interface ShapeContext {
112
+ color: string;
113
+ scheme: 'light' | 'dark';
114
+ units: UnitContext;
115
+ }
116
+
117
+ /**
118
+ * A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
119
+ * naming a gradient in the drawing, with what follows it, its fallback,
120
+ * left off. Null for anything else, `context-fill` and `context-stroke`
121
+ * among it, which only a marker or a `<use>` has a context for.
122
+ */
123
+ export function svgPaint(value: string): string | null {
124
+ const v = value.trim();
125
+ if (v.toLowerCase() === 'none') return 'none';
126
+ const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
127
+ if (url) return `url(${url[2]})`;
128
+ return parseColor(v);
129
+ }
130
+
131
+ const NUMBER = /^[+-]?(?:\d*\.\d+|\d+)(?:e[+-]?\d+)?$/i;
132
+
133
+ /** A word of a `display` that makes a box (CSS Display 3, 2). */
134
+ const DISPLAY =
135
+ /^(?:block|inline|run-in|flow|flow-root|table|flex|grid|ruby|list-item|contents|inline-[a-z]+|table-[a-z-]+|ruby-[a-z-]+|-webkit-[a-z-]+)$/;
136
+
137
+ const LINE_JOINS: Record<string, string> = {
138
+ miter: 'miter',
139
+ round: 'round',
140
+ bevel: 'bevel',
141
+ // the two a canvas has no join for end in a miter, clipped or not
142
+ 'miter-clip': 'miter',
143
+ arcs: 'miter',
144
+ };
145
+
146
+ /**
147
+ * One declaration's value as a drawing reads it, or null for a value the
148
+ * property does not take, which leaves the declaration out of the cascade
149
+ * as any invalid one is left. `inherit` for a CSS-wide keyword that puts an
150
+ * inherited property back to what the element inherits.
151
+ */
152
+ export function shapeValue(
153
+ prop: string,
154
+ raw: string,
155
+ ctx: ShapeContext,
156
+ ): string | null {
157
+ let value = raw.trim();
158
+ const wide = value.toLowerCase();
159
+ if (wide === 'inherit') return PROPS[prop] ? 'inherit' : null;
160
+ if (wide === 'initial') {
161
+ return prop === 'color'
162
+ ? (ctx.units.initial?.color ?? '#000000')
163
+ : INITIAL[prop];
164
+ }
165
+ if (wide === 'unset' || wide === 'revert' || wide === 'revert-layer') {
166
+ return PROPS[prop] ? 'inherit' : INITIAL[prop];
167
+ }
168
+ if (LIGHT_DARK.test(value)) {
169
+ const picked = lightDark(value, ctx.scheme);
170
+ if (picked === null) return null;
171
+ value = picked;
172
+ }
173
+ switch (prop) {
174
+ case 'fill':
175
+ case 'stroke': {
176
+ const paint = svgPaint(value);
177
+ // `currentColor` is the shape's own `color`, which the drawing knows
178
+ return paint === null || paint === 'currentColor'
179
+ ? paint
180
+ : inkColor(paint, ctx.color);
181
+ }
182
+ case 'color':
183
+ case 'stop-color': {
184
+ const color = parseColor(value);
185
+ return color === null ? null : inkColor(color, ctx.color);
186
+ }
187
+ case 'stroke-width': {
188
+ // a number is user units, as a length in pixels is; a percentage is
189
+ // of the viewport's diagonal, which only the drawing has
190
+ const px = NUMBER.test(value)
191
+ ? Number(value)
192
+ : lengthIn(value, ctx.units);
193
+ return px !== null && px >= 0 ? String(px) : null;
194
+ }
195
+ case 'stroke-miterlimit': {
196
+ const n = NUMBER.test(value) ? Number(value) : NaN;
197
+ return n >= 1 ? String(n) : null;
198
+ }
199
+ case 'fill-opacity':
200
+ case 'stroke-opacity':
201
+ case 'stop-opacity':
202
+ case 'opacity': {
203
+ const a = parseAlpha(value);
204
+ return a === null ? null : String(a);
205
+ }
206
+ case 'stroke-linecap': {
207
+ const v = value.toLowerCase();
208
+ return v === 'butt' || v === 'round' || v === 'square' ? v : null;
209
+ }
210
+ case 'stroke-linejoin':
211
+ return LINE_JOINS[value.toLowerCase()] ?? null;
212
+ case 'fill-rule': {
213
+ const v = value.toLowerCase();
214
+ return v === 'nonzero' || v === 'evenodd' ? v : null;
215
+ }
216
+ case 'visibility': {
217
+ const v = value.toLowerCase();
218
+ if (v === 'visible') return v;
219
+ return v === 'hidden' || v === 'collapse' ? 'hidden' : null;
220
+ }
221
+ case 'display': {
222
+ // `none`, or some box: which one is nothing to a shape
223
+ const v = value.toLowerCase();
224
+ if (v === 'none') return v;
225
+ return v.split(/\s+/).every((word) => DISPLAY.test(word))
226
+ ? 'inline'
227
+ : null;
228
+ }
229
+ default:
230
+ return null;
231
+ }
232
+ }
233
+
234
+ /** A length in user units, which are CSS pixels; null for none, and for a
235
+ * percentage. */
236
+ function lengthIn(value: string, units: UnitContext): number | null {
237
+ const length = parseLength(value, units);
238
+ return typeof length === 'number' ? length / units.scale : null;
239
+ }