@react-x11/components 0.11.0 → 0.13.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.
- package/dist/flow/node.d.ts +14 -6
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +82 -64
- package/dist/flow/node.js.map +1 -1
- package/dist/html/controls.d.ts +38 -10
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +43 -58
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +538 -128
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +19 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +86 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +40 -2
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +391 -13
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +52 -1
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +328 -11
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -7
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +10 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +27 -2
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +5 -0
- package/dist/html/css/vars.d.ts.map +1 -1
- package/dist/html/css/vars.js +2 -2
- package/dist/html/css/vars.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +38 -3
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +25 -1
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +99 -6
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -264
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +11 -2
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +186 -19
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +65 -7
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +87 -22
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +2 -2
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +8 -3
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/flex.js +4 -1
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +4 -0
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +26 -0
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/inline.d.ts +26 -2
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +1232 -115
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +120 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +993 -174
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +75 -2
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +836 -200
- package/dist/html/paint.js.map +1 -1
- package/dist/html/surfaces.d.ts +7 -2
- package/dist/html/surfaces.d.ts.map +1 -1
- package/dist/html/surfaces.js +16 -0
- package/dist/html/surfaces.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +620 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/window.d.ts +22 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +35 -8
- package/dist/internal/window.js.map +1 -1
- package/dist/richtext/node.d.ts +7 -0
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js.map +1 -1
- package/package.json +6 -4
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +79 -58
- package/src/html/css/cascade.ts +610 -146
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +431 -13
- package/src/html/css/style.ts +367 -11
- package/src/html/css/ua.ts +76 -8
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +41 -3
- package/src/html/fonts.ts +116 -6
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -300
- package/src/html/layout/block.ts +231 -16
- package/src/html/layout/boxes.ts +133 -32
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/flex.ts +4 -1
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1445 -108
- package/src/html/node.ts +1010 -166
- package/src/html/paint.ts +980 -270
- package/src/html/surfaces.ts +21 -1
- package/src/html/widgets.ts +735 -0
- package/src/index.ts +1 -0
- package/src/internal/window.ts +36 -8
- package/src/richtext/node.ts +7 -0
package/src/html/css/color.ts
CHANGED
|
@@ -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
|
+
]);
|
package/src/html/css/parse.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface Stylesheet {
|
|
|
60
60
|
* keeps these so a resize can tell "the layout changed" from "the
|
|
61
61
|
* *cascade* changed", and restyle only when it crossed one. */
|
|
62
62
|
breakpoints: number[];
|
|
63
|
+
/** Whether a `@media` rule in this sheet tests the viewport's height,
|
|
64
|
+
* which makes a document restyle when the height moves, as a `vh`
|
|
65
|
+
* does. */
|
|
66
|
+
readsHeight?: boolean;
|
|
67
|
+
/** Whether one tests its aspect ratio, which a width that crosses no
|
|
68
|
+
* breakpoint can change: the document restyles as one with a `vw`
|
|
69
|
+
* does. */
|
|
70
|
+
readsWidth?: boolean;
|
|
63
71
|
/** The `@font-face` rules, in order (see `fonts.ts`). */
|
|
64
72
|
fontFaces: FontFaceRule[];
|
|
65
73
|
/** `@counter-style` rules, in order: a name and its descriptors, which
|
|
@@ -94,12 +102,26 @@ export interface FontFaceSource {
|
|
|
94
102
|
format: string | null;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
/** The tests this evaluates live: a width, a colour scheme
|
|
105
|
+
/** The tests this evaluates live: a width, a height, a colour scheme.
|
|
98
106
|
* Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
|
|
99
107
|
* decided once, at parse time, by `staticPass`. */
|
|
100
108
|
export interface MediaCondition {
|
|
101
109
|
min?: number;
|
|
102
110
|
max?: number;
|
|
111
|
+
/** The viewport's height, as `min`/`max` its width: `min-height`,
|
|
112
|
+
* `max-height` and a range on `height` (Media Queries 4, 4.2). */
|
|
113
|
+
minHeight?: number;
|
|
114
|
+
maxHeight?: number;
|
|
115
|
+
/** The display's resolution in dots per CSS pixel, its scale: `min-`
|
|
116
|
+
* and `max-resolution`, and WebKit's `-webkit-min-device-pixel-ratio`,
|
|
117
|
+
* which a design writes to swap in its high-DPI images. */
|
|
118
|
+
minResolution?: number;
|
|
119
|
+
maxResolution?: number;
|
|
120
|
+
/** The viewport's width over its height: `aspect-ratio` and its `min-`
|
|
121
|
+
* and `max-`, the device's the viewport's too, and `orientation`, which
|
|
122
|
+
* is landscape where the width is the greater. */
|
|
123
|
+
minAspect?: number;
|
|
124
|
+
maxAspect?: number;
|
|
103
125
|
/** `prefers-color-scheme`, answered from the palette in force. */
|
|
104
126
|
scheme?: 'light' | 'dark';
|
|
105
127
|
/** Set when the query could not be evaluated as a width or scheme test:
|
|
@@ -229,6 +251,13 @@ export function parseStylesheet(
|
|
|
229
251
|
for (const c of conditions) {
|
|
230
252
|
if (c.min !== undefined) breakpoints.add(c.min);
|
|
231
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
|
+
}
|
|
232
261
|
}
|
|
233
262
|
// A nested `@media` intersects with the one above it; pushing a
|
|
234
263
|
// level rather than merging keeps "all of these blocks hold" exact
|
|
@@ -242,8 +271,11 @@ export function parseStylesheet(
|
|
|
242
271
|
} else if (name === 'supports' && at.block !== null) {
|
|
243
272
|
// Everything in a `@supports` block is markup this renderer either
|
|
244
273
|
// understands or ignores per-declaration, so entering it is closer
|
|
245
|
-
// to right than skipping it
|
|
246
|
-
|
|
274
|
+
// to right than skipping it — but for a condition it can answer
|
|
275
|
+
// false (`supportsCondition`)
|
|
276
|
+
if (supportsCondition(at.prelude) !== false) {
|
|
277
|
+
walk(at.block, media, layer, path);
|
|
278
|
+
}
|
|
247
279
|
} else if (name === 'layer') {
|
|
248
280
|
// `@layer a, b;` names layers, and so fixes their order, and
|
|
249
281
|
// `@layer a { … }` puts rules in one; one with no name is a layer
|
|
@@ -332,6 +364,13 @@ export function parseStylesheet(
|
|
|
332
364
|
for (const c of conditions) {
|
|
333
365
|
if (c.min !== undefined) breakpoints.add(c.min);
|
|
334
366
|
if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
|
|
367
|
+
if (c.minHeight !== undefined || c.maxHeight !== undefined) {
|
|
368
|
+
sheet.readsHeight = true;
|
|
369
|
+
}
|
|
370
|
+
if (c.minAspect !== undefined || c.maxAspect !== undefined) {
|
|
371
|
+
sheet.readsHeight = true;
|
|
372
|
+
sheet.readsWidth = true;
|
|
373
|
+
}
|
|
335
374
|
}
|
|
336
375
|
styleRule(
|
|
337
376
|
selectors,
|
|
@@ -342,7 +381,9 @@ export function parseStylesheet(
|
|
|
342
381
|
depth + 1,
|
|
343
382
|
);
|
|
344
383
|
} else if (name === 'supports') {
|
|
345
|
-
|
|
384
|
+
if (supportsCondition(item.prelude) !== false) {
|
|
385
|
+
styleRule(selectors, item.block, media, layer, path, depth + 1);
|
|
386
|
+
}
|
|
346
387
|
} else if (name === 'layer' && LAYER_NAME.test(item.prelude.trim())) {
|
|
347
388
|
const [ranks, full] = enter(item.prelude.trim(), layer, path);
|
|
348
389
|
styleRule(selectors, item.block, media, ranks, full, depth + 1);
|
|
@@ -1350,17 +1391,36 @@ function readAtRule(text: string, at: number): AtRule {
|
|
|
1350
1391
|
* divides a device width into (`640.5` at 2x) fall on the right side. */
|
|
1351
1392
|
const MAX_EDGE = 1 / 64;
|
|
1352
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* The media features that are the viewport's width and its height here.
|
|
1396
|
+
* `device-width` and `device-height` are the Web-exposed screen area's
|
|
1397
|
+
* (Media Queries 4, appendix A), which a user agent may answer with the
|
|
1398
|
+
* viewport's (CSSOM View 2.3), and a document drawn into an element has no
|
|
1399
|
+
* screen of its own to answer with. Deprecated, and still what a page from
|
|
1400
|
+
* before `width` asks with: DuckDuckGo Lite's phone sheet is
|
|
1401
|
+
* `(max-device-width: 700px)`, and a query that went unread held, so a
|
|
1402
|
+
* desktop window got the phone's 12px dropdowns.
|
|
1403
|
+
*/
|
|
1404
|
+
const VIEWPORT_SIDES = {
|
|
1405
|
+
width: new Set(['width', 'device-width']),
|
|
1406
|
+
height: new Set(['height', 'device-height']),
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1353
1409
|
/**
|
|
1354
1410
|
* A width range in Media Queries 4's syntax — `(width >= 48rem)`, `(60rem >
|
|
1355
1411
|
* width)`, `(40rem <= width < 60rem)` — as the widths it holds between, or
|
|
1356
1412
|
* null for a term that is not one. A strict bound is a sixty-fourth of a
|
|
1357
1413
|
* pixel inside the value, where a viewport's width never lands.
|
|
1358
1414
|
*/
|
|
1359
|
-
function widthRange(
|
|
1415
|
+
function widthRange(
|
|
1416
|
+
term: string,
|
|
1417
|
+
feature: 'width' | 'height' = 'width',
|
|
1418
|
+
): { min?: number; max?: number } | null {
|
|
1360
1419
|
const inner = /^\(\s*(.*?)\s*\)$/.exec(term)?.[1];
|
|
1361
1420
|
if (!inner || !/[<>=]/.test(inner)) return null;
|
|
1362
1421
|
const parts = inner.split(/\s*(<=|>=|<|>|=)\s*/);
|
|
1363
|
-
const isWidth = (part: string) =>
|
|
1422
|
+
const isWidth = (part: string) =>
|
|
1423
|
+
VIEWPORT_SIDES[feature].has(part.toLowerCase());
|
|
1364
1424
|
const out: { min?: number; max?: number } = {};
|
|
1365
1425
|
// `width OP value`, with the operator read from the width's side
|
|
1366
1426
|
const bound = (op: string, value: string): boolean => {
|
|
@@ -1755,8 +1815,15 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
1755
1815
|
const condition: MediaCondition = {};
|
|
1756
1816
|
let pass = true;
|
|
1757
1817
|
let sawWidth = false;
|
|
1818
|
+
let sawHeight = false;
|
|
1819
|
+
let sawResolution = false;
|
|
1820
|
+
let sawAspect = false;
|
|
1758
1821
|
const negated = /^\s*not\b/i.test(group);
|
|
1759
|
-
|
|
1822
|
+
// `and` needs white space after it, where a `(` would make it a
|
|
1823
|
+
// function, and none after the `)` it follows: DuckDuckGo writes
|
|
1824
|
+
// `(max-device-width: 701px)and (orientation: landscape)`, which read
|
|
1825
|
+
// as one term that was no feature, and held
|
|
1826
|
+
for (const part of group.split(/(?:\s+|(?<=\)))and\s+/i)) {
|
|
1760
1827
|
const term = part
|
|
1761
1828
|
.trim()
|
|
1762
1829
|
.replace(/^not\s+/i, '')
|
|
@@ -1775,17 +1842,61 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
1775
1842
|
sawWidth = true;
|
|
1776
1843
|
continue;
|
|
1777
1844
|
}
|
|
1778
|
-
const
|
|
1845
|
+
const heights = widthRange(term, 'height');
|
|
1846
|
+
if (heights) {
|
|
1847
|
+
if (heights.min !== undefined) {
|
|
1848
|
+
condition.minHeight = Math.max(condition.minHeight ?? 0, heights.min);
|
|
1849
|
+
}
|
|
1850
|
+
if (heights.max !== undefined) {
|
|
1851
|
+
condition.maxHeight = Math.min(
|
|
1852
|
+
condition.maxHeight ?? Infinity,
|
|
1853
|
+
heights.max,
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
sawHeight = true;
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
const feature = mediaFeature(term);
|
|
1779
1860
|
if (feature) {
|
|
1780
1861
|
const key = feature[1].toLowerCase();
|
|
1781
1862
|
const len = parseLength(feature[2], ZERO_UNITS);
|
|
1782
1863
|
const px = typeof len === 'number' ? len : null;
|
|
1783
|
-
if (
|
|
1864
|
+
if (
|
|
1865
|
+
(key === 'min-width' || key === 'min-device-width') &&
|
|
1866
|
+
px !== null
|
|
1867
|
+
) {
|
|
1784
1868
|
condition.min = Math.max(condition.min ?? 0, px);
|
|
1785
1869
|
sawWidth = true;
|
|
1786
|
-
} else if (
|
|
1870
|
+
} else if (
|
|
1871
|
+
(key === 'max-width' || key === 'max-device-width') &&
|
|
1872
|
+
px !== null
|
|
1873
|
+
) {
|
|
1787
1874
|
condition.max = Math.min(condition.max ?? Infinity, px);
|
|
1788
1875
|
sawWidth = true;
|
|
1876
|
+
} else if (
|
|
1877
|
+
px === null &&
|
|
1878
|
+
/^(?:min-|max-)?(?:device-)?(?:width|height)$/.test(key)
|
|
1879
|
+
) {
|
|
1880
|
+
// a size that is no length is a query that does not parse, which
|
|
1881
|
+
// Media Queries 4 (3.2) makes `not all`: `(min-width:0\0)`, the
|
|
1882
|
+
// hack that kept a block for Internet Explorer 9 and 10, held
|
|
1883
|
+
// here as a query that asked nothing, and its rules applied
|
|
1884
|
+
pass = false;
|
|
1885
|
+
} else if (
|
|
1886
|
+
(key === 'min-height' || key === 'min-device-height') &&
|
|
1887
|
+
px !== null
|
|
1888
|
+
) {
|
|
1889
|
+
// the viewport's height, answered live as its width is: a
|
|
1890
|
+
// design that sets its heading's size by the window's height
|
|
1891
|
+
// took the tallest step's at every height
|
|
1892
|
+
condition.minHeight = Math.max(condition.minHeight ?? 0, px);
|
|
1893
|
+
sawHeight = true;
|
|
1894
|
+
} else if (
|
|
1895
|
+
(key === 'max-height' || key === 'max-device-height') &&
|
|
1896
|
+
px !== null
|
|
1897
|
+
) {
|
|
1898
|
+
condition.maxHeight = Math.min(condition.maxHeight ?? Infinity, px);
|
|
1899
|
+
sawHeight = true;
|
|
1789
1900
|
} else if (key === 'prefers-color-scheme') {
|
|
1790
1901
|
// Answered live, from the palette in force: a document dropped
|
|
1791
1902
|
// into a dark application takes its dark branch, and follows the
|
|
@@ -1804,7 +1915,73 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
1804
1915
|
// renderer needs.
|
|
1805
1916
|
pass = false;
|
|
1806
1917
|
} else if (key === 'orientation') {
|
|
1807
|
-
|
|
1918
|
+
// the viewport's, as its aspect ratio: portrait where its height
|
|
1919
|
+
// is at least its width (Media Queries 4, 4.5)
|
|
1920
|
+
const orientation = feature[2].trim().toLowerCase();
|
|
1921
|
+
if (orientation === 'portrait') {
|
|
1922
|
+
condition.maxAspect = Math.min(condition.maxAspect ?? Infinity, 1);
|
|
1923
|
+
sawAspect = true;
|
|
1924
|
+
} else if (orientation === 'landscape') {
|
|
1925
|
+
condition.minAspect = Math.max(
|
|
1926
|
+
condition.minAspect ?? 0,
|
|
1927
|
+
1 + ASPECT_EDGE,
|
|
1928
|
+
);
|
|
1929
|
+
sawAspect = true;
|
|
1930
|
+
} else pass = false;
|
|
1931
|
+
} else if (ASPECT_FEATURES.test(key)) {
|
|
1932
|
+
const ratio = ratioOf(feature[2]);
|
|
1933
|
+
if (ratio === null) pass = false;
|
|
1934
|
+
else {
|
|
1935
|
+
if (!key.startsWith('max-')) {
|
|
1936
|
+
condition.minAspect = Math.max(condition.minAspect ?? 0, ratio);
|
|
1937
|
+
}
|
|
1938
|
+
if (!key.startsWith('min-')) {
|
|
1939
|
+
condition.maxAspect = Math.min(
|
|
1940
|
+
condition.maxAspect ?? Infinity,
|
|
1941
|
+
ratio,
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
sawAspect = true;
|
|
1945
|
+
}
|
|
1946
|
+
} else if (RESOLUTION_FEATURES.has(key)) {
|
|
1947
|
+
const dppx = resolutionOf(feature[2], key.startsWith('-webkit-'));
|
|
1948
|
+
if (dppx === null) pass = false;
|
|
1949
|
+
else {
|
|
1950
|
+
if (!key.includes('max-')) {
|
|
1951
|
+
condition.minResolution = Math.max(
|
|
1952
|
+
condition.minResolution ?? 0,
|
|
1953
|
+
dppx,
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
if (!key.includes('min-')) {
|
|
1957
|
+
condition.maxResolution = Math.min(
|
|
1958
|
+
condition.maxResolution ?? Infinity,
|
|
1959
|
+
dppx,
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
sawResolution = true;
|
|
1963
|
+
}
|
|
1964
|
+
} else if (desktopFeature(key, feature[2]) !== true) {
|
|
1965
|
+
// any other feature as a desktop screen with a mouse answers
|
|
1966
|
+
// it, and one nothing knows is false (Media Queries 4, 3.2):
|
|
1967
|
+
// Firefox's and Opera's resolution features, which a design lists
|
|
1968
|
+
// beside WebKit's, held here, and a page at one dot to the pixel
|
|
1969
|
+
// took the rules it keeps for two
|
|
1970
|
+
pass = false;
|
|
1971
|
+
}
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
const flag = /^\(\s*(-?[a-z][a-z0-9-]*)\s*\)$/i.exec(term);
|
|
1975
|
+
if (flag) {
|
|
1976
|
+
// a feature in the boolean context, `(hover)`: true where its value
|
|
1977
|
+
// is anything but zero or none
|
|
1978
|
+
const key = flag[1].toLowerCase();
|
|
1979
|
+
if (
|
|
1980
|
+
!BOOLEAN_TRUE.has(key) &&
|
|
1981
|
+
!RESOLUTION_FEATURES.has(key) &&
|
|
1982
|
+
desktopFeature(key, null) !== true
|
|
1983
|
+
) {
|
|
1984
|
+
pass = false;
|
|
1808
1985
|
}
|
|
1809
1986
|
continue;
|
|
1810
1987
|
}
|
|
@@ -1820,11 +1997,23 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
1820
1997
|
// `not` over a scheme is the other scheme. `not` over a width range
|
|
1821
1998
|
// is not expressible as one range; the honest reduction is to decide
|
|
1822
1999
|
// it statically rather than invert it wrongly.
|
|
1823
|
-
if (
|
|
2000
|
+
if (
|
|
2001
|
+
!sawWidth &&
|
|
2002
|
+
!sawHeight &&
|
|
2003
|
+
!sawResolution &&
|
|
2004
|
+
!sawAspect &&
|
|
2005
|
+
pass &&
|
|
2006
|
+
condition.scheme
|
|
2007
|
+
) {
|
|
1824
2008
|
out.push({ scheme: condition.scheme === 'dark' ? 'light' : 'dark' });
|
|
1825
2009
|
continue;
|
|
1826
2010
|
}
|
|
1827
|
-
out.push({
|
|
2011
|
+
out.push({
|
|
2012
|
+
staticPass:
|
|
2013
|
+
!sawWidth && !sawHeight && !sawResolution && !sawAspect && pass
|
|
2014
|
+
? false
|
|
2015
|
+
: !pass,
|
|
2016
|
+
});
|
|
1828
2017
|
continue;
|
|
1829
2018
|
}
|
|
1830
2019
|
if (!pass) {
|
|
@@ -1833,6 +2022,9 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
1833
2022
|
}
|
|
1834
2023
|
if (
|
|
1835
2024
|
!sawWidth &&
|
|
2025
|
+
!sawHeight &&
|
|
2026
|
+
!sawResolution &&
|
|
2027
|
+
!sawAspect &&
|
|
1836
2028
|
condition.scheme === undefined &&
|
|
1837
2029
|
condition.min === undefined &&
|
|
1838
2030
|
condition.max === undefined
|
|
@@ -1845,6 +2037,221 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
1845
2037
|
return out.length ? out : [{ staticPass: true }];
|
|
1846
2038
|
}
|
|
1847
2039
|
|
|
2040
|
+
/**
|
|
2041
|
+
* The properties whose support a `@supports` condition is answered for:
|
|
2042
|
+
* the mask's, which pages test before they draw an icon as a masked colour
|
|
2043
|
+
* and keep a background image under `not` for an engine without them.
|
|
2044
|
+
* Drawn here, and the fallback drawn too, an icon was its image in black
|
|
2045
|
+
* under the mask, over the colour it was meant to be.
|
|
2046
|
+
*/
|
|
2047
|
+
const ANSWERED = new Set(
|
|
2048
|
+
[
|
|
2049
|
+
'mask',
|
|
2050
|
+
'mask-image',
|
|
2051
|
+
'mask-repeat',
|
|
2052
|
+
'mask-position',
|
|
2053
|
+
'mask-size',
|
|
2054
|
+
'mask-origin',
|
|
2055
|
+
'mask-clip',
|
|
2056
|
+
].flatMap((name) => [name, `-webkit-${name}`]),
|
|
2057
|
+
);
|
|
2058
|
+
|
|
2059
|
+
/**
|
|
2060
|
+
* Whether a `@supports` condition holds (CSS Conditional 3, 6): true or
|
|
2061
|
+
* false where it turns on declarations of the properties this answers for
|
|
2062
|
+
* (`ANSWERED`), combined by `not`, `and` and `or`; null where it turns on
|
|
2063
|
+
* anything else, or is none, and the block is entered as every one was.
|
|
2064
|
+
* Answering more is not safe without a list of what this draws: Tailwind 4
|
|
2065
|
+
* keeps its variables' starting values under a test for engines without
|
|
2066
|
+
* `@property`, which this is, and false there would drop every shadow and
|
|
2067
|
+
* gradient it writes.
|
|
2068
|
+
*/
|
|
2069
|
+
export function supportsCondition(prelude: string): boolean | null {
|
|
2070
|
+
const text = prelude.trim();
|
|
2071
|
+
if (/^not\b/i.test(text)) {
|
|
2072
|
+
const inner = supportsCondition(text.slice(3));
|
|
2073
|
+
return inner === null ? null : !inner;
|
|
2074
|
+
}
|
|
2075
|
+
// the terms at the top level, and the one operator between them
|
|
2076
|
+
const terms: string[] = [];
|
|
2077
|
+
let op = '';
|
|
2078
|
+
let depth = 0;
|
|
2079
|
+
let start = 0;
|
|
2080
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
2081
|
+
const c = text[i];
|
|
2082
|
+
if (c === '(') depth += 1;
|
|
2083
|
+
else if (c === ')') depth -= 1;
|
|
2084
|
+
else if (depth === 0 && /\s/.test(c)) {
|
|
2085
|
+
const word = /^\s+(and|or)\s+/i.exec(text.slice(i));
|
|
2086
|
+
if (!word) continue;
|
|
2087
|
+
const next = word[1].toLowerCase();
|
|
2088
|
+
// `and` and `or` mixed without parentheses is no condition
|
|
2089
|
+
if (op && op !== next) return null;
|
|
2090
|
+
op = next;
|
|
2091
|
+
terms.push(text.slice(start, i));
|
|
2092
|
+
i += word[0].length - 1;
|
|
2093
|
+
start = i + 1;
|
|
2094
|
+
}
|
|
2095
|
+
if (depth < 0) return null;
|
|
2096
|
+
}
|
|
2097
|
+
if (terms.length) {
|
|
2098
|
+
terms.push(text.slice(start));
|
|
2099
|
+
const values = terms.map(supportsCondition);
|
|
2100
|
+
if (op === 'and') {
|
|
2101
|
+
if (values.includes(false)) return false;
|
|
2102
|
+
return values.includes(null) ? null : true;
|
|
2103
|
+
}
|
|
2104
|
+
if (values.includes(true)) return true;
|
|
2105
|
+
return values.includes(null) ? null : false;
|
|
2106
|
+
}
|
|
2107
|
+
// one term: a condition in parentheses, or a declaration in them
|
|
2108
|
+
if (!(text.startsWith('(') && text.endsWith(')'))) return null;
|
|
2109
|
+
const inner = text.slice(1, -1).trim();
|
|
2110
|
+
if (inner.startsWith('(') || /^not\b/i.test(inner)) {
|
|
2111
|
+
return supportsCondition(inner);
|
|
2112
|
+
}
|
|
2113
|
+
const declaration = /^([a-z-]+)\s*:\s*(\S[\s\S]*)$/i.exec(inner);
|
|
2114
|
+
if (!declaration || !ANSWERED.has(declaration[1].toLowerCase())) return null;
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* A `(name: value)` media feature: the whole term, its name, and its value
|
|
2120
|
+
* read to the parenthesis that closes the feature, or null where the term is
|
|
2121
|
+
* no such thing. A value may hold parentheses of its own: MediaWiki's
|
|
2122
|
+
* breakpoints are `(max-width: calc(640px - 1px))`, and a value read to the
|
|
2123
|
+
* first `)` found no feature there — the term was passed over as though it
|
|
2124
|
+
* said nothing, and every narrow-screen rule held at every width.
|
|
2125
|
+
*/
|
|
2126
|
+
/** `aspect-ratio` and the device's, bare or `min-`/`max-`. */
|
|
2127
|
+
const ASPECT_FEATURES = /^(?:min-|max-)?(?:device-)?aspect-ratio$/;
|
|
2128
|
+
|
|
2129
|
+
/** How far past square a landscape viewport is: any width over the height. */
|
|
2130
|
+
const ASPECT_EDGE = 1e-9;
|
|
2131
|
+
|
|
2132
|
+
/** A ratio, `16/9` or `16 / 9` or `1.5`, as a number; null for anything
|
|
2133
|
+
* else, or a ratio of nothing. */
|
|
2134
|
+
function ratioOf(value: string): number | null {
|
|
2135
|
+
const m = /^\s*(\d*\.?\d+)\s*(?:\/\s*(\d*\.?\d+)\s*)?$/.exec(value);
|
|
2136
|
+
if (!m) return null;
|
|
2137
|
+
const ratio = Number(m[1]) / (m[2] === undefined ? 1 : Number(m[2]));
|
|
2138
|
+
return Number.isFinite(ratio) && ratio > 0 ? ratio : null;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/** The resolution features, the standard's and WebKit's, which Chrome
|
|
2142
|
+
* answers; `min--moz-device-pixel-ratio` and `-o-min-device-pixel-ratio`
|
|
2143
|
+
* are no feature of its, and none of this. */
|
|
2144
|
+
const RESOLUTION_FEATURES = new Set([
|
|
2145
|
+
'resolution',
|
|
2146
|
+
'min-resolution',
|
|
2147
|
+
'max-resolution',
|
|
2148
|
+
'-webkit-device-pixel-ratio',
|
|
2149
|
+
'-webkit-min-device-pixel-ratio',
|
|
2150
|
+
'-webkit-max-device-pixel-ratio',
|
|
2151
|
+
]);
|
|
2152
|
+
|
|
2153
|
+
/** A resolution in dots per CSS pixel: `2dppx`, `2x`, `192dpi`, `75.6dpcm`,
|
|
2154
|
+
* or WebKit's device pixel ratio, a number. Null for anything else. */
|
|
2155
|
+
function resolutionOf(value: string, ratio: boolean): number | null {
|
|
2156
|
+
const m = /^\s*(\d*\.?\d+(?:e[+-]?\d+)?)\s*(dppx|x|dpi|dpcm)?\s*$/i.exec(
|
|
2157
|
+
value,
|
|
2158
|
+
);
|
|
2159
|
+
if (!m) return null;
|
|
2160
|
+
const n = Number(m[1]);
|
|
2161
|
+
const unit = (m[2] ?? '').toLowerCase();
|
|
2162
|
+
if (ratio) return unit ? null : n;
|
|
2163
|
+
if (unit === 'dppx' || unit === 'x') return n;
|
|
2164
|
+
if (unit === 'dpi') return n / 96;
|
|
2165
|
+
if (unit === 'dpcm') return (n * 2.54) / 96;
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/** The features true in the boolean context whatever the viewport: a
|
|
2170
|
+
* size, a scheme, an orientation, there is always one. */
|
|
2171
|
+
const BOOLEAN_TRUE = new Set([
|
|
2172
|
+
'width',
|
|
2173
|
+
'height',
|
|
2174
|
+
'device-width',
|
|
2175
|
+
'device-height',
|
|
2176
|
+
'aspect-ratio',
|
|
2177
|
+
'orientation',
|
|
2178
|
+
'prefers-color-scheme',
|
|
2179
|
+
]);
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* A media feature as a desktop screen driven by a mouse answers it — the
|
|
2183
|
+
* device `<Html>` draws on — for the features that do not depend on the
|
|
2184
|
+
* viewport: true, false, or null for a feature nothing knows, which is
|
|
2185
|
+
* false (Media Queries 4, 3.2). `value` is null in the boolean context.
|
|
2186
|
+
* The colour is eight bits a component on no palette and no grid; scripts
|
|
2187
|
+
* never run here, so `scripting` is `none`.
|
|
2188
|
+
*/
|
|
2189
|
+
function desktopFeature(key: string, value: string | null): boolean | null {
|
|
2190
|
+
const v = value?.trim().toLowerCase() ?? null;
|
|
2191
|
+
const numeric = (feature: string, has: number): boolean | null => {
|
|
2192
|
+
const bare = key.replace(/^(min|max)-/, '');
|
|
2193
|
+
if (bare !== feature) return null;
|
|
2194
|
+
if (v === null) return key === feature ? has !== 0 : null;
|
|
2195
|
+
const n = Number(v);
|
|
2196
|
+
if (!Number.isFinite(n)) return false;
|
|
2197
|
+
if (key.startsWith('min-')) return has >= n;
|
|
2198
|
+
if (key.startsWith('max-')) return has <= n;
|
|
2199
|
+
return has === n;
|
|
2200
|
+
};
|
|
2201
|
+
const keyword = (answers: Record<string, boolean>, bool: boolean) =>
|
|
2202
|
+
v === null ? bool : (answers[v] ?? false);
|
|
2203
|
+
switch (key) {
|
|
2204
|
+
case 'hover':
|
|
2205
|
+
case 'any-hover':
|
|
2206
|
+
return keyword({ hover: true, none: false }, true);
|
|
2207
|
+
case 'pointer':
|
|
2208
|
+
case 'any-pointer':
|
|
2209
|
+
return keyword({ fine: true, coarse: false, none: false }, true);
|
|
2210
|
+
case 'update':
|
|
2211
|
+
return keyword({ fast: true, slow: false, none: false }, true);
|
|
2212
|
+
case 'overflow-block':
|
|
2213
|
+
case 'overflow-inline':
|
|
2214
|
+
return keyword({ scroll: true }, true);
|
|
2215
|
+
case 'scan':
|
|
2216
|
+
return false;
|
|
2217
|
+
case 'color-gamut':
|
|
2218
|
+
return keyword({ srgb: true }, true);
|
|
2219
|
+
case 'dynamic-range':
|
|
2220
|
+
case 'video-dynamic-range':
|
|
2221
|
+
return keyword({ standard: true }, true);
|
|
2222
|
+
case 'prefers-contrast':
|
|
2223
|
+
case 'prefers-reduced-transparency':
|
|
2224
|
+
case 'prefers-reduced-data':
|
|
2225
|
+
return keyword({ 'no-preference': true }, false);
|
|
2226
|
+
case 'forced-colors':
|
|
2227
|
+
return keyword({ none: true }, false);
|
|
2228
|
+
case 'scripting':
|
|
2229
|
+
return keyword({ none: true }, false);
|
|
2230
|
+
case 'display-mode':
|
|
2231
|
+
return keyword({ browser: true }, true);
|
|
2232
|
+
case '-webkit-transform-3d':
|
|
2233
|
+
return v === null || v === '1';
|
|
2234
|
+
}
|
|
2235
|
+
return (
|
|
2236
|
+
numeric('color', 8) ??
|
|
2237
|
+
numeric('monochrome', 0) ??
|
|
2238
|
+
numeric('color-index', 0) ??
|
|
2239
|
+
numeric('grid', 0)
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function mediaFeature(term: string): [string, string, string] | null {
|
|
2244
|
+
const m = /^\(\s*([a-z-]+)\s*:([\s\S]*)\)$/i.exec(term);
|
|
2245
|
+
if (!m) return null;
|
|
2246
|
+
let depth = 0;
|
|
2247
|
+
for (const c of m[2]) {
|
|
2248
|
+
if (c === '(') depth += 1;
|
|
2249
|
+
else if (c === ')' && --depth < 0) return null;
|
|
2250
|
+
}
|
|
2251
|
+
const value = m[2].trim();
|
|
2252
|
+
return depth === 0 && value ? [term, m[1], value] : null;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
1848
2255
|
/** A `@media` width is compared with the viewport in CSS pixels — the
|
|
1849
2256
|
* cascade divides the device width by the scale before asking — so the
|
|
1850
2257
|
* thresholds parse at scale 1 whatever panel the document lands on. */
|
|
@@ -1856,6 +2263,11 @@ export function mediaMatches(
|
|
|
1856
2263
|
media: MediaCondition[][] | null,
|
|
1857
2264
|
width: number,
|
|
1858
2265
|
scheme: 'light' | 'dark' = 'light',
|
|
2266
|
+
/** The viewport's height in CSS pixels; a test of it holds at no height
|
|
2267
|
+
* where none is given. */
|
|
2268
|
+
height = NaN,
|
|
2269
|
+
/** The display's scale, device pixels to the CSS pixel. */
|
|
2270
|
+
resolution = 1,
|
|
1859
2271
|
): boolean {
|
|
1860
2272
|
if (!media) return true;
|
|
1861
2273
|
for (const block of media) {
|
|
@@ -1868,6 +2280,12 @@ export function mediaMatches(
|
|
|
1868
2280
|
if (
|
|
1869
2281
|
(c.min === undefined || width >= c.min) &&
|
|
1870
2282
|
(c.max === undefined || width <= c.max) &&
|
|
2283
|
+
(c.minHeight === undefined || height >= c.minHeight) &&
|
|
2284
|
+
(c.maxHeight === undefined || height <= c.maxHeight) &&
|
|
2285
|
+
(c.minResolution === undefined || resolution >= c.minResolution) &&
|
|
2286
|
+
(c.minAspect === undefined || width >= c.minAspect * height) &&
|
|
2287
|
+
(c.maxAspect === undefined || width <= c.maxAspect * height) &&
|
|
2288
|
+
(c.maxResolution === undefined || resolution <= c.maxResolution) &&
|
|
1871
2289
|
(c.scheme === undefined || c.scheme === scheme)
|
|
1872
2290
|
) {
|
|
1873
2291
|
any = true;
|