Skip to content

Commit 2fb4f23

Browse files
authored
feat(core): css-what parser for CSS selectors + support for :not(), :is(), and :where() Level 4 and ~ (#10514)
1 parent 88a0472 commit 2fb4f23

File tree

10 files changed

+723
-422
lines changed

10 files changed

+723
-422
lines changed

apps/automated/src/ui/styling/style-tests.ts

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,111 @@ export function test_id_selector() {
278278
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
279279
}
280280

281+
export function test_not_pseudo_class_selector() {
282+
let page = helper.getClearCurrentPage();
283+
page.style.color = unsetValue;
284+
let btnWithId: Button;
285+
let btnWithNoId: Button;
286+
287+
// >> article-using-not-pseudo-class-selector
288+
page.css = 'Button:not(#myButton) { color: red; }';
289+
290+
//// Will be styled
291+
btnWithNoId = new Button();
292+
// << article-using-not-pseudo-class-selector
293+
294+
//// Won't be styled
295+
btnWithId = new Button();
296+
btnWithId.id = 'myButton';
297+
298+
const stack = new StackLayout();
299+
page.content = stack;
300+
stack.addChild(btnWithNoId);
301+
stack.addChild(btnWithId);
302+
303+
helper.assertViewColor(btnWithNoId, '#FF0000');
304+
TKUnit.assert(btnWithId.style.color === undefined, 'Color should not have a value');
305+
}
306+
307+
export function test_is_pseudo_class_selector() {
308+
let page = helper.getClearCurrentPage();
309+
page.style.color = unsetValue;
310+
let btnWithId: Button;
311+
let btnWithNoId: Button;
312+
313+
// >> article-using-is-pseudo-class-selector
314+
page.css = 'Button:is(#myButton) { color: red; }';
315+
316+
//// Will be styled
317+
btnWithId = new Button();
318+
btnWithId.id = 'myButton';
319+
320+
//// Won't be styled
321+
btnWithNoId = new Button();
322+
// << article-using-is-pseudo-class-selector
323+
324+
const stack = new StackLayout();
325+
page.content = stack;
326+
stack.addChild(btnWithId);
327+
stack.addChild(btnWithNoId);
328+
329+
helper.assertViewColor(btnWithId, '#FF0000');
330+
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
331+
}
332+
333+
export function test_where_pseudo_class_selector() {
334+
let page = helper.getClearCurrentPage();
335+
page.style.color = unsetValue;
336+
let btnWithId: Button;
337+
let btnWithNoId: Button;
338+
339+
// >> article-using-where-pseudo-class-selector
340+
page.css = 'Button:where(#myButton) { color: red; }';
341+
342+
//// Will be styled
343+
btnWithId = new Button();
344+
btnWithId.id = 'myButton';
345+
346+
//// Won't be styled
347+
btnWithNoId = new Button();
348+
// << article-using-where-pseudo-class-selector
349+
350+
const stack = new StackLayout();
351+
page.content = stack;
352+
stack.addChild(btnWithId);
353+
stack.addChild(btnWithNoId);
354+
355+
helper.assertViewColor(btnWithId, '#FF0000');
356+
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
357+
}
358+
359+
export function test_where_pseudo_class_selector_zero_specificity() {
360+
let page = helper.getClearCurrentPage();
361+
page.style.color = unsetValue;
362+
let btnWithId: Button;
363+
let btnWithNoId: Button;
364+
365+
// >> article-using-where-pseudo-class-selector-zero-specificity
366+
page.css = '#myButton { color: green; } Button:where(#myButton) { color: red; }';
367+
368+
//// Will be styled
369+
btnWithId = new Button();
370+
btnWithId.id = 'myButton';
371+
372+
//// Won't be styled
373+
btnWithNoId = new Button();
374+
// << article-using-where-pseudo-class-selector-zero-specificity
375+
376+
const stack = new StackLayout();
377+
page.content = stack;
378+
stack.addChild(btnWithId);
379+
stack.addChild(btnWithNoId);
380+
381+
// Pseudo-class :where() has zero specificity, therefore we expect the first rule to be applied
382+
helper.assertViewColor(btnWithId, '#008000');
383+
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
384+
}
385+
281386
// State selector tests
282387
export function test_state_selector() {
283388
let page = helper.getClearCurrentPage();
@@ -763,7 +868,7 @@ export function test_set_invalid_CSS_values_dont_cause_crash() {
763868
(views: Array<View>) => {
764869
TKUnit.assertEqual(30, testButton.style.fontSize);
765870
},
766-
{ pageCss: invalidCSS }
871+
{ pageCss: invalidCSS },
767872
);
768873
}
769874

@@ -782,7 +887,7 @@ export function test_set_mixed_CSS_cases_works() {
782887
helper.assertViewBackgroundColor(testButton, '#FF0000');
783888
helper.assertViewColor(testButton, '#0000FF');
784889
},
785-
{ pageCss: casedCSS }
890+
{ pageCss: casedCSS },
786891
);
787892
}
788893

