@react-x11/components 0.11.0 → 0.12.0

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 (100) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +26 -7
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +28 -3
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts.map +1 -1
  10. package/dist/html/css/cascade.js +19 -6
  11. package/dist/html/css/cascade.js.map +1 -1
  12. package/dist/html/css/color.d.ts +19 -0
  13. package/dist/html/css/color.d.ts.map +1 -1
  14. package/dist/html/css/color.js +86 -0
  15. package/dist/html/css/color.js.map +1 -1
  16. package/dist/html/css/parse.d.ts +11 -0
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +114 -4
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/style.d.ts +38 -1
  21. package/dist/html/css/style.d.ts.map +1 -1
  22. package/dist/html/css/style.js +326 -8
  23. package/dist/html/css/style.js.map +1 -1
  24. package/dist/html/css/ua.js +14 -0
  25. package/dist/html/css/ua.js.map +1 -1
  26. package/dist/html/css/values.d.ts +10 -1
  27. package/dist/html/css/values.d.ts.map +1 -1
  28. package/dist/html/css/values.js +27 -2
  29. package/dist/html/css/values.js.map +1 -1
  30. package/dist/html/css/vars.d.ts +5 -0
  31. package/dist/html/css/vars.d.ts.map +1 -1
  32. package/dist/html/css/vars.js +2 -2
  33. package/dist/html/css/vars.js.map +1 -1
  34. package/dist/html/dom.d.ts.map +1 -1
  35. package/dist/html/dom.js +35 -2
  36. package/dist/html/dom.js.map +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/index.js +38 -1
  39. package/dist/html/index.js.map +1 -1
  40. package/dist/html/layout/block.d.ts +11 -2
  41. package/dist/html/layout/block.d.ts.map +1 -1
  42. package/dist/html/layout/block.js +181 -18
  43. package/dist/html/layout/block.js.map +1 -1
  44. package/dist/html/layout/boxes.d.ts +34 -6
  45. package/dist/html/layout/boxes.d.ts.map +1 -1
  46. package/dist/html/layout/boxes.js +35 -14
  47. package/dist/html/layout/boxes.js.map +1 -1
  48. package/dist/html/layout/cache.d.ts +2 -2
  49. package/dist/html/layout/cache.d.ts.map +1 -1
  50. package/dist/html/layout/cache.js +8 -3
  51. package/dist/html/layout/cache.js.map +1 -1
  52. package/dist/html/layout/floats.d.ts +4 -0
  53. package/dist/html/layout/floats.d.ts.map +1 -1
  54. package/dist/html/layout/floats.js +26 -0
  55. package/dist/html/layout/floats.js.map +1 -1
  56. package/dist/html/layout/inline.d.ts +26 -2
  57. package/dist/html/layout/inline.d.ts.map +1 -1
  58. package/dist/html/layout/inline.js +1222 -115
  59. package/dist/html/layout/inline.js.map +1 -1
  60. package/dist/html/node.d.ts +33 -0
  61. package/dist/html/node.d.ts.map +1 -1
  62. package/dist/html/node.js +512 -51
  63. package/dist/html/node.js.map +1 -1
  64. package/dist/html/paint.d.ts +53 -1
  65. package/dist/html/paint.d.ts.map +1 -1
  66. package/dist/html/paint.js +546 -180
  67. package/dist/html/paint.js.map +1 -1
  68. package/dist/html/surfaces.d.ts +7 -2
  69. package/dist/html/surfaces.d.ts.map +1 -1
  70. package/dist/html/surfaces.js +16 -0
  71. package/dist/html/surfaces.js.map +1 -1
  72. package/dist/internal/window.d.ts +22 -0
  73. package/dist/internal/window.d.ts.map +1 -1
  74. package/dist/internal/window.js +35 -8
  75. package/dist/internal/window.js.map +1 -1
  76. package/dist/richtext/node.d.ts +7 -0
  77. package/dist/richtext/node.d.ts.map +1 -1
  78. package/dist/richtext/node.js.map +1 -1
  79. package/package.json +5 -3
  80. package/src/flow/node.ts +80 -64
  81. package/src/html/controls.ts +50 -9
  82. package/src/html/css/cascade.ts +19 -6
  83. package/src/html/css/color.ts +87 -0
  84. package/src/html/css/parse.ts +107 -4
  85. package/src/html/css/style.ts +343 -8
  86. package/src/html/css/ua.ts +14 -0
  87. package/src/html/css/values.ts +31 -3
  88. package/src/html/css/vars.ts +2 -2
  89. package/src/html/dom.ts +38 -2
  90. package/src/html/index.ts +39 -1
  91. package/src/html/layout/block.ts +228 -16
  92. package/src/html/layout/boxes.ts +55 -21
  93. package/src/html/layout/cache.ts +8 -3
  94. package/src/html/layout/floats.ts +24 -0
  95. package/src/html/layout/inline.ts +1435 -108
  96. package/src/html/node.ts +535 -54
  97. package/src/html/paint.ts +650 -241
  98. package/src/html/surfaces.ts +21 -1
  99. package/src/internal/window.ts +36 -8
  100. package/src/richtext/node.ts +7 -0
