@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.
- 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 +26 -7
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +28 -3
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +19 -6
- 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 +11 -0
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +114 -4
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +38 -1
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +326 -8
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +14 -0
- 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 +35 -2
- package/dist/html/dom.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +38 -1
- 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 +181 -18
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +34 -6
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +35 -14
- 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/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 +1222 -115
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +33 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +512 -51
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +53 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +546 -180
- 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/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 +5 -3
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +50 -9
- package/src/html/css/cascade.ts +19 -6
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +107 -4
- package/src/html/css/style.ts +343 -8
- package/src/html/css/ua.ts +14 -0
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +38 -2
- package/src/html/index.ts +39 -1
- package/src/html/layout/block.ts +228 -16
- package/src/html/layout/boxes.ts +55 -21
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1435 -108
- package/src/html/node.ts +535 -54
- package/src/html/paint.ts +650 -241
- package/src/html/surfaces.ts +21 -1
- package/src/internal/window.ts +36 -8
- package/src/richtext/node.ts +7 -0
package/src/html/controls.ts
CHANGED
|
@@ -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 —
|
|
52
|
-
* or a background (`styledField`). The document draws
|
|
53
|
-
* widget goes bare inside its content box, here, with
|
|
54
|
-
* of its own and its text in the element's colour and
|
|
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
|
|
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
|
|
102
|
-
*
|
|
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')
|
|
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.
|
package/src/html/css/cascade.ts
CHANGED
|
@@ -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]
|
|
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(?:
|
|
1531
|
-
const VIEWPORT_UNITS = /\d(
|
|
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
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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. */
|