apps/ui/src/css/combinators-page.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@
3131
color: white;
3232
}
3333

34+
.general-sibling--type Button ~ Label {
35+
background-color: green;
36+
color: white;
37+
}
38+
39+
.general-sibling--class .test-child ~ .test-child-2 {
40+
background-color: yellow;
41+
}
42+
43+
.general-sibling--attribute Button[data="test-child"] ~ Button[data="test-child-2"] {
44+
background-color: blueviolet;
45+
color: white;
46+
}
47+
48+
.general-sibling--pseudo-selector Button.ref ~ Button:disabled {
49+
background-color: black;
50+
color: white;
51+
}
52+
3453
.sibling-test-label {
3554
text-align: center;
3655
}
56+
57+
.sibling-test-label {
58+
margin-top: 8;
59+
}

apps/ui/src/css/combinators-page.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,39 @@
6161
<Button isEnabled="false" text="But I am!"/>
6262
</StackLayout>
6363

64+
<StackLayout class="general-sibling--type">
65+
<Label text="General sibling test by type"/>
66+
<Label class="sibling-test-label" text="I'm not!"/>
67+
<Button text="I'm the ref"/>
68+
<Label class="sibling-test-label" text="I'm a general sibling!"/>
69+
<Label class="sibling-test-label" text="Me too!"/>
70+
</StackLayout>
71+
72+
<StackLayout class="general-sibling--class">
73+
<Label text="General sibling test by class"/>
74+
<Button class="test-child-2" text="I'm not!"/>
75+
<Button class="test-child" text="I'm the ref"/>
76+
<Button class="test-child-2" text="I'm a general sibling!"/>
77+
<Button class="test-child-2" text="Me too!"/>
78+
</StackLayout>
79+
80+
<StackLayout class="general-sibling--attribute">
81+
<Label text="General sibling test by attribute"/>
82+
<Button data="test-child-2" text="I'm not!"/>
83+
<Button data="test-child" text="I'm the ref"/>
84+
<Button data="test-child-2" text="I'm a general sibling!"/>
85+
<Button data="test-child-2" text="Me too!"/>
86+
</StackLayout>
87+
88+
<StackLayout class="general-sibling--pseudo-selector">
89+
<Label text="General sibling test by pseudo-selector"/>
90+
<Button text="I'm not!"/>
91+
<Button isEnabled="false" text="I'm not either!"/>
92+
<Button class="ref" text="I'm the ref"/>
93+
<Button isEnabled="false" text="I'm a general sibling!"/>
94+
<Button isEnabled="false" text="Me too!"/>
95+
</StackLayout>
96+
6497
</StackLayout>
6598
</ScrollView>
6699
</Page>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"copyfiles": "^2.4.0",
4343
"css": "^3.0.0",
4444
"css-tree": "^1.1.2",
45+
"css-what": "^6.1.0",
4546
"dotenv": "~16.4.0",
4647
"emoji-regex": "^10.3.0",
4748
"eslint": "~8.57.0",
@@ -76,4 +77,3 @@
7677
]
7778
}
7879
}
79-

packages/core/css/parser.spec.ts

Lines changed: 2 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Color } from '../color';
2-
import { parseURL, parseColor, parsePercentageOrLength, parseBackgroundPosition, parseBackground, parseSelector, AttributeSelectorTest } from './parser';
2+
import { parseURL, parseColor, parsePercentageOrLength, parseBackgroundPosition, parseBackground } from './parser';
33
import { CSS3Parser, TokenObjectType } from './CSS3Parser';
44
import { CSSNativeScript } from './CSSNativeScript';
55

@@ -155,121 +155,6 @@ describe('css', () => {
155155
});
156156
});
157157

