Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): New parser for CSS selectors #10514

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5552d65
feat(core): New parser for CSS selectors
CatchABus Apr 13, 2024
0f90fb9
fix: Invalid :not() nested selector parsing
CatchABus Apr 13, 2024
ef4a144
chore: Removed unneeded nullish coalescing operator
CatchABus Apr 13, 2024
2210ae6
fix: Pseudo-class :not() is not dynamic
CatchABus Apr 13, 2024
ed28c8e
feat: Added case-insensitivity support for attribute selectors
CatchABus Apr 13, 2024
2a9487e
fix: Added missing :not() selector change tracking and corrected dyna…
CatchABus Apr 13, 2024
d54e174
feat: Added support for ':is()' pseudo-class
CatchABus Apr 14, 2024
fdbad98
chore: Minor trace improvement
CatchABus Apr 14, 2024
2fca2f6
feat: Added support for pseudo-class selector list types
CatchABus Apr 14, 2024
0748746
feat: Proper functional pseudo-class specificity
CatchABus Apr 14, 2024
39e72da
fix: Added null-check for pseudo-class lookupSort calls
CatchABus Apr 14, 2024
12103ce
fix: Removed pseudo-class lookupSort override as it caused problems
CatchABus Apr 14, 2024
39acfca
feat: Proper dynamic state for functional pseudo-classes
CatchABus Apr 14, 2024
158751c
chore: Removed old css selector parser unused code
CatchABus Apr 14, 2024
3ba567c
chore: Base functional pseudo-class should be abstract
CatchABus Apr 14, 2024
a34b7de
chore: Removed unused method
CatchABus Apr 14, 2024
35b72cb
chore: Re-added the previously removed method
CatchABus Apr 14, 2024
55d2815
perf: Do not generate selector sequences for a single selectors
CatchABus Apr 15, 2024
aeb4fd3
feat: Added combinator support for pseudo-class selector list
CatchABus Apr 15, 2024
e36ce66
feat: Added support for CSS general sibling combination (~)
CatchABus Apr 16, 2024
4a7aa62
chore: Better CSS selector combinator readability
CatchABus Apr 16, 2024
dce0159
chore: Renaming selector arrays
CatchABus Apr 16, 2024
59b4c97
perf: Reduce the number of instances and arrays for complex selectors
CatchABus Apr 20, 2024
f965bfb
test: Added UI sample for general sibling selector
CatchABus Apr 20, 2024
15f4df6
test: Added unit tests for new pseudo-classes
CatchABus Apr 20, 2024
38f0367
chore: Minor comment correction
CatchABus Apr 20, 2024
a3f20cf
fix: Dependency css-what was missing from core
CatchABus Apr 20, 2024
21413fa
perf: Faster lookup for :is() and :where() that contain a single sele…
CatchABus Apr 30, 2024
d5b8fb1
Merge branch 'main' into css-selector-parser-rework
NathanWalker May 2, 2024
97d3bc7
chore: Added missing dependency to root
CatchABus May 2, 2024
82df947
chore: Moving new dependency to root dev deps
CatchABus May 2, 2024
872a0da
test: Added automated tests for new functional pseudo-classes
CatchABus May 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
109 changes: 107 additions & 2 deletions apps/automated/src/ui/styling/style-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,111 @@ export function test_id_selector() {
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
}

export function test_not_pseudo_class_selector() {
let page = helper.getClearCurrentPage();
page.style.color = unsetValue;
let btnWithId: Button;
let btnWithNoId: Button;

// >> article-using-not-pseudo-class-selector
page.css = 'Button:not(#myButton) { color: red; }';

//// Will be styled
btnWithNoId = new Button();
// << article-using-not-pseudo-class-selector

//// Won't be styled
btnWithId = new Button();
btnWithId.id = 'myButton';

const stack = new StackLayout();
page.content = stack;
stack.addChild(btnWithNoId);
stack.addChild(btnWithId);

helper.assertViewColor(btnWithNoId, '#FF0000');
TKUnit.assert(btnWithId.style.color === undefined, 'Color should not have a value');
}

export function test_is_pseudo_class_selector() {
let page = helper.getClearCurrentPage();
page.style.color = unsetValue;
let btnWithId: Button;
let btnWithNoId: Button;

// >> article-using-is-pseudo-class-selector
page.css = 'Button:is(#myButton) { color: red; }';

//// Will be styled
btnWithId = new Button();
btnWithId.id = 'myButton';

//// Won't be styled
btnWithNoId = new Button();
// << article-using-is-pseudo-class-selector

const stack = new StackLayout();
page.content = stack;
stack.addChild(btnWithId);
stack.addChild(btnWithNoId);

helper.assertViewColor(btnWithId, '#FF0000');
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
}