@@ -24,7 +24,7 @@ import type { Element } from 'domhandler';
24
24
  import { attr, tagOf } from './dom.js';
25
25
  import type { ComputedStyle } from './css/style.js';
26
26
  import { isTransparent } from './css/values.js';
27
- import type { BoxTree, ReplacedKind } from './layout/boxes.js';
27
+ import type { Box, BoxTree, ReplacedKind } from './layout/boxes.js';
28
28
  import type { FontsLike } from './layout/inline.js';
29
29
 
30
30
  /** The palette numbers a control's box has to reserve room for. */
@@ -48,15 +48,24 @@ export interface ControlRect {
48
48
  width: number;
49
49
  height: number;
50
50
  /**
51
- * Set on a text field whose own box the author styled — gave it a border
52
- * or a background (`styledField`). The document draws that box, and the
53
- * widget goes bare inside its content box, here, with no frame or fill
54
- * of its own and its text in the element's colour and font.
51
+ * Set on a text field or a `<select>` whose own box the author styled —
52
+ * gave it a border or a background (`styledField`). The document draws
53
+ * that box, and the widget goes bare inside its content box, here, with
54
+ * no frame or fill of its own and its text in the element's colour and
55
+ * font.
55
56
  */
56
57
  bare?: BareField;
58
+ /**
59
+ * How opaque the widget is drawn, where it is less than 1: the element's
60
+ * own `opacity` times every ancestor's, since each fades what is in it as
61
+ * a group. At 0 the widget is not seen and still takes a press, as the
62
+ * element does in a browser — the checkbox a CSS-only dropdown lays,
63
+ * invisible, over its label.
64
+ */
65
+ opacity?: number;
57
66
  }
58
67
 