158-
describe('selectors', () => {
159-
test(parseSelector, ` listview#products.mark gridlayout:selected[row="2"] a> b > c >d>e *[src] `, {
160-
start: 0,
161-
end: 79,
162-
value: [
163-
[
164-
[
165-
{ type: '', identifier: 'listview' },
166-
{ type: '#', identifier: 'products' },
167-
{ type: '.', identifier: 'mark' },
168-
],
169-
' ',
170-
],
171-
[
172-
[
173-
{ type: '', identifier: 'gridlayout' },
174-
{ type: ':', identifier: 'selected' },
175-
{ type: '[]', property: 'row', test: '=', value: '2' },
176-
],
177-
' ',
178-
],
179-
[[{ type: '', identifier: 'a' }], '>'],
180-
[[{ type: '', identifier: 'b' }], '>'],
181-
[[{ type: '', identifier: 'c' }], '>'],
182-
[[{ type: '', identifier: 'd' }], '>'],
183-
[[{ type: '', identifier: 'e' }], ' '],
184-
[[{ type: '*' }, { type: '[]', property: 'src' }], undefined],
185-
],
186-
});
187-
test(parseSelector, '*', { start: 0, end: 1, value: [[[{ type: '*' }], undefined]] });
188-
test(parseSelector, 'button', { start: 0, end: 6, value: [[[{ type: '', identifier: 'button' }], undefined]] });
189-
test(parseSelector, '.login', { start: 0, end: 6, value: [[[{ type: '.', identifier: 'login' }], undefined]] });
190-
test(parseSelector, '#login', { start: 0, end: 6, value: [[[{ type: '#', identifier: 'login' }], undefined]] });
191-
test(parseSelector, ':hover', { start: 0, end: 6, value: [[[{ type: ':', identifier: 'hover' }], undefined]] });
192-
test(parseSelector, '[src]', { start: 0, end: 5, value: [[[{ type: '[]', property: 'src' }], undefined]] });
193-
test(parseSelector, `[src = "res://"]`, { start: 0, end: 16, value: [[[{ type: '[]', property: 'src', test: '=', value: `res://` }], undefined]] });
194-
(<AttributeSelectorTest[]>['=', '^=', '$=', '*=', '=', '~=', '|=']).forEach((attributeTest) => {
195-
test(parseSelector, `[src ${attributeTest} "val"]`, { start: 0, end: 12 + attributeTest.length, value: [[[{ type: '[]', property: 'src', test: attributeTest, value: 'val' }], undefined]] });
196-
});
197-
test(parseSelector, 'listview > .image', {
198-
start: 0,
199-
end: 17,
200-
value: [
201-
[[{ type: '', identifier: 'listview' }], '>'],
202-
[[{ type: '.', identifier: 'image' }], undefined],
203-
],
204-
});
205-
test(parseSelector, 'listview .image', {
206-
start: 0,
207-
end: 16,
208-
value: [
209-
[[{ type: '', identifier: 'listview' }], ' '],
210-
[[{ type: '.', identifier: 'image' }], undefined],
211-
],
212-
});
213-
test(parseSelector, 'button:hover', {
214-
start: 0,
215-
end: 12,
216-
value: [
217-
[
218-
[
219-
{ type: '', identifier: 'button' },
220-
{ type: ':', identifier: 'hover' },
221-
],
222-
undefined,
223-
],
224-
],
225-
});
226-
test(parseSelector, 'listview>:selected image.product', {
227-
start: 0,
228-
end: 32,
229-
value: [
230-
[[{ type: '', identifier: 'listview' }], '>'],
231-
[[{ type: ':', identifier: 'selected' }], ' '],
232-
[
233-
[
234-
{ type: '', identifier: 'image' },
235-
{ type: '.', identifier: 'product' },
236-
],
237-
undefined,
238-
],
239-
],
240-
});
241-
test(parseSelector, 'button[testAttr]', {
242-
start: 0,
243-
end: 16,
244-
value: [
245-
[
246-
[
247-
{ type: '', identifier: 'button' },
248-
{ type: '[]', property: 'testAttr' },
249-
],
250-
undefined,
251-
],
252-
],
253-
});
254-
test(parseSelector, 'button#login[user][pass]:focused:hovered', {
255-
start: 0,
256-
end: 40,
257-
value: [
258-
[
259-
[
260-
{ type: '', identifier: 'button' },
261-
{ type: '#', identifier: 'login' },
262-
{ type: '[]', property: 'user' },
263-
{ type: '[]', property: 'pass' },
264-
{ type: ':', identifier: 'focused' },
265-
{ type: ':', identifier: 'hovered' },
266-
],
267-
undefined,
268-
],
269-
],
270-
});
271-
});
272-
273158
describe('css3', () => {
274159
let themeCoreLightIos: string;
275160
let whatIsNewIos: string;
@@ -468,7 +353,7 @@ describe('css', () => {
468353
const reworkAst = reworkCss.parse(themeCoreLightIos, { source: 'nativescript-theme-core/css/core.light.css' });
469354
fs.writeFileSync(
470355
outReworkFile,
471-
JSON.stringify(reworkAst, (k, v) => (k === 'position' ? undefined : v), ' ')
356+
JSON.stringify(reworkAst, (k, v) => (k === 'position' ? undefined : v), ' '),
472357
);
473358

474359
const nsParser = new CSS3Parser(themeCoreLightIos);

0 commit comments

Comments
 (0)