export function test_where_pseudo_class_selector() {
let page = helper.getClearCurrentPage();
page.style.color = unsetValue;
let btnWithId: Button;
let btnWithNoId: Button;

// >> article-using-where-pseudo-class-selector
page.css = 'Button:where(#myButton) { color: red; }';

//// Will be styled
btnWithId = new Button();
btnWithId.id = 'myButton';

//// Won't be styled
btnWithNoId = new Button();
// << article-using-where-pseudo-class-selector

const stack = new StackLayout();
page.content = stack;
stack.addChild(btnWithId);
stack.addChild(btnWithNoId);

helper.assertViewColor(btnWithId, '#FF0000');
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
}

export function test_where_pseudo_class_selector_zero_specificity() {
let page = helper.getClearCurrentPage();
page.style.color = unsetValue;
let btnWithId: Button;
let btnWithNoId: Button;

// >> article-using-where-pseudo-class-selector-zero-specificity
page.css = '#myButton { color: green; } Button:where(#myButton) { color: red; }';

//// Will be styled
btnWithId = new Button();
btnWithId.id = 'myButton';

//// Won't be styled
btnWithNoId = new Button();
// << article-using-where-pseudo-class-selector-zero-specificity

const stack = new StackLayout();
page.content = stack;
stack.addChild(btnWithId);
stack.addChild(btnWithNoId);

// Pseudo-class :where() has zero specificity, therefore we expect the first rule to be applied
helper.assertViewColor(btnWithId, '#008000');
TKUnit.assert(btnWithNoId.style.color === undefined, 'Color should not have a value');
}

// State selector tests
export function test_state_selector() {
let page = helper.getClearCurrentPage();
Expand Down Expand Up @@ -763,7 +868,7 @@ export function test_set_invalid_CSS_values_dont_cause_crash() {
(views: Array<View>) => {
TKUnit.assertEqual(30, testButton.style.fontSize);
},
{ pageCss: invalidCSS }
{ pageCss: invalidCSS },
);
}

Expand All @@ -782,7 +887,7 @@ export function test_set_mixed_CSS_cases_works() {
helper.assertViewBackgroundColor(testButton, '#FF0000');
helper.assertViewColor(testButton, '#0000FF');
},
{ pageCss: casedCSS }
{ pageCss: casedCSS },
);
}

Expand Down
23 changes: 23 additions & 0 deletions apps/ui/src/css/combinators-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@
color: white;
}

.general-sibling--type Button ~ Label {
background-color: green;
color: white;
}

.general-sibling--class .test-child ~ .test-child-2 {
background-color: yellow;
}

.general-sibling--attribute Button[data="test-child"] ~ Button[data="test-child-2"] {
background-color: blueviolet;
color: white;
}

.general-sibling--pseudo-selector Button.ref ~ Button:disabled {
background-color: black;
color: white;
}

.sibling-test-label {
text-align: center;
}

.sibling-test-label {
margin-top: 8;
}
33 changes: 33 additions & 0 deletions apps/ui/src/css/combinators-page.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,39 @@
<Button isEnabled="false" text="But I am!"/>
</StackLayout>

<StackLayout class="general-sibling--type">
<Label text="General sibling test by type"/>
<Label class="sibling-test-label" text="I'm not!"/>
<Button text="I'm the ref"/>
<Label class="sibling-test-label" text="I'm a general sibling!"/>
<Label class="sibling-test-label" text="Me too!"/>
</StackLayout>

<StackLayout class="general-sibling--class">
<Label text="General sibling test by class"/>
<Button class="test-child-2" text="I'm not!"/>
<Button class="test-child" text="I'm the ref"/>
<Button class="test-child-2" text="I'm a general sibling!"/>
<Button class="test-child-2" text="Me too!"/>
</StackLayout>

<StackLayout class="general-sibling--attribute">
<Label text="General sibling test by attribute"/>
<Button data="test-child-2" text="I'm not!"/>
<Button data="test-child" text="I'm the ref"/>
<Button data="test-child-2" text="I'm a general sibling!"/>
<Button data="test-child-2" text="Me too!"/>
</StackLayout>

<StackLayout class="general-sibling--pseudo-selector">
<Label text="General sibling test by pseudo-selector"/>
<Button text="I'm not!"/>
<Button isEnabled="false" text="I'm not either!"/>
<Button class="ref" text="I'm the ref"/>
<Button isEnabled="false" text="I'm a general sibling!"/>
<Button isEnabled="false" text="Me too!"/>
</StackLayout>

</StackLayout>
</ScrollView>
</Page>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"copyfiles": "^2.4.0",
"css": "^3.0.0",
"css-tree": "^1.1.2",
"css-what": "^6.1.0",
"dotenv": "~16.4.0",
"emoji-regex": "^10.3.0",
"eslint": "~8.57.0",
Expand Down Expand Up @@ -89,4 +90,3 @@
]
}
}