59
- /** Where a styled text field's widget goes, and how its text looks. */
68
+ /** Where a styled field's widget goes, and how its text looks. */
60
69
  export interface BareField {
61
70
  x: number;
62
71
  y: number;
@@ -65,6 +74,12 @@ export interface BareField {
65
74
  color: string;
66
75
  fontFamily: string;
67
76
  fontSize: number;
77
+ /**
78
+ * A `<select>`'s: whether it draws its arrow. It does unless the page set
79
+ * `appearance: none`, which is how a page that draws its own arrow — as a
80
+ * background image, most often — says so.
81
+ */
82
+ chevron?: boolean;
68
83
  }
69
84
 
70
85
  /** The rectangles every control in a laid-out document landed on. */
@@ -73,6 +88,8 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
73
88
  for (const box of tree.controls) {
74
89
  if (!box.el) continue;
75
90
  if (box.width <= 0 || box.height <= 0) continue;
91
+ // a hidden element draws nothing and takes no press (CSS 2.1 11.2)
92
+ if (box.style.visibility !== 'visible') continue;
76
93
  const rect: ControlRect = {
77
94
  element: box.el,
78
95
  kind: box.replaced,
@@ -81,6 +98,11 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
81
98
  width: box.width,
82
99
  height: box.height,
83
100
  };
101
+ let opacity = 1;
102
+ for (let at: Box | null = box; at; at = at.parent) {
103
+ opacity *= at.style.opacity;
104
+ }
105
+ if (opacity < 1) rect.opacity = Math.max(0, opacity);
84
106
  if (styledField(box.replaced, box.style)) {
85
107
  rect.bare = {
86
108
  x: box.contentX,
@@ -90,6 +112,9 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
90
112
  color: box.style.color,
91
113
  fontFamily: box.style.fontFamily,
92
114
  fontSize: box.style.fontSize,
115
+ ...(box.replaced === 'select' && {
116
+ chevron: box.style.appearance !== 'none',
117
+ }),
93
118
  };
94
119
  }
95
120
  out.push(rect);
@@ -98,17 +123,23 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
98
123
  }
99
124
 
100
125
  /**
101
- * Whether a text field's box is the author's to draw: one given a border or
102
- * a background of its own, which the UA sheet gives no control, or set to
126
+ * Whether a field's box is the author's to draw: one given a border or a
127
+ * background of its own, which the UA sheet gives no control, or set to
103
128
  * `appearance: none`, which says so outright. A browser drops a field's
104
129
  * native look for the author's then (CSS UI 4 7.1, `appearance`), and so
105
130
  * does this: the widget's frame and fill would hide the author's, and what
106
131
  * they would draw is the theme's rather than the page's. `appearance: none`
107
132
  * is how a design system writes every field it has — meetup.com's search
108
133
  * pill holds two with no background, only one of them with a border.
134
+ *
135
+ * A `<select>` is one too. A browser keeps its arrow when the page gave it a
136
+ * border or a background (Blink's `menulist-button`), and leaves that out
137
+ * as well at `appearance: none` — `BareField.chevron`.
109
138
  */
110
139
  export function styledField(kind: ReplacedKind, style: ComputedStyle): boolean {
111
- if (kind !== 'input' && kind !== 'textarea') return false;
140
+ if (kind !== 'input' && kind !== 'textarea' && kind !== 'select') {
141
+ return false;
142
+ }
112
143
  return (
113
144
  style.appearance === 'none' ||
114
145
  !isTransparent(style.backgroundColor) ||
@@ -168,6 +199,16 @@ export function measureControl(
168
199
  }
169
200
  case 'select': {
170
201
  const widest = optionWidths(el);
202
+ // a select the page styled is its widest option and its arrow, and
203
+ // the room around them is the page's
204
+ if (styledField(kind, style)) {
205
+ return {
206
+ width: Math.round(
207
+ ch * widest + (style.appearance === 'none' ? 0 : em),
208
+ ),
209
+ height: lineHeight,
210
+ };
211
+ }
171
212
  // The room for the chevron is the widget's, not the document's, but
172
213
  // the document has to reserve it or the last letter of the widest
173
214
  // option sits under it.
@@ -49,6 +49,7 @@ import { parseDeclarations } from './parse.js';
49
49
  import { CounterStyles, counterStyleRule } from './counter-styles.js';
50
50
  import type { CounterStyleRule } from './counter-styles.js';
51
51
  import type { UnitContext } from './values.js';
52
+ import { viewportUnit } from './values.js';
52
53
  import { customProperties, substituteIn } from './vars.js';
53
54
  import type { CustomProps } from './vars.js';
54
55
 
@@ -703,7 +704,7 @@ export class Cascade {
703
704
  for (const d of declarations) {
704
705
  if (!VIEWPORT_UNIT.test(d.value)) continue;
705
706
  for (const m of d.value.matchAll(VIEWPORT_UNITS)) {
706
- const unit = m[1].toLowerCase();
707
+ const unit = viewportUnit(m[1]);
707
708
  if (unit !== 'vh') this.readsViewportWidth = true;
708
709
  if (unit !== 'vw') this.readsViewportHeight = true;
709
710
  }
@@ -1196,7 +1197,8 @@ export class Cascade {
1196
1197
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
1197
1198
  // measured in, however the declarations are ordered. A `width: 10ex`
1198
1199
  // in a sheet under an inline `font-weight: 900` was measured in the
1199
- // family's regular face.
1200
+ // family's regular face. The colour scheme goes ahead of the rest for
1201
+ // the same reason: every `light-dark()` among them is read by it.
1200
1202
  let sized = false;
1201
1203
  let keyword = false;
1202
1204
  for (const c of candidates) {
@@ -1204,7 +1206,8 @@ export class Cascade {
1204
1206
  if (
1205
1207
  d.prop === 'font-family' ||
1206
1208
  d.prop === 'font-weight' ||
1207
- d.prop === 'font-style'
1209
+ d.prop === 'font-style' ||
1210
+ d.prop === 'color-scheme'
1208
1211
  ) {
1209
1212
  this._apply(style, parentStyle, d, ctxParent);
1210
1213
  } else if (d.prop === 'font-size' || d.prop === 'font') {
@@ -1490,10 +1493,20 @@ function asRoot(style: ComputedStyle): ComputedStyle {
1490
1493
 
1491
1494
  /** `:root` is the `<html>` element: the one a browser implies around a
1492
1495
  * fragment, whose style the root box takes (`rootStyle`), and never a
1493
- * fragment's top-level elements, which css-select would take for it. */
1496
+ * fragment's top-level elements, which css-select would take for it.
1497
+ *
1498
+ * `:focus` and its two kin match nothing: no element of the document takes
1499
+ * the focus — a control's widget does, beside it. css-select has none of
1500
+ * the three and throws on them, which dropped every rule that named one,
1501
+ * `:not(:focus)` among them: Wikipedia's skip link hides with
1502
+ * `.mw-jump-link:not(:focus)`, and without it stood in the flow at the top
1503
+ * of every page. */
1494
1504
  const PSEUDOS = {
1495
1505
  root: (el: Element) =>
1496
1506
  el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
1507
+ focus: () => false,
1508
+ 'focus-visible': () => false,
1509
+ 'focus-within': () => false,
1497
1510
  // css-select's, but its ranges and tags lower-cased as ASCII has it, and
1498
1511
  // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
1499
1512
  // Kelvin sign for the K, for `:lang(kl)`
@@ -1527,8 +1540,8 @@ const PSEUDOS = {
1527
1540
 
1528
1541
  /** A length in a unit of the viewport: a number, then the unit, and no
1529
1542
  * more of a name after it. */
1530
- const VIEWPORT_UNIT = /\d(?:vw|vh|vmin|vmax)(?![\w-])/i;
1531
- const VIEWPORT_UNITS = /\d(vw|vh|vmin|vmax)(?![\w-])/gi;
1543
+ const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
1544
+ const VIEWPORT_UNITS = /\d([sld]?v(?:w|h|i|b|min|max))(?![\w-])/gi;
1532
1545
 
1533
1546
  /**
1534
1547
  * A selector with its `[attr~=""]` made one that matches nothing: an empty
@@ -1,4 +1,5 @@
1
1
  import * as ntk from 'react-x11/ntk';
2
+ import { closingParen, topLevelComma } from './vars.js';
2
3
 
3
4
  // CSS Color 4's functional colours: read here, and written back in the one
4
5
  // form every drawing context reads — `#rrggbb`, or `rgba(r, g, b, a)` with
@@ -753,3 +754,89 @@ const REC2020_TO_XYZ: readonly Triple[] = [
753
754
  [26158966 / 99577255, 472592308 / 697040785, 8267143 / 139408157],
754
755
  [0, 19567812 / 697040785, 295819943 / 278816314],
755
756
  ];
757
+
758
+ // --- colour schemes ---------------------------------------------------------
759
+
760
+ /** Whether a value has a `light-dark()` in it. */
761
+ export const LIGHT_DARK = /(?:^|[^\w-])light-dark\(/i;
762
+ const LIGHT_DARK_AT = /(?:^|[^\w-])(light-dark\()/gi;
763
+
764
+ /**
765
+ * A value with each `light-dark()` in it replaced by the branch `scheme`
766
+ * picks (CSS Color 5, 7): the first where the element's used colour
767
+ * scheme is light, the second where it is dark. Replaced in the text, as a
768
+ * `var()` is, so a branch can be any colour — or another `light-dark()` —
769
+ * in any value a colour stands in. Null where one is not two arguments,
770
+ * which makes the declaration invalid.
771
+ */
772
+ export function lightDark(
773
+ value: string,
774
+ scheme: 'light' | 'dark',
775
+ ): string | null {
776
+ let out = value;
777
+ // a branch can hold another, which the next round replaces
778
+ for (let round = 0; round < 32; round += 1) {
779
+ LIGHT_DARK_AT.lastIndex = 0;
780
+ const m = LIGHT_DARK_AT.exec(out);
781
+ if (!m) return out;
782
+ const open = m.index + m[0].length;
783
+ const start = open - m[1].length;
784
+ const close = closingParen(out, open);
785
+ if (close < 0) return null;
786
+ const inner = out.slice(open, close);
787
+ const comma = topLevelComma(inner);
788
+ if (comma < 0) return null;
789
+ const light = inner.slice(0, comma).trim();
790
+ const dark = inner.slice(comma + 1).trim();
791
+ if (!light || !dark || topLevelComma(dark) >= 0) return null;
792
+ out =
793
+ out.slice(0, start) +
794
+ (scheme === 'dark' ? dark : light) +
795
+ out.slice(close + 1);
796
+ }
797
+ return null;
798
+ }
799
+
800
+ /**
801
+ * The scheme an element with this `color-scheme` is drawn in (CSS Color
802
+ * Adjust 1, 2.2), given the one `preferred` — the palette's, which stands
803
+ * for the reader's preference: that one where the element supports it,
804
+ * else the first it names that is one, else the one `normal` gives, which
805
+ * is the palette's own too. Null for a value that is not a `color-scheme`.
806
+ */
807
+ export function usedColorScheme(
808
+ value: string,
809
+ preferred: 'light' | 'dark' = 'light',
810
+ ): 'light' | 'dark' | null {
811
+ const words = value.trim().toLowerCase().split(/\s+/);
812
+ if (words.length === 1 && words[0] === 'normal') return preferred;
813
+ // `[ light | dark | <custom-ident> ]+ && only?`: `only` once, first or
814
+ // last, and never on its own
815
+ const only = words.indexOf('only');
816
+ if (only >= 0) {
817
+ if (only !== 0 && only !== words.length - 1) return null;
818
+ words.splice(only, 1);
819
+ if (!words.length) return null;
820
+ }
821
+ let first: 'light' | 'dark' | null = null;
822
+ for (const word of words) {
823
+ if (!SCHEME_IDENT.test(word) || NOT_SCHEMES.has(word)) return null;
824
+ if (word === preferred) return preferred;
825
+ if (!first && (word === 'light' || word === 'dark')) first = word;
826
+ }
827
+ return first ?? preferred;
828
+ }
829
+
830
+ const SCHEME_IDENT = /^-?(?:[a-z_]|--)[\w-]*$/;
831
+ /** What a scheme's name cannot be: the keywords the grammar has, and the
832
+ * CSS-wide ones, which reach here only in a list. */
833
+ const NOT_SCHEMES = new Set([
834
+ 'normal',
835
+ 'only',
836
+ 'inherit',
837
+ 'initial',
838
+ 'unset',
839
+ 'revert',
840
+ 'revert-layer',
841
+ 'default',
842
+ ]);
@@ -242,8 +242,11 @@ export function parseStylesheet(
242
242
  } else if (name === 'supports' && at.block !== null) {
243
243
  // Everything in a `@supports` block is markup this renderer either
244
244
  // understands or ignores per-declaration, so entering it is closer
245
- // to right than skipping it.
246
- walk(at.block, media, layer, path);
245
+ // to right than skipping it — but for a condition it can answer
246
+ // false (`supportsCondition`)
247
+ if (supportsCondition(at.prelude) !== false) {
248
+ walk(at.block, media, layer, path);
249
+ }
247
250
  } else if (name === 'layer') {
248
251
  // `@layer a, b;` names layers, and so fixes their order, and
249
252
  // `@layer a { … }` puts rules in one; one with no name is a layer
@@ -342,7 +345,9 @@ export function parseStylesheet(
342
345
  depth + 1,
343
346
  );
344
347
  } else if (name === 'supports') {
345
- styleRule(selectors, item.block, media, layer, path, depth + 1);
348
+ if (supportsCondition(item.prelude) !== false) {
349
+ styleRule(selectors, item.block, media, layer, path, depth + 1);
350
+ }
346
351
  } else if (name === 'layer' && LAYER_NAME.test(item.prelude.trim())) {
347
352
  const [ranks, full] = enter(item.prelude.trim(), layer, path);
348
353
  styleRule(selectors, item.block, media, ranks, full, depth + 1);
@@ -1775,7 +1780,7 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1775
1780
  sawWidth = true;
1776
1781
  continue;
1777
1782
  }
1778
- const feature = /^\(\s*([a-z-]+)\s*:\s*([^)]+?)\s*\)$/i.exec(term);
1783
+ const feature = mediaFeature(term);
1779
1784
  if (feature) {
1780
1785
  const key = feature[1].toLowerCase();
1781
1786
  const len = parseLength(feature[2], ZERO_UNITS);
@@ -1845,6 +1850,104 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1845
1850
  return out.length ? out : [{ staticPass: true }];
1846
1851
  }
1847
1852
 
1853
+ /**
1854
+ * The properties whose support a `@supports` condition is answered for:
1855
+ * the mask's, which pages test before they draw an icon as a masked colour
1856
+ * and keep a background image under `not` for an engine without them.
1857
+ * Drawn here, and the fallback drawn too, an icon was its image in black
1858
+ * under the mask, over the colour it was meant to be.
1859
+ */
1860
+ const ANSWERED = new Set(
1861
+ [
1862
+ 'mask',
1863
+ 'mask-image',
1864
+ 'mask-repeat',
1865
+ 'mask-position',
1866
+ 'mask-size',
1867
+ 'mask-origin',
1868
+ 'mask-clip',
1869
+ ].flatMap((name) => [name, `-webkit-${name}`]),
1870
+ );
1871
+
1872
+ /**
1873
+ * Whether a `@supports` condition holds (CSS Conditional 3, 6): true or
1874
+ * false where it turns on declarations of the properties this answers for
1875
+ * (`ANSWERED`), combined by `not`, `and` and `or`; null where it turns on
1876
+ * anything else, or is none, and the block is entered as every one was.
1877
+ * Answering more is not safe without a list of what this draws: Tailwind 4
1878
+ * keeps its variables' starting values under a test for engines without
1879
+ * `@property`, which this is, and false there would drop every shadow and
1880
+ * gradient it writes.
1881
+ */
1882
+ export function supportsCondition(prelude: string): boolean | null {
1883
+ const text = prelude.trim();
1884
+ if (/^not\b/i.test(text)) {
1885
+ const inner = supportsCondition(text.slice(3));
1886
+ return inner === null ? null : !inner;
1887
+ }
1888
+ // the terms at the top level, and the one operator between them
1889
+ const terms: string[] = [];
1890
+ let op = '';
1891
+ let depth = 0;
1892
+ let start = 0;
1893
+ for (let i = 0; i < text.length; i += 1) {
1894
+ const c = text[i];
1895
+ if (c === '(') depth += 1;
1896
+ else if (c === ')') depth -= 1;
1897
+ else if (depth === 0 && /\s/.test(c)) {
1898
+ const word = /^\s+(and|or)\s+/i.exec(text.slice(i));
1899
+ if (!word) continue;
1900
+ const next = word[1].toLowerCase();
1901
+ // `and` and `or` mixed without parentheses is no condition
1902
+ if (op && op !== next) return null;
1903
+ op = next;
1904
+ terms.push(text.slice(start, i));
1905
+ i += word[0].length - 1;
1906
+ start = i + 1;
1907
+ }
1908
+ if (depth < 0) return null;
1909
+ }
1910
+ if (terms.length) {
1911
+ terms.push(text.slice(start));
1912
+ const values = terms.map(supportsCondition);
1913
+ if (op === 'and') {
1914
+ if (values.includes(false)) return false;
1915
+ return values.includes(null) ? null : true;
1916
+ }
1917
+ if (values.includes(true)) return true;
1918
+ return values.includes(null) ? null : false;
1919
+ }
1920
+ // one term: a condition in parentheses, or a declaration in them
1921
+ if (!(text.startsWith('(') && text.endsWith(')'))) return null;
1922
+ const inner = text.slice(1, -1).trim();
1923
+ if (inner.startsWith('(') || /^not\b/i.test(inner)) {
1924
+ return supportsCondition(inner);
1925
+ }
1926
+ const declaration = /^([a-z-]+)\s*:\s*(\S[\s\S]*)$/i.exec(inner);
1927
+ if (!declaration || !ANSWERED.has(declaration[1].toLowerCase())) return null;
1928
+ return true;
1929
+ }
1930
+
1931
+ /**
1932
+ * A `(name: value)` media feature: the whole term, its name, and its value
1933
+ * read to the parenthesis that closes the feature, or null where the term is
1934
+ * no such thing. A value may hold parentheses of its own: MediaWiki's
1935
+ * breakpoints are `(max-width: calc(640px - 1px))`, and a value read to the
1936
+ * first `)` found no feature there — the term was passed over as though it
1937
+ * said nothing, and every narrow-screen rule held at every width.
1938
+ */
1939
+ function mediaFeature(term: string): [string, string, string] | null {
1940
+ const m = /^\(\s*([a-z-]+)\s*:([\s\S]*)\)$/i.exec(term);
1941
+ if (!m) return null;
1942
+ let depth = 0;
1943
+ for (const c of m[2]) {
1944
+ if (c === '(') depth += 1;
1945
+ else if (c === ')' && --depth < 0) return null;
1946
+ }
1947
+ const value = m[2].trim();
1948
+ return depth === 0 && value ? [term, m[1], value] : null;
1949
+ }
1950
+
1848
1951
  /** A `@media` width is compared with the viewport in CSS pixels — the
1849
1952
  * cascade divides the device width by the scale before asking — so the
1850
1953
  * thresholds parse at scale 1 whatever panel the document lands on. */