@react-x11/components 0.12.0 → 0.14.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 (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +545 -126
  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 +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -773,6 +773,24 @@ export function lightDark(
773
773
  value: string,
774
774
  scheme: 'light' | 'dark',
775
775
  ): string | null {
776
+ // kept by the value: the UA sheet's link colour is one, so every link of
777
+ // every document asks, and a theme's are a few asked by every element
778
+ const kept = PICKED[scheme];
779
+ let picked = kept.get(value);
780
+ if (picked === undefined) {
781
+ picked = pickScheme(value, scheme);
782
+ if (kept.size >= 512) kept.clear();
783
+ kept.set(value, picked);
784
+ }
785
+ return picked;
786
+ }
787
+
788
+ const PICKED = {
789
+ light: new Map<string, string | null>(),
790
+ dark: new Map<string, string | null>(),
791
+ };
792
+
793
+ function pickScheme(value: string, scheme: 'light' | 'dark'): string | null {
776
794
  let out = value;
777
795
  // a branch can hold another, which the next round replaces
778
796
  for (let round = 0; round < 32; round += 1) {
@@ -827,6 +845,17 @@ export function usedColorScheme(
827
845
  return first ?? preferred;
828
846
  }
829
847
 
848
+ /**
849
+ * A browser's own colours in each scheme — the system colours `Canvas`,
850
+ * `CanvasText` and `LinkText` (CSS Color 4, 6.2), as Chrome has them: what
851
+ * a document is drawn in where its root's scheme is not the palette's,
852
+ * which has the colours of its own scheme and of no other.
853
+ */
854
+ export const SCHEME_COLORS = {
855
+ light: { canvas: '#ffffff', text: '#000000', link: '#0000ee' },
856
+ dark: { canvas: '#121212', text: '#ffffff', link: '#9e9eff' },
857
+ } as const;
858
+
830
859
  const SCHEME_IDENT = /^-?(?:[a-z_]|--)[\w-]*$/;
831
860
  /** What a scheme's name cannot be: the keywords the grammar has, and the
832
861
  * CSS-wide ones, which reach here only in a list. */
@@ -56,10 +56,22 @@ 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. */
62
66
  breakpoints: number[];
67
+ /** Whether a `@media` rule in this sheet tests the viewport's height,
68
+ * which makes a document restyle when the height moves, as a `vh`
69
+ * does. */
70
+ readsHeight?: boolean;
71
+ /** Whether one tests its aspect ratio, which a width that crosses no
72
+ * breakpoint can change: the document restyles as one with a `vw`
73
+ * does. */
74
+ readsWidth?: boolean;
63
75
  /** The `@font-face` rules, in order (see `fonts.ts`). */
64
76
  fontFaces: FontFaceRule[];
65
77
  /** `@counter-style` rules, in order: a name and its descriptors, which
@@ -76,8 +88,9 @@ export interface Stylesheet {
76
88
  export interface FontFaceRule {
77
89
  /** The family as the document names it, unquoted. */
78
90
  family: string;
79
- /** The `url()`s of `src`, in order, each with its `format()` hint
80
- * 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. */
81
94
  sources: FontFaceSource[];
82
95
  /** The weights the face covers: `[400, 400]` for a static regular, a
83
96
  * range for a variable face. */
@@ -89,17 +102,31 @@ export interface FontFaceRule {
89
102
  media: MediaCondition[][] | null;
90
103
  }
91
104
 
92
- export interface FontFaceSource {
93
- url: string;
94
- format: string | null;
95
- }
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 };
96
109
 
97
- /** The tests this evaluates live: a width, a colour scheme, or both.
110
+ /** The tests this evaluates live: a width, a height, a colour scheme.
98
111
  * Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
99
112
  * decided once, at parse time, by `staticPass`. */
100
113
  export interface MediaCondition {
101
114
  min?: number;
102
115
  max?: number;
116
+ /** The viewport's height, as `min`/`max` its width: `min-height`,
117
+ * `max-height` and a range on `height` (Media Queries 4, 4.2). */
118
+ minHeight?: number;
119
+ maxHeight?: number;
120
+ /** The display's resolution in dots per CSS pixel, its scale: `min-`
121
+ * and `max-resolution`, and WebKit's `-webkit-min-device-pixel-ratio`,
122
+ * which a design writes to swap in its high-DPI images. */
123
+ minResolution?: number;
124
+ maxResolution?: number;
125
+ /** The viewport's width over its height: `aspect-ratio` and its `min-`
126
+ * and `max-`, the device's the viewport's too, and `orientation`, which
127
+ * is landscape where the width is the greater. */
128
+ minAspect?: number;
129
+ maxAspect?: number;
103
130
  /** `prefers-color-scheme`, answered from the palette in force. */
104
131
  scheme?: 'light' | 'dark';
105
132
  /** Set when the query could not be evaluated as a width or scheme test:
@@ -154,16 +181,38 @@ export function parseStylesheet(
154
181
  * the document's for a `<style>` — or null to leave them as written.
155
182
  * Every `url()`, `@import` and `@font-face` source comes out absolute. */
156
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,
157
189
  ): Stylesheet {
158
190
  text = withoutComments(text);
159
191
  const sheet: Stylesheet = {
160
192
  rules: [],
161
193
  imports: [],
194
+ importConditions: [],
162
195
  breakpoints: [],
163
196
  fontFaces: [],
164
197
  };
165
198
  let order = startOrder;
166
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);
167
216
  // `@import` counts only ahead of every other rule, `@charset` aside
168
217
  let importsAllowed = true;
169
218
 
@@ -211,10 +260,12 @@ export function parseStylesheet(
211
260
  const name = at.name.toLowerCase();
212
261
  if (name === 'charset') continue;
213
262
  if (name === 'import') {
214
- if (importsAllowed && media === null) {
263
+ if (importsAllowed && media === under) {
215
264
  const url = importUrl(at.prelude);
216
- if (url && importApplies(at.prelude)) {
265
+ const conditions = importConditions(at.prelude);
266
+ if (url && conditions !== false) {
217
267
  sheet.imports.push(resolveUrl(url, base));
268
+ sheet.importConditions.push(conditions);
218
269
  }
219
270
  }
220
271
  continue;
@@ -226,10 +277,7 @@ export function parseStylesheet(
226
277
  }
227
278
  if (name === 'media' && at.block !== null) {
228
279
  const conditions = parseMediaQuery(at.prelude);
229
- for (const c of conditions) {
230
- if (c.min !== undefined) breakpoints.add(c.min);
231
- if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
232
- }
280
+ note(conditions);
233
281
  // A nested `@media` intersects with the one above it; pushing a
234
282
  // level rather than merging keeps "all of these blocks hold" exact
235
283
  // when two of them overlap.
@@ -335,6 +383,13 @@ export function parseStylesheet(
335
383
  for (const c of conditions) {
336
384
  if (c.min !== undefined) breakpoints.add(c.min);
337
385
  if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
386
+ if (c.minHeight !== undefined || c.maxHeight !== undefined) {
387
+ sheet.readsHeight = true;
388
+ }
389
+ if (c.minAspect !== undefined || c.maxAspect !== undefined) {
390
+ sheet.readsHeight = true;
391
+ sheet.readsWidth = true;
392
+ }
338
393
  }
339
394
  styleRule(
340
395
  selectors,
@@ -378,7 +433,7 @@ export function parseStylesheet(
378
433
  }
379
434
  };
380
435
 
381
- walk(text, null);
436
+ walk(text, under);
382
437
  sheet.breakpoints = [...breakpoints].sort((a, b) => a - b);
383
438
  return sheet;
384
439
  }
@@ -1355,17 +1410,36 @@ function readAtRule(text: string, at: number): AtRule {
1355
1410
  * divides a device width into (`640.5` at 2x) fall on the right side. */
1356
1411
  const MAX_EDGE = 1 / 64;
1357
1412
 
1413
+ /**
1414
+ * The media features that are the viewport's width and its height here.
1415
+ * `device-width` and `device-height` are the Web-exposed screen area's
1416
+ * (Media Queries 4, appendix A), which a user agent may answer with the
1417
+ * viewport's (CSSOM View 2.3), and a document drawn into an element has no
1418
+ * screen of its own to answer with. Deprecated, and still what a page from
1419
+ * before `width` asks with: DuckDuckGo Lite's phone sheet is
1420
+ * `(max-device-width: 700px)`, and a query that went unread held, so a
1421
+ * desktop window got the phone's 12px dropdowns.
1422
+ */
1423
+ const VIEWPORT_SIDES = {
1424
+ width: new Set(['width', 'device-width']),
1425
+ height: new Set(['height', 'device-height']),
1426
+ };
1427
+
1358
1428
  /**
1359
1429
  * A width range in Media Queries 4's syntax — `(width >= 48rem)`, `(60rem >
1360
1430
  * width)`, `(40rem <= width < 60rem)` — as the widths it holds between, or
1361
1431
  * null for a term that is not one. A strict bound is a sixty-fourth of a
1362
1432
  * pixel inside the value, where a viewport's width never lands.
1363
1433
  */
1364
- function widthRange(term: string): { min?: number; max?: number } | null {
1434
+ function widthRange(
1435
+ term: string,
1436
+ feature: 'width' | 'height' = 'width',
1437
+ ): { min?: number; max?: number } | null {
1365
1438
  const inner = /^\(\s*(.*?)\s*\)$/.exec(term)?.[1];
1366
1439
  if (!inner || !/[<>=]/.test(inner)) return null;
1367
1440
  const parts = inner.split(/\s*(<=|>=|<|>|=)\s*/);
1368
- const isWidth = (part: string) => part.toLowerCase() === 'width';
1441
+ const isWidth = (part: string) =>
1442
+ VIEWPORT_SIDES[feature].has(part.toLowerCase());
1369
1443
  const out: { min?: number; max?: number } = {};
1370
1444
  // `width OP value`, with the operator read from the width's side
1371
1445
  const bound = (op: string, value: string): boolean => {
@@ -1536,18 +1610,28 @@ function importUrl(prelude: string): string | null {
1536
1610
  return m ? m[1] : null;
1537
1611
  }
1538
1612
 
1539
- /** Whether an `@import`'s media list, the rest of its prelude after the
1540
- * URL, lets it apply here: a list that only names other media, `print` or
1541
- * `braille`, leaves the sheet out (CSS 2.1 6.3). A list that depends on the
1542
- * width is taken to apply; an import is fetched once, not per width. */
1543
- 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 {
1544
1625
  const m =
1545
1626
  /^\s*(?:url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)|"[^"]*"|'[^']*')(.*)$/s.exec(
1546
1627
  prelude,
1547
1628
  );
1548
1629
  const list = m?.[1].trim();
1549
- if (!list) return true;
1550
- 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;
1551
1635
  }
1552
1636
 
1553
1637
  /**
@@ -1629,10 +1713,13 @@ const GENERIC_FAMILIES = new Set([
1629
1713
  ]);
1630
1714
 
1631
1715
  /**
1632
- * A `@font-face` block, or null when it names no family, or no source this
1633
- * can ask for: `local()` names a face the system has, which this does not
1634
- * look up, and a family that is only `local()`s is left to the system as it
1635
- * 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.
1636
1723
  */
1637
1724
  function parseFontFace(
1638
1725
  block: string,
@@ -1665,6 +1752,14 @@ function parseFontFace(
1665
1752
  const sources: FontFaceSource[] = [];
1666
1753
  for (const part of splitTopLevel(src, ',')) {
1667
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
+ }
1668
1763
  if (!/^url\(/i.test(item)) continue;
1669
1764
  const end = urlEnd(item, 3);
1670
1765
  const url = parseUrl(item.slice(0, end));
@@ -1760,8 +1855,15 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1760
1855
  const condition: MediaCondition = {};
1761
1856
  let pass = true;
1762
1857
  let sawWidth = false;
1858
+ let sawHeight = false;
1859
+ let sawResolution = false;
1860
+ let sawAspect = false;
1763
1861
  const negated = /^\s*not\b/i.test(group);
1764
- for (const part of group.split(/\s+and\s+/i)) {
1862
+ // `and` needs white space after it, where a `(` would make it a
1863
+ // function, and none after the `)` it follows: DuckDuckGo writes
1864
+ // `(max-device-width: 701px)and (orientation: landscape)`, which read
1865
+ // as one term that was no feature, and held
1866
+ for (const part of group.split(/(?:\s+|(?<=\)))and\s+/i)) {
1765
1867
  const term = part
1766
1868
  .trim()
1767
1869
  .replace(/^not\s+/i, '')
@@ -1780,17 +1882,61 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1780
1882
  sawWidth = true;
1781
1883
  continue;
1782
1884
  }
1885
+ const heights = widthRange(term, 'height');
1886
+ if (heights) {
1887
+ if (heights.min !== undefined) {
1888
+ condition.minHeight = Math.max(condition.minHeight ?? 0, heights.min);
1889
+ }
1890
+ if (heights.max !== undefined) {
1891
+ condition.maxHeight = Math.min(
1892
+ condition.maxHeight ?? Infinity,
1893
+ heights.max,
1894
+ );
1895
+ }
1896
+ sawHeight = true;
1897
+ continue;
1898
+ }
1783
1899
  const feature = mediaFeature(term);
1784
1900
  if (feature) {
1785
1901
  const key = feature[1].toLowerCase();
1786
1902
  const len = parseLength(feature[2], ZERO_UNITS);
1787
1903
  const px = typeof len === 'number' ? len : null;
1788
- if (key === 'min-width' && px !== null) {
1904
+ if (
1905
+ (key === 'min-width' || key === 'min-device-width') &&
1906
+ px !== null
1907
+ ) {
1789
1908
  condition.min = Math.max(condition.min ?? 0, px);
1790
1909
  sawWidth = true;
1791
- } else if (key === 'max-width' && px !== null) {
1910
+ } else if (
1911
+ (key === 'max-width' || key === 'max-device-width') &&
1912
+ px !== null
1913
+ ) {
1792
1914
  condition.max = Math.min(condition.max ?? Infinity, px);
1793
1915
  sawWidth = true;
1916
+ } else if (
1917
+ px === null &&
1918
+ /^(?:min-|max-)?(?:device-)?(?:width|height)$/.test(key)
1919
+ ) {
1920
+ // a size that is no length is a query that does not parse, which
1921
+ // Media Queries 4 (3.2) makes `not all`: `(min-width:0\0)`, the
1922
+ // hack that kept a block for Internet Explorer 9 and 10, held
1923
+ // here as a query that asked nothing, and its rules applied
1924
+ pass = false;
1925
+ } else if (
1926
+ (key === 'min-height' || key === 'min-device-height') &&
1927
+ px !== null
1928
+ ) {
1929
+ // the viewport's height, answered live as its width is: a
1930
+ // design that sets its heading's size by the window's height
1931
+ // took the tallest step's at every height
1932
+ condition.minHeight = Math.max(condition.minHeight ?? 0, px);
1933
+ sawHeight = true;
1934
+ } else if (
1935
+ (key === 'max-height' || key === 'max-device-height') &&
1936
+ px !== null
1937
+ ) {
1938
+ condition.maxHeight = Math.min(condition.maxHeight ?? Infinity, px);
1939
+ sawHeight = true;
1794
1940
  } else if (key === 'prefers-color-scheme') {
1795
1941
  // Answered live, from the palette in force: a document dropped
1796
1942
  // into a dark application takes its dark branch, and follows the
@@ -1809,7 +1955,73 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1809
1955
  // renderer needs.
1810
1956
  pass = false;
1811
1957
  } else if (key === 'orientation') {
1812
- pass = feature[2].trim().toLowerCase() === 'landscape';
1958
+ // the viewport's, as its aspect ratio: portrait where its height
1959
+ // is at least its width (Media Queries 4, 4.5)
1960
+ const orientation = feature[2].trim().toLowerCase();
1961
+ if (orientation === 'portrait') {
1962
+ condition.maxAspect = Math.min(condition.maxAspect ?? Infinity, 1);
1963
+ sawAspect = true;
1964
+ } else if (orientation === 'landscape') {
1965
+ condition.minAspect = Math.max(
1966
+ condition.minAspect ?? 0,
1967
+ 1 + ASPECT_EDGE,
1968
+ );
1969
+ sawAspect = true;
1970
+ } else pass = false;
1971
+ } else if (ASPECT_FEATURES.test(key)) {
1972
+ const ratio = ratioOf(feature[2]);
1973
+ if (ratio === null) pass = false;
1974
+ else {
1975
+ if (!key.startsWith('max-')) {
1976
+ condition.minAspect = Math.max(condition.minAspect ?? 0, ratio);
1977
+ }
1978
+ if (!key.startsWith('min-')) {
1979
+ condition.maxAspect = Math.min(
1980
+ condition.maxAspect ?? Infinity,
1981
+ ratio,
1982
+ );
1983
+ }
1984
+ sawAspect = true;
1985
+ }
1986
+ } else if (RESOLUTION_FEATURES.has(key)) {
1987
+ const dppx = resolutionOf(feature[2], key.startsWith('-webkit-'));
1988
+ if (dppx === null) pass = false;
1989
+ else {
1990
+ if (!key.includes('max-')) {
1991
+ condition.minResolution = Math.max(
1992
+ condition.minResolution ?? 0,
1993
+ dppx,
1994
+ );
1995
+ }
1996
+ if (!key.includes('min-')) {
1997
+ condition.maxResolution = Math.min(
1998
+ condition.maxResolution ?? Infinity,
1999
+ dppx,
2000
+ );
2001
+ }
2002
+ sawResolution = true;
2003
+ }
2004
+ } else if (desktopFeature(key, feature[2]) !== true) {
2005
+ // any other feature as a desktop screen with a mouse answers
2006
+ // it, and one nothing knows is false (Media Queries 4, 3.2):
2007
+ // Firefox's and Opera's resolution features, which a design lists
2008
+ // beside WebKit's, held here, and a page at one dot to the pixel
2009
+ // took the rules it keeps for two
2010
+ pass = false;
2011
+ }
2012
+ continue;
2013
+ }
2014
+ const flag = /^\(\s*(-?[a-z][a-z0-9-]*)\s*\)$/i.exec(term);
2015
+ if (flag) {
2016
+ // a feature in the boolean context, `(hover)`: true where its value
2017
+ // is anything but zero or none
2018
+ const key = flag[1].toLowerCase();
2019
+ if (
2020
+ !BOOLEAN_TRUE.has(key) &&
2021
+ !RESOLUTION_FEATURES.has(key) &&
2022
+ desktopFeature(key, null) !== true
2023
+ ) {
2024
+ pass = false;
1813
2025
  }
1814
2026
  continue;
1815
2027
  }
@@ -1825,11 +2037,23 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1825
2037
  // `not` over a scheme is the other scheme. `not` over a width range
1826
2038
  // is not expressible as one range; the honest reduction is to decide
1827
2039
  // it statically rather than invert it wrongly.
1828
- if (!sawWidth && pass && condition.scheme) {
2040
+ if (
2041
+ !sawWidth &&
2042
+ !sawHeight &&
2043
+ !sawResolution &&
2044
+ !sawAspect &&
2045
+ pass &&
2046
+ condition.scheme
2047
+ ) {
1829
2048
  out.push({ scheme: condition.scheme === 'dark' ? 'light' : 'dark' });
1830
2049
  continue;
1831
2050
  }
1832
- out.push({ staticPass: !sawWidth && pass ? false : !pass });
2051
+ out.push({
2052
+ staticPass:
2053
+ !sawWidth && !sawHeight && !sawResolution && !sawAspect && pass
2054
+ ? false
2055
+ : !pass,
2056
+ });
1833
2057
  continue;
1834
2058
  }
1835
2059
  if (!pass) {
@@ -1838,6 +2062,9 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1838
2062
  }
1839
2063
  if (
1840
2064
  !sawWidth &&
2065
+ !sawHeight &&
2066
+ !sawResolution &&
2067
+ !sawAspect &&
1841
2068
  condition.scheme === undefined &&
1842
2069
  condition.min === undefined &&
1843
2070
  condition.max === undefined
@@ -1936,6 +2163,123 @@ export function supportsCondition(prelude: string): boolean | null {
1936
2163
  * first `)` found no feature there — the term was passed over as though it
1937
2164
  * said nothing, and every narrow-screen rule held at every width.
1938
2165
  */
2166
+ /** `aspect-ratio` and the device's, bare or `min-`/`max-`. */
2167
+ const ASPECT_FEATURES = /^(?:min-|max-)?(?:device-)?aspect-ratio$/;
2168
+
2169
+ /** How far past square a landscape viewport is: any width over the height. */
2170
+ const ASPECT_EDGE = 1e-9;
2171
+
2172
+ /** A ratio, `16/9` or `16 / 9` or `1.5`, as a number; null for anything
2173
+ * else, or a ratio of nothing. */
2174
+ function ratioOf(value: string): number | null {
2175
+ const m = /^\s*(\d*\.?\d+)\s*(?:\/\s*(\d*\.?\d+)\s*)?$/.exec(value);
2176
+ if (!m) return null;
2177
+ const ratio = Number(m[1]) / (m[2] === undefined ? 1 : Number(m[2]));
2178
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : null;
2179
+ }
2180
+
2181
+ /** The resolution features, the standard's and WebKit's, which Chrome
2182
+ * answers; `min--moz-device-pixel-ratio` and `-o-min-device-pixel-ratio`
2183
+ * are no feature of its, and none of this. */
2184
+ const RESOLUTION_FEATURES = new Set([
2185
+ 'resolution',
2186
+ 'min-resolution',
2187
+ 'max-resolution',
2188
+ '-webkit-device-pixel-ratio',
2189
+ '-webkit-min-device-pixel-ratio',
2190
+ '-webkit-max-device-pixel-ratio',
2191
+ ]);
2192
+
2193
+ /** A resolution in dots per CSS pixel: `2dppx`, `2x`, `192dpi`, `75.6dpcm`,
2194
+ * or WebKit's device pixel ratio, a number. Null for anything else. */
2195
+ function resolutionOf(value: string, ratio: boolean): number | null {
2196
+ const m = /^\s*(\d*\.?\d+(?:e[+-]?\d+)?)\s*(dppx|x|dpi|dpcm)?\s*$/i.exec(
2197
+ value,
2198
+ );
2199
+ if (!m) return null;
2200
+ const n = Number(m[1]);
2201
+ const unit = (m[2] ?? '').toLowerCase();
2202
+ if (ratio) return unit ? null : n;
2203
+ if (unit === 'dppx' || unit === 'x') return n;
2204
+ if (unit === 'dpi') return n / 96;
2205
+ if (unit === 'dpcm') return (n * 2.54) / 96;
2206
+ return null;
2207
+ }
2208
+
2209
+ /** The features true in the boolean context whatever the viewport: a
2210
+ * size, a scheme, an orientation, there is always one. */
2211
+ const BOOLEAN_TRUE = new Set([
2212
+ 'width',
2213
+ 'height',
2214
+ 'device-width',
2215
+ 'device-height',
2216
+ 'aspect-ratio',
2217
+ 'orientation',
2218
+ 'prefers-color-scheme',
2219
+ ]);
2220
+
2221
+ /**
2222
+ * A media feature as a desktop screen driven by a mouse answers it — the
2223
+ * device `<Html>` draws on — for the features that do not depend on the
2224
+ * viewport: true, false, or null for a feature nothing knows, which is
2225
+ * false (Media Queries 4, 3.2). `value` is null in the boolean context.
2226
+ * The colour is eight bits a component on no palette and no grid; scripts
2227
+ * never run here, so `scripting` is `none`.
2228
+ */
2229
+ function desktopFeature(key: string, value: string | null): boolean | null {
2230
+ const v = value?.trim().toLowerCase() ?? null;
2231
+ const numeric = (feature: string, has: number): boolean | null => {
2232
+ const bare = key.replace(/^(min|max)-/, '');
2233
+ if (bare !== feature) return null;
2234
+ if (v === null) return key === feature ? has !== 0 : null;
2235
+ const n = Number(v);
2236
+ if (!Number.isFinite(n)) return false;
2237
+ if (key.startsWith('min-')) return has >= n;
2238
+ if (key.startsWith('max-')) return has <= n;
2239
+ return has === n;
2240
+ };
2241
+ const keyword = (answers: Record<string, boolean>, bool: boolean) =>
2242
+ v === null ? bool : (answers[v] ?? false);
2243
+ switch (key) {
2244
+ case 'hover':
2245
+ case 'any-hover':
2246
+ return keyword({ hover: true, none: false }, true);
2247
+ case 'pointer':
2248
+ case 'any-pointer':
2249
+ return keyword({ fine: true, coarse: false, none: false }, true);
2250
+ case 'update':
2251
+ return keyword({ fast: true, slow: false, none: false }, true);
2252
+ case 'overflow-block':
2253
+ case 'overflow-inline':
2254
+ return keyword({ scroll: true }, true);
2255
+ case 'scan':
2256
+ return false;
2257
+ case 'color-gamut':
2258
+ return keyword({ srgb: true }, true);
2259
+ case 'dynamic-range':
2260
+ case 'video-dynamic-range':
2261
+ return keyword({ standard: true }, true);
2262
+ case 'prefers-contrast':
2263
+ case 'prefers-reduced-transparency':
2264
+ case 'prefers-reduced-data':
2265
+ return keyword({ 'no-preference': true }, false);
2266
+ case 'forced-colors':
2267
+ return keyword({ none: true }, false);
2268
+ case 'scripting':
2269
+ return keyword({ none: true }, false);
2270
+ case 'display-mode':
2271
+ return keyword({ browser: true }, true);
2272
+ case '-webkit-transform-3d':
2273
+ return v === null || v === '1';
2274
+ }
2275
+ return (
2276
+ numeric('color', 8) ??
2277
+ numeric('monochrome', 0) ??
2278
+ numeric('color-index', 0) ??
2279
+ numeric('grid', 0)
2280
+ );
2281
+ }
2282
+
1939
2283
  function mediaFeature(term: string): [string, string, string] | null {
1940
2284
  const m = /^\(\s*([a-z-]+)\s*:([\s\S]*)\)$/i.exec(term);
1941
2285
  if (!m) return null;
@@ -1959,6 +2303,11 @@ export function mediaMatches(
1959
2303
  media: MediaCondition[][] | null,
1960
2304
  width: number,
1961
2305
  scheme: 'light' | 'dark' = 'light',
2306
+ /** The viewport's height in CSS pixels; a test of it holds at no height
2307
+ * where none is given. */
2308
+ height = NaN,
2309
+ /** The display's scale, device pixels to the CSS pixel. */
2310
+ resolution = 1,
1962
2311
  ): boolean {
1963
2312
  if (!media) return true;
1964
2313
  for (const block of media) {
@@ -1971,6 +2320,12 @@ export function mediaMatches(
1971
2320
  if (
1972
2321
  (c.min === undefined || width >= c.min) &&
1973
2322
  (c.max === undefined || width <= c.max) &&
2323
+ (c.minHeight === undefined || height >= c.minHeight) &&
2324
+ (c.maxHeight === undefined || height <= c.maxHeight) &&
2325
+ (c.minResolution === undefined || resolution >= c.minResolution) &&
2326
+ (c.minAspect === undefined || width >= c.minAspect * height) &&
2327
+ (c.maxAspect === undefined || width <= c.maxAspect * height) &&
2328
+ (c.maxResolution === undefined || resolution <= c.maxResolution) &&
1974
2329
  (c.scheme === undefined || c.scheme === scheme)
1975
2330
  ) {
1976
2331
  any = true;