119 changes: 2 additions & 117 deletions packages/core/css/parser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Color } from '../color';
import { parseURL, parseColor, parsePercentageOrLength, parseBackgroundPosition, parseBackground, parseSelector, AttributeSelectorTest } from './parser';
import { parseURL, parseColor, parsePercentageOrLength, parseBackgroundPosition, parseBackground } from './parser';
import { CSS3Parser, TokenObjectType } from './CSS3Parser';
import { CSSNativeScript } from './CSSNativeScript';

Expand Down Expand Up @@ -155,121 +155,6 @@ describe('css', () => {
});
});

describe('selectors', () => {
test(parseSelector, ` listview#products.mark gridlayout:selected[row="2"] a> b > c >d>e *[src] `, {
start: 0,
end: 79,
value: [
[
[
{ type: '', identifier: 'listview' },
{ type: '#', identifier: 'products' },
{ type: '.', identifier: 'mark' },
],
' ',
],
[
[
{ type: '', identifier: 'gridlayout' },
{ type: ':', identifier: 'selected' },
{ type: '[]', property: 'row', test: '=', value: '2' },
],
' ',
],
[[{ type: '', identifier: 'a' }], '>'],
[[{ type: '', identifier: 'b' }], '>'],
[[{ type: '', identifier: 'c' }], '>'],
[[{ type: '', identifier: 'd' }], '>'],
[[{ type: '', identifier: 'e' }], ' '],
[[{ type: '*' }, { type: '[]', property: 'src' }], undefined],
],
});
test(parseSelector, '*', { start: 0, end: 1, value: [[[{ type: '*' }], undefined]] });
test(parseSelector, 'button', { start: 0, end: 6, value: [[[{ type: '', identifier: 'button' }], undefined]] });
test(parseSelector, '.login', { start: 0, end: 6, value: [[[{ type: '.', identifier: 'login' }], undefined]] });
test(parseSelector, '#login', { start: 0, end: 6, value: [[[{ type: '#', identifier: 'login' }], undefined]] });
test(parseSelector, ':hover', { start: 0, end: 6, value: [[[{ type: ':', identifier: 'hover' }], undefined]] });
test(parseSelector, '[src]', { start: 0, end: 5, value: [[[{ type: '[]', property: 'src' }], undefined]] });
test(parseSelector, `[src = "res://"]`, { start: 0, end: 16, value: [[[{ type: '[]', property: 'src', test: '=', value: `res://` }], undefined]] });
(<AttributeSelectorTest[]>['=', '^=', '$=', '*=', '=', '~=', '|=']).forEach((attributeTest) => {
test(parseSelector, `[src ${attributeTest} "val"]`, { start: 0, end: 12 + attributeTest.length, value: [[[{ type: '[]', property: 'src', test: attributeTest, value: 'val' }], undefined]] });
});
test(parseSelector, 'listview > .image', {
start: 0,
end: 17,
value: [
[[{ type: '', identifier: 'listview' }], '>'],
[[{ type: '.', identifier: 'image' }], undefined],
],
});
test(parseSelector, 'listview .image', {
start: 0,
end: 16,
value: [
[[{ type: '', identifier: 'listview' }], ' '],
[[{ type: '.', identifier: 'image' }], undefined],
],
});
test(parseSelector, 'button:hover', {
start: 0,
end: 12,
value: [
[
[
{ type: '', identifier: 'button' },
{ type: ':', identifier: 'hover' },
],
undefined,
],
],
});
test(parseSelector, 'listview>:selected image.product', {
start: 0,
end: 32,
value: [
[[{ type: '', identifier: 'listview' }], '>'],
[[{ type: ':', identifier: 'selected' }], ' '],
[
[
{ type: '', identifier: 'image' },
{ type: '.', identifier: 'product' },
],
undefined,
],
],
});
test(parseSelector, 'button[testAttr]', {
start: 0,
end: 16,
value: [
[
[
{ type: '', identifier: 'button' },
{ type: '[]', property: 'testAttr' },
],
undefined,
],
],
});
test(parseSelector, 'button#login[user][pass]:focused:hovered', {
start: 0,
end: 40,
value: [
[
[
{ type: '', identifier: 'button' },
{ type: '#', identifier: 'login' },
{ type: '[]', property: 'user' },
{ type: '[]', property: 'pass' },
{ type: ':', identifier: 'focused' },
{ type: ':', identifier: 'hovered' },
],
undefined,
],
],
});
});

describe('css3', () => {
let themeCoreLightIos: string;
let whatIsNewIos: string;
Expand Down Expand Up @@ -468,7 +353,7 @@ describe('css', () => {
const reworkAst = reworkCss.parse(themeCoreLightIos, { source: 'nativescript-theme-core/css/core.light.css' });
fs.writeFileSync(
outReworkFile,
JSON.stringify(reworkAst, (k, v) => (k === 'position' ? undefined : v), ' ')
JSON.stringify(reworkAst, (k, v) => (k === 'position' ? undefined : v), ' '),
);

const nsParser = new CSS3Parser(themeCoreLightIos);
Expand Down