From 5552d65beacd8235f99f9d9b51e51749ecc890d0 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 13 Apr 2024 20:06:07 +0000 Subject: [PATCH 01/31] feat(core): New parser for CSS selectors --- package.json | 2 +- packages/core/ui/styling/css-selector.ts | 263 ++++++++++++------- packages/core/ui/styling/style-properties.ts | 6 +- 3 files changed, 178 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index a6e90c6d7e..a578f5c980 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "url": "https://github.com/NativeScript/NativeScript.git" }, "dependencies": { + "css-what": "^6.1.0", "nativescript-theme-core": "^1.0.4" }, "devDependencies": { @@ -89,4 +90,3 @@ ] } } - diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index bccd3971dc..97fc2188f2 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -1,6 +1,8 @@ +import * as CSSWhat from 'css-what'; import '../../globals'; import { isCssVariable } from '../core/properties'; -import { isNullOrUndefined } from '../../utils/types'; +import { Trace } from '../../trace'; +import { isNullOrUndefined, isUndefined } from '../../utils/types'; import * as ReworkCSS from '../../css'; import { Combinator as ICombinator, SimpleSelectorSequence as ISimpleSelectorSequence, Selector as ISelector, SimpleSelector as ISimpleSelector, parseSelector } from '../../css/parser'; @@ -34,6 +36,7 @@ export interface Changes { pseudoClasses?: Set; } +/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ const enum Specificity { Inline = 1000, Id = 100, @@ -55,6 +58,30 @@ const enum Rarity { Universal = 0, Inline = 0, } +/* eslint-enable @typescript-eslint/no-duplicate-enum-values */ + +enum Combinator { + 'descendant' = ' ', + 'child' = '>', + 'adjacent' = '+', + + // Not supported + 'parent' = '<', + 'sibling' = '~', + 'column-combinator' = '||', +} + +enum AttributeSelectorOperator { + exists = '', + equals = '=', + start = '^=', + end = '$=', + any = '*=', + element = '~=', + hyphen = '|=', +} + +declare type AttributeTest = 'exists' | 'equals' | 'start' | 'end' | 'any' | 'element' | 'hyphen'; interface LookupSorter { sortById(id: string, sel: SelectorCore); @@ -74,6 +101,8 @@ namespace Match { export const Static = false; } +const SUPPORTED_COMBINATORS: Array = [Combinator.descendant, Combinator.child, Combinator.adjacent]; + function getNodeDirectSibling(node): null | Node { if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt) { return null; @@ -97,7 +126,6 @@ function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic = }; } -declare type Combinator = '+' | '>' | '~' | ' '; @SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static) export abstract class SelectorCore { public pos: number; @@ -221,63 +249,62 @@ export class ClassSelector extends SimpleSelector { } } -declare type AttributeTest = '=' | '^=' | '$=' | '*=' | '=' | '~=' | '|='; @SelectorProperties(Specificity.Attribute, Rarity.Attribute, Match.Dynamic) export class AttributeSelector extends SimpleSelector { - constructor(public attribute: string, public test?: AttributeTest, public value?: string) { + constructor( + public attribute: string, + public test: AttributeTest, + public value?: string, + ) { super(); + } + public toString(): string { + return `[${this.attribute}${wrap(AttributeSelectorOperator[this.test] ?? this.test)}${this.value || ''}]${wrap(this.combinator)}`; + } + public match(node: Node): boolean { + let attr = node[this.attribute]; - if (!test) { - // HasAttribute - this.match = (node) => !isNullOrUndefined(node[attribute]); - - return; + if (this.test === 'exists') { + return !isNullOrUndefined(attr); } - if (!value) { - this.match = (node) => false; + if (!this.value) { + return false; } - this.match = (node) => { - const attr = node[attribute] + ''; + // Now, convert value to string + attr += ''; - if (test === '=') { - // Equals - return attr === value; - } - - if (test === '^=') { - // PrefixMatch - return attr.startsWith(value); - } + // = + if (this.test === 'equals') { + return attr === this.value; + } - if (test === '$=') { - // SuffixMatch - return attr.endsWith(value); - } + // ^= + if (this.test === 'start') { + return attr.startsWith(this.value); + } - if (test === '*=') { - // SubstringMatch - return attr.indexOf(value) !== -1; - } + // $= + if (this.test === 'end') { + return attr.endsWith(this.value); + } - if (test === '~=') { - // Includes - const words = attr.split(' '); + // *= + if (this.test === 'any') { + return attr.indexOf(this.value) !== -1; + } - return words && words.indexOf(value) !== -1; - } + // ~= + if (this.test === 'element') { + const words = attr.split(' '); + return words && words.indexOf(this.value) !== -1; + } - if (test === '|=') { - // DashMatch - return attr === value || attr.startsWith(value + '-'); - } - }; - } - public toString(): string { - return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ''}]${wrap(this.combinator)}`; - } - public match(node: Node): boolean { + // |= + if (this.test === 'hyphen') { + return attr === this.value || attr.startsWith(this.value + '-'); + } return false; } public mayMatch(node: Node): boolean { @@ -307,6 +334,42 @@ export class PseudoClassSelector extends SimpleSelector { } } +@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic) +export class NotPseudoClassSelector extends SimpleSelector { + private selectors: SimpleSelector[]; + + constructor(dataType: CSSWhat.DataType) { + super(); + + const selectors: SimpleSelector[] = []; + + if (Array.isArray(dataType)) { + for (const asts of dataType) { + for (const ast of asts) { + const combinator = Combinator[ast.type]; + if (combinator != null) { + Trace.write(`Invalid combinator ${combinator ?? ast.type} inside :not() pseudo-class!`, Trace.categories.Style, Trace.messageType.warn); + break; + } + + selectors.push(createSimpleSelectorFromAst(ast)); + } + } + } + + this.selectors = selectors; + } + public toString(): string { + return `:not(${this.selectors.join(', ')})${wrap(this.combinator)}`; + } + public match(node: Node): boolean { + return !this.selectors.some((selector) => selector.match(node)); + } + public mayMatch(node: Node): boolean { + return true; + } +} + export class SimpleSelectorSequence extends SimpleSelector { private head: SimpleSelector; constructor(public selectors: SimpleSelector[]) { @@ -339,7 +402,6 @@ export class Selector extends SelectorCore { constructor(public selectors: SimpleSelector[]) { super(); - const supportedCombinator = [undefined, ' ', '>', '+']; let siblingGroup: SimpleSelector[]; let lastGroup: SimpleSelector[][]; const groups: SimpleSelector[][][] = []; @@ -349,11 +411,12 @@ export class Selector extends SelectorCore { for (let i = selectors.length - 1; i > -1; i--) { const sel = selectors[i]; + const isCombinatorSet = !isUndefined(sel.combinator); - if (supportedCombinator.indexOf(sel.combinator) === -1) { - throw new Error(`Unsupported combinator "${sel.combinator}".`); + if (isCombinatorSet && !SUPPORTED_COMBINATORS.includes(sel.combinator)) { + throw new Error(`Unsupported combinator "${sel.combinator}" for selector ${sel}.`); } - if (sel.combinator === undefined || sel.combinator === ' ') { + if (!isCombinatorSet || sel.combinator === ' ') { groups.push((lastGroup = [(siblingGroup = [])])); } if (sel.combinator === '>') { @@ -504,7 +567,10 @@ export namespace Selector { export class RuleSet { tag: string | number; scopedTag: string; - constructor(public selectors: SelectorCore[], public declarations: Declaration[]) { + constructor( + public selectors: SelectorCore[], + public declarations: Declaration[], + ) { this.selectors.forEach((sel) => (sel.ruleset = this)); } public toString(): string { @@ -528,72 +594,91 @@ function createDeclaration(decl: ReworkCSS.Declaration): any { return { property: isCssVariable(decl.property) ? decl.property : decl.property.toLowerCase(), value: decl.value }; } -function createSimpleSelectorFromAst(ast: ISimpleSelector): SimpleSelector { - if (ast.type === '.') { - return new ClassSelector(ast.identifier); - } +function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { + if (ast.type === 'attribute') { + if (ast.name === 'class') { + return new ClassSelector(ast.value); + } - if (ast.type === '') { - return new TypeSelector(ast.identifier.replace('-', '').toLowerCase()); - } + if (ast.name === 'id') { + return new IdSelector(ast.value); + } - if (ast.type === '#') { - return new IdSelector(ast.identifier); + return new AttributeSelector(ast.name, ast.action, ast.value); } - if (ast.type === '[]') { - return new AttributeSelector(ast.property, ast.test, ast.test && ast.value); + if (ast.type === 'tag') { + return new TypeSelector(ast.name.replace('-', '').toLowerCase()); } - if (ast.type === ':') { - return new PseudoClassSelector(ast.identifier); + if (ast.type === 'pseudo') { + if (ast.name === 'not') { + return new NotPseudoClassSelector(ast.data); + } + return new PseudoClassSelector(ast.name); } - if (ast.type === '*') { + if (ast.type === 'universal') { return new UniversalSelector(); } } -function createSimpleSelectorSequenceFromAst(ast: ISimpleSelectorSequence): SimpleSelectorSequence | SimpleSelector { - if (ast.length === 0) { - return new InvalidSelector(new Error('Empty simple selector sequence.')); - } else if (ast.length === 1) { - return createSimpleSelectorFromAst(ast[0]); - } else { - return new SimpleSelectorSequence(ast.map(createSimpleSelectorFromAst)); - } +function createSimpleSelectorSequence(selectors: Array): SimpleSelectorSequence { + return new SimpleSelectorSequence(selectors.map(createSimpleSelectorFromAst)); } -function createSelectorFromAst(ast: ISelector): SimpleSelector | SimpleSelectorSequence | Selector { - if (ast.length === 0) { - return new InvalidSelector(new Error('Empty selector.')); - } else if (ast.length === 1) { - return createSimpleSelectorSequenceFromAst(ast[0][0]); +function createSelectorFromAst(asts: CSSWhat.Selector[]): SimpleSelector | SimpleSelectorSequence | Selector { + let result: SimpleSelector | SimpleSelectorSequence | Selector; + + if (asts.length === 0) { + result = new InvalidSelector(new Error('Empty selector.')); + } else if (asts.length === 1) { + result = createSimpleSelectorFromAst(asts[0]); } else { - const simpleSelectorSequences = []; - let simpleSelectorSequence: SimpleSelectorSequence | SimpleSelector; - let combinator: ICombinator; - for (let i = 0; i < ast.length; i++) { - simpleSelectorSequence = createSimpleSelectorSequenceFromAst(ast[i][0]); - combinator = ast[i][1]; - if (combinator) { + const simpleSelectorSequences: Array = []; + + let pendingSelectorInstances: Array = []; + let combinatorCount: number = 0; + + for (const ast of asts) { + const combinator = Combinator[ast.type]; + + // Combinator means the end of a sequence + if (combinator != null) { + const simpleSelectorSequence = createSimpleSelectorSequence(pendingSelectorInstances); simpleSelectorSequence.combinator = combinator; + simpleSelectorSequences.push(simpleSelectorSequence); + + combinatorCount++; + // Cleanup stored selectors for the new sequence to take place + pendingSelectorInstances = []; + } else { + pendingSelectorInstances.push(ast); } - simpleSelectorSequences.push(simpleSelectorSequence); } - return new Selector(simpleSelectorSequences); + if (combinatorCount > 0) { + // Create a sequence using the remaining selectors after the last combinator + if (pendingSelectorInstances.length) { + simpleSelectorSequences.push(createSimpleSelectorSequence(pendingSelectorInstances)); + } + result = new Selector(simpleSelectorSequences); + } else { + result = createSimpleSelectorSequence(pendingSelectorInstances); + } } + + return result; } export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector { try { - const parsedSelector = parseSelector(sel); - if (!parsedSelector) { + const result = CSSWhat.parse(sel); + if (!result?.length) { return new InvalidSelector(new Error('Empty selector')); } - return createSelectorFromAst(parsedSelector.value); + return createSelectorFromAst(result[0]); } catch (e) { return new InvalidSelector(e); } diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index fbea95aa2a..4e1665101b 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -734,7 +734,7 @@ function normalizeTransformation({ property, value }: Transformation): Transform } function convertTransformValue(property: string, stringValue: string): TransformationValue { - /* eslint-disable prefer-const */ + // eslint-disable-next-line prefer-const let [x, y, z] = stringValue.split(',').map(parseFloat); if (property === 'translate') { y ??= IDENTITY_TRANSFORMATION.translate.y; @@ -1239,8 +1239,8 @@ const boxShadowProperty = new CssProperty({ blurRadius: Length.toDevicePixels(newValue.blurRadius, 0), spreadRadius: Length.toDevicePixels(newValue.spreadRadius, 0), color: newValue.color, - } - : null + } + : null, ); }, valueConverter: (value) => { From 0f90fb91ced859d006390628565e55c3173d500e Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 13 Apr 2024 20:37:30 +0000 Subject: [PATCH 02/31] fix: Invalid :not() nested selector parsing --- packages/core/ui/styling/css-selector.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 97fc2188f2..e8355ae7d5 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -336,15 +336,17 @@ export class PseudoClassSelector extends SimpleSelector { @SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic) export class NotPseudoClassSelector extends SimpleSelector { - private selectors: SimpleSelector[]; + private selectorGroups: SimpleSelector[][]; constructor(dataType: CSSWhat.DataType) { super(); - const selectors: SimpleSelector[] = []; + const selectorGroups: SimpleSelector[][] = []; if (Array.isArray(dataType)) { for (const asts of dataType) { + const selectors: SimpleSelector[] = []; + for (const ast of asts) { const combinator = Combinator[ast.type]; if (combinator != null) { @@ -354,16 +356,21 @@ export class NotPseudoClassSelector extends SimpleSelector { selectors.push(createSimpleSelectorFromAst(ast)); } + + if (selectors.length) { + selectorGroups.push(selectors); + } } } - this.selectors = selectors; + this.selectorGroups = selectorGroups; } public toString(): string { - return `:not(${this.selectors.join(', ')})${wrap(this.combinator)}`; + const selectors = this.selectorGroups.map((group) => group.join('')); + return `:not(${selectors.join(', ')})${wrap(this.combinator)}`; } public match(node: Node): boolean { - return !this.selectors.some((selector) => selector.match(node)); + return !this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); } public mayMatch(node: Node): boolean { return true; @@ -604,7 +611,7 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { return new IdSelector(ast.value); } - return new AttributeSelector(ast.name, ast.action, ast.value); + return new AttributeSelector(ast.name, ast.action, ast.value); } if (ast.type === 'tag') { From ef4a14438319543a3a40dbbdf531c4620b414df1 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 13 Apr 2024 20:41:24 +0000 Subject: [PATCH 03/31] chore: Removed unneeded nullish coalescing operator --- packages/core/ui/styling/css-selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index e8355ae7d5..ced8273403 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -350,7 +350,7 @@ export class NotPseudoClassSelector extends SimpleSelector { for (const ast of asts) { const combinator = Combinator[ast.type]; if (combinator != null) { - Trace.write(`Invalid combinator ${combinator ?? ast.type} inside :not() pseudo-class!`, Trace.categories.Style, Trace.messageType.warn); + Trace.write(`Invalid combinator ${combinator} inside :not() pseudo-class!`, Trace.categories.Style, Trace.messageType.warn); break; } From 2210ae61a794cf06ccbdec70fd583568c84c8319 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 13 Apr 2024 21:09:07 +0000 Subject: [PATCH 04/31] fix: Pseudo-class :not() is not dynamic --- packages/core/ui/styling/css-selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index ced8273403..b41e8f2c3a 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -334,7 +334,7 @@ export class PseudoClassSelector extends SimpleSelector { } } -@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic) +@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Static) export class NotPseudoClassSelector extends SimpleSelector { private selectorGroups: SimpleSelector[][]; From ed28c8e9f897008130f193b5e30538a51148a488 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 13 Apr 2024 21:42:29 +0000 Subject: [PATCH 05/31] feat: Added case-insensitivity support for attribute selectors --- packages/core/ui/styling/css-selector.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index b41e8f2c3a..1ca0ac7234 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -254,7 +254,8 @@ export class AttributeSelector extends SimpleSelector { constructor( public attribute: string, public test: AttributeTest, - public value?: string, + public value: string, + public ignoreCase: boolean, ) { super(); } @@ -275,6 +276,11 @@ export class AttributeSelector extends SimpleSelector { // Now, convert value to string attr += ''; + if (this.ignoreCase) { + attr = attr.toLowerCase(); + this.value = this.value.toLowerCase(); + } + // = if (this.test === 'equals') { return attr === this.value; @@ -611,7 +617,7 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { return new IdSelector(ast.value); } - return new AttributeSelector(ast.name, ast.action, ast.value); + return new AttributeSelector(ast.name, ast.action, ast.value, !!ast.ignoreCase); } if (ast.type === 'tag') { From 2a9487ef1521361c9cdeb027978ef7651846879f Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 13 Apr 2024 21:54:25 +0000 Subject: [PATCH 06/31] fix: Added missing :not() selector change tracking and corrected dynamic state --- packages/core/ui/styling/css-selector.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 1ca0ac7234..ac9bbe0dd7 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -340,7 +340,7 @@ export class PseudoClassSelector extends SimpleSelector { } } -@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Static) +@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic) export class NotPseudoClassSelector extends SimpleSelector { private selectorGroups: SimpleSelector[][]; @@ -381,6 +381,13 @@ export class NotPseudoClassSelector extends SimpleSelector { public mayMatch(node: Node): boolean { return true; } + public trackChanges(node, map): void { + for (const selectors of this.selectorGroups) { + for (const selector of selectors) { + selector.trackChanges(node, map); + } + } + } } export class SimpleSelectorSequence extends SimpleSelector { From d54e174254d7304c1eb622523c127dc5b121c41a Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 12:16:06 +0000 Subject: [PATCH 07/31] feat: Added support for ':is()' pseudo-class --- packages/core/ui/styling/css-selector.ts | 30 +++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index ac9bbe0dd7..1a39555ac1 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -341,11 +341,11 @@ export class PseudoClassSelector extends SimpleSelector { } @SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic) -export class NotPseudoClassSelector extends SimpleSelector { +export class FunctionalPseudoClassSelector extends PseudoClassSelector { private selectorGroups: SimpleSelector[][]; - constructor(dataType: CSSWhat.DataType) { - super(); + constructor(cssPseudoClass: string, dataType: CSSWhat.DataType) { + super(cssPseudoClass); const selectorGroups: SimpleSelector[][] = []; @@ -356,7 +356,7 @@ export class NotPseudoClassSelector extends SimpleSelector { for (const ast of asts) { const combinator = Combinator[ast.type]; if (combinator != null) { - Trace.write(`Invalid combinator ${combinator} inside :not() pseudo-class!`, Trace.categories.Style, Trace.messageType.warn); + Trace.write(`Invalid :${this.cssPseudoClass}() selector list format (${combinator}). Pseudo-class list does not accept selectors with combinators`, Trace.categories.Style, Trace.messageType.warn); break; } @@ -373,10 +373,24 @@ export class NotPseudoClassSelector extends SimpleSelector { } public toString(): string { const selectors = this.selectorGroups.map((group) => group.join('')); - return `:not(${selectors.join(', ')})${wrap(this.combinator)}`; + return `:${this.cssPseudoClass}(${selectors.join(', ')})${wrap(this.combinator)}`; } public match(node: Node): boolean { - return !this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + let matches; + + switch (this.cssPseudoClass) { + case 'is': + matches = this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + break; + case 'not': + matches = !this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + break; + default: + matches = false; + break; + } + + return matches; } public mayMatch(node: Node): boolean { return true; @@ -632,8 +646,8 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { } if (ast.type === 'pseudo') { - if (ast.name === 'not') { - return new NotPseudoClassSelector(ast.data); + if (ast.data) { + return new FunctionalPseudoClassSelector(ast.name, ast.data); } return new PseudoClassSelector(ast.name); } From fdbad980e0854bfb39b5886e785c2d9bb2d07202 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 12:24:16 +0000 Subject: [PATCH 08/31] chore: Minor trace improvement --- packages/core/ui/styling/css-selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 1a39555ac1..1bca73ec0e 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -356,7 +356,7 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { for (const ast of asts) { const combinator = Combinator[ast.type]; if (combinator != null) { - Trace.write(`Invalid :${this.cssPseudoClass}() selector list format (${combinator}). Pseudo-class list does not accept selectors with combinators`, Trace.categories.Style, Trace.messageType.warn); + Trace.write(`Invalid :${this.cssPseudoClass}() selector list format '${combinator}'(${ast.type}). Pseudo-class list does not accept selectors with combinators`, Trace.categories.Style, Trace.messageType.warn); break; } From 2fca2f652823224840c294bbc79bb10a223be6d4 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 15:02:04 +0000 Subject: [PATCH 09/31] feat: Added support for pseudo-class selector list types --- packages/core/ui/styling/css-selector.ts | 88 ++++++++++++++++-------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 1bca73ec0e..691820ca09 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -60,6 +60,12 @@ const enum Rarity { } /* eslint-enable @typescript-eslint/no-duplicate-enum-values */ +const enum PseudoClassSelectorList { + Regular = 0, + Forgiving = 1, + Relative = 2, +} + enum Combinator { 'descendant' = ' ', 'child' = '>', @@ -115,12 +121,13 @@ function getNodeDirectSibling(node): null | Node { return node.parent.getChildAt(nodeIndex - 1); } -function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic = false): ClassDecorator { +function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic = false, pseudoSelectorListType?: PseudoClassSelectorList): ClassDecorator { return (cls) => { cls.prototype.specificity = specificity; cls.prototype.rarity = rarity; cls.prototype.combinator = undefined; cls.prototype.dynamic = dynamic; + cls.prototype.pseudoSelectorListType = pseudoSelectorListType; return cls; }; @@ -133,6 +140,7 @@ export abstract class SelectorCore { public rarity: Rarity; public combinator: Combinator; public ruleset: RuleSet; + public pseudoSelectorListType?: PseudoClassSelectorList; /** * Dynamic selectors depend on attributes and pseudo classes. */ @@ -179,7 +187,7 @@ export class InvalidSelector extends SimpleSelector { super(); } public toString(): string { - return ``; + return `<${this.e}>`; } public match(node: Node): boolean { return false; @@ -340,9 +348,9 @@ export class PseudoClassSelector extends SimpleSelector { } } -@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic) +@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) export class FunctionalPseudoClassSelector extends PseudoClassSelector { - private selectorGroups: SimpleSelector[][]; + protected selectorGroups: SimpleSelector[][]; constructor(cssPseudoClass: string, dataType: CSSWhat.DataType) { super(cssPseudoClass); @@ -350,21 +358,40 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { const selectorGroups: SimpleSelector[][] = []; if (Array.isArray(dataType)) { + let hasInvalidSelector: boolean = false; + for (const asts of dataType) { const selectors: SimpleSelector[] = []; for (const ast of asts) { - const combinator = Combinator[ast.type]; - if (combinator != null) { - Trace.write(`Invalid :${this.cssPseudoClass}() selector list format '${combinator}'(${ast.type}). Pseudo-class list does not accept selectors with combinators`, Trace.categories.Style, Trace.messageType.warn); + const selector = createSimpleSelectorFromAst(ast); + if (selector instanceof InvalidSelector) { + if (Trace.isEnabled()) { + let message = `Invalid :${this.cssPseudoClass}() list selector '${selectors.join('') + selector}`; + if (Combinator[ast.type] != null) { + message += '. Pseudo-class selector list does not currently accept combinators'; + } + + Trace.write(message, Trace.categories.Style, Trace.messageType.warn); + } + + hasInvalidSelector = true; break; } - selectors.push(createSimpleSelectorFromAst(ast)); + selectors.push(selector); } - if (selectors.length) { - selectorGroups.push(selectors); + if (hasInvalidSelector) { + // Only forgiving selector list can ignore invalid selectors + if (this.pseudoSelectorListType !== PseudoClassSelectorList.Forgiving) { + selectorGroups.splice(0); + break; + } + } else { + if (selectors.length) { + selectorGroups.push(selectors); + } } } } @@ -376,21 +403,7 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { return `:${this.cssPseudoClass}(${selectors.join(', ')})${wrap(this.combinator)}`; } public match(node: Node): boolean { - let matches; - - switch (this.cssPseudoClass) { - case 'is': - matches = this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); - break; - case 'not': - matches = !this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); - break; - default: - matches = false; - break; - } - - return matches; + return false; } public mayMatch(node: Node): boolean { return true; @@ -404,6 +417,20 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { } } +@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) +export class NotFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { + public match(node: Node): boolean { + return !this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + } +} + +@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Forgiving) +export class IsFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { + public match(node: Node): boolean { + return this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + } +} + export class SimpleSelectorSequence extends SimpleSelector { private head: SimpleSelector; constructor(public selectors: SimpleSelector[]) { @@ -646,15 +673,22 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { } if (ast.type === 'pseudo') { - if (ast.data) { - return new FunctionalPseudoClassSelector(ast.name, ast.data); + if (ast.name === 'is') { + return new IsFunctionalPseudoClassSelector(ast.name, ast.data); + } + + if (ast.name === 'not') { + return new NotFunctionalPseudoClassSelector(ast.name, ast.data); } + return new PseudoClassSelector(ast.name); } if (ast.type === 'universal') { return new UniversalSelector(); } + + return new InvalidSelector(new Error(ast.type)); } function createSimpleSelectorSequence(selectors: Array): SimpleSelectorSequence { From 0748746f1a120f6e0c274e51be108b75ffa74a7f Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 17:45:00 +0000 Subject: [PATCH 10/31] feat: Proper functional pseudo-class specificity --- packages/core/ui/styling/css-selector.ts | 71 +++++++++++++++++------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 691820ca09..8aeefceaa3 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -46,6 +46,11 @@ const enum Specificity { Type = 1, Universal = 0, Invalid = 0, + Zero = 0, + /** + * Selector has the specificity of the selector with the highest specificity inside selector list. + */ + SelectorListHighest = -1, } const enum Rarity { @@ -348,14 +353,17 @@ export class PseudoClassSelector extends SimpleSelector { } } -@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) +@SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) export class FunctionalPseudoClassSelector extends PseudoClassSelector { - protected selectorGroups: SimpleSelector[][]; + protected selectorSequences: SimpleSelectorSequence[]; constructor(cssPseudoClass: string, dataType: CSSWhat.DataType) { super(cssPseudoClass); - const selectorGroups: SimpleSelector[][] = []; + const selectorSequences: SimpleSelectorSequence[] = []; + const needsHighestSpecificity: boolean = this.specificity === Specificity.SelectorListHighest; + + let specificity: number = 0; if (Array.isArray(dataType)) { let hasInvalidSelector: boolean = false; @@ -385,22 +393,30 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { if (hasInvalidSelector) { // Only forgiving selector list can ignore invalid selectors if (this.pseudoSelectorListType !== PseudoClassSelectorList.Forgiving) { - selectorGroups.splice(0); + selectorSequences.splice(0); + specificity = 0; break; } } else { if (selectors.length) { - selectorGroups.push(selectors); + const selectorSequence = new SimpleSelectorSequence(selectors); + + // The specificity of some pseudo-classes is replaced by the specificity of the most specific selector in its comma-separated argument of selectors + if (needsHighestSpecificity && selectorSequence.specificity > specificity) { + specificity = selectorSequence.specificity; + } + + selectorSequences.push(selectorSequence); } } } } - this.selectorGroups = selectorGroups; + this.selectorSequences = selectorSequences; + this.specificity = specificity; } public toString(): string { - const selectors = this.selectorGroups.map((group) => group.join('')); - return `:${this.cssPseudoClass}(${selectors.join(', ')})${wrap(this.combinator)}`; + return `:${this.cssPseudoClass}(${this.selectorSequences.join(', ')})${wrap(this.combinator)}`; } public match(node: Node): boolean { return false; @@ -409,25 +425,36 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { return true; } public trackChanges(node, map): void { - for (const selectors of this.selectorGroups) { - for (const selector of selectors) { - selector.trackChanges(node, map); - } + for (const sequence of this.selectorSequences) { + sequence.trackChanges(node, map); + } + } + + public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { + for (const sequence of this.selectorSequences) { + sequence.lookupSort(sorter, base); } } } -@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) +@SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) export class NotFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { - return !this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + return !this.selectorSequences.some((sequence) => sequence.match(node)); } } -@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Forgiving) +@SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Forgiving) export class IsFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { - return this.selectorGroups.some((selectors) => !selectors.some((selector) => !selector.match(node))); + return this.selectorSequences.some((sequence) => sequence.match(node)); + } +} + +@SelectorProperties(Specificity.Zero, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Forgiving) +export class WhereFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { + public match(node: Node): boolean { + return this.selectorSequences.some((sequence) => sequence.match(node)); } } @@ -677,6 +704,10 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { return new IsFunctionalPseudoClassSelector(ast.name, ast.data); } + if (ast.name === 'where') { + return new WhereFunctionalPseudoClassSelector(ast.name, ast.data); + } + if (ast.name === 'not') { return new NotFunctionalPseudoClassSelector(ast.name, ast.data); } @@ -691,7 +722,7 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { return new InvalidSelector(new Error(ast.type)); } -function createSimpleSelectorSequence(selectors: Array): SimpleSelectorSequence { +function initSimpleSelectorSequenceWithSelectors(selectors: Array): SimpleSelectorSequence { return new SimpleSelectorSequence(selectors.map(createSimpleSelectorFromAst)); } @@ -713,7 +744,7 @@ function createSelectorFromAst(asts: CSSWhat.Selector[]): SimpleSelector | Simpl // Combinator means the end of a sequence if (combinator != null) { - const simpleSelectorSequence = createSimpleSelectorSequence(pendingSelectorInstances); + const simpleSelectorSequence = initSimpleSelectorSequenceWithSelectors(pendingSelectorInstances); simpleSelectorSequence.combinator = combinator; simpleSelectorSequences.push(simpleSelectorSequence); @@ -728,11 +759,11 @@ function createSelectorFromAst(asts: CSSWhat.Selector[]): SimpleSelector | Simpl if (combinatorCount > 0) { // Create a sequence using the remaining selectors after the last combinator if (pendingSelectorInstances.length) { - simpleSelectorSequences.push(createSimpleSelectorSequence(pendingSelectorInstances)); + simpleSelectorSequences.push(initSimpleSelectorSequenceWithSelectors(pendingSelectorInstances)); } result = new Selector(simpleSelectorSequences); } else { - result = createSimpleSelectorSequence(pendingSelectorInstances); + result = initSimpleSelectorSequenceWithSelectors(pendingSelectorInstances); } } From 39e72dab6860cde109512a278d498f8ed7d6bb9a Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 18:14:12 +0000 Subject: [PATCH 11/31] fix: Added null-check for pseudo-class lookupSort calls --- packages/core/ui/styling/css-selector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 8aeefceaa3..a71b3548df 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -429,10 +429,10 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { sequence.trackChanges(node, map); } } - public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { + const baseSelector = base || this; for (const sequence of this.selectorSequences) { - sequence.lookupSort(sorter, base); + sequence.lookupSort(sorter, baseSelector); } } } From 12103ce727855bb4960bcd84395b85a5f497975d Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 19:19:22 +0000 Subject: [PATCH 12/31] fix: Removed pseudo-class lookupSort override as it caused problems --- packages/core/ui/styling/css-selector.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index a71b3548df..965599fea4 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -429,12 +429,6 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { sequence.trackChanges(node, map); } } - public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { - const baseSelector = base || this; - for (const sequence of this.selectorSequences) { - sequence.lookupSort(sorter, baseSelector); - } - } } @SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) From 39acfca31b45e2464796b6f049bc749c83afdd71 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 19:38:57 +0000 Subject: [PATCH 13/31] feat: Proper dynamic state for functional pseudo-classes --- packages/core/ui/styling/css-selector.ts | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 965599fea4..1c75a5b48e 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -126,12 +126,23 @@ function getNodeDirectSibling(node): null | Node { return node.parent.getChildAt(nodeIndex - 1); } -function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic = false, pseudoSelectorListType?: PseudoClassSelectorList): ClassDecorator { +function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic = false): ClassDecorator { return (cls) => { cls.prototype.specificity = specificity; cls.prototype.rarity = rarity; cls.prototype.combinator = undefined; cls.prototype.dynamic = dynamic; + + return cls; + }; +} + +function FunctionalPseudoClassProperties(specificity: Specificity, rarity: Rarity, pseudoSelectorListType: PseudoClassSelectorList): ClassDecorator { + return (cls) => { + cls.prototype.specificity = specificity; + cls.prototype.rarity = rarity; + cls.prototype.combinator = undefined; + cls.prototype.dynamic = false; cls.prototype.pseudoSelectorListType = pseudoSelectorListType; return cls; @@ -145,7 +156,6 @@ export abstract class SelectorCore { public rarity: Rarity; public combinator: Combinator; public ruleset: RuleSet; - public pseudoSelectorListType?: PseudoClassSelectorList; /** * Dynamic selectors depend on attributes and pseudo classes. */ @@ -353,9 +363,10 @@ export class PseudoClassSelector extends SimpleSelector { } } -@SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) +@FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) export class FunctionalPseudoClassSelector extends PseudoClassSelector { protected selectorSequences: SimpleSelectorSequence[]; + protected selectorListType?: PseudoClassSelectorList; constructor(cssPseudoClass: string, dataType: CSSWhat.DataType) { super(cssPseudoClass); @@ -392,7 +403,7 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { if (hasInvalidSelector) { // Only forgiving selector list can ignore invalid selectors - if (this.pseudoSelectorListType !== PseudoClassSelectorList.Forgiving) { + if (this.selectorListType !== PseudoClassSelectorList.Forgiving) { selectorSequences.splice(0); specificity = 0; break; @@ -414,6 +425,8 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { this.selectorSequences = selectorSequences; this.specificity = specificity; + // Functional pseudo-classes become dynamic based on selectors in selector list + this.dynamic = this.selectorSequences.some((sequence) => sequence.dynamic); } public toString(): string { return `:${this.cssPseudoClass}(${this.selectorSequences.join(', ')})${wrap(this.combinator)}`; @@ -431,21 +444,21 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { } } -@SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Regular) +@FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) export class NotFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { return !this.selectorSequences.some((sequence) => sequence.match(node)); } } -@SelectorProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Forgiving) +@FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Forgiving) export class IsFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { return this.selectorSequences.some((sequence) => sequence.match(node)); } } -@SelectorProperties(Specificity.Zero, Rarity.PseudoClass, Match.Dynamic, PseudoClassSelectorList.Forgiving) +@FunctionalPseudoClassProperties(Specificity.Zero, Rarity.PseudoClass, PseudoClassSelectorList.Forgiving) export class WhereFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { return this.selectorSequences.some((sequence) => sequence.match(node)); From 158751ca9d3cce9de550fd29e590e6c5e72d512d Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 20:02:34 +0000 Subject: [PATCH 14/31] chore: Removed old css selector parser unused code --- packages/core/css/parser.spec.ts | 119 +---------------- packages/core/css/parser.ts | 158 ----------------------- packages/core/ui/styling/css-selector.ts | 15 +-- 3 files changed, 9 insertions(+), 283 deletions(-) diff --git a/packages/core/css/parser.spec.ts b/packages/core/css/parser.spec.ts index a41c669467..5c246f2020 100644 --- a/packages/core/css/parser.spec.ts +++ b/packages/core/css/parser.spec.ts @@ -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'; @@ -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]] }); - (['=', '^=', '$=', '*=', '=', '~=', '|=']).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; @@ -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); diff --git a/packages/core/css/parser.ts b/packages/core/css/parser.ts index 83705ba3da..d451f58df2 100644 --- a/packages/core/css/parser.ts +++ b/packages/core/css/parser.ts @@ -612,161 +612,3 @@ export function parseBackground(text: string, start = 0): Parsed { return { start, end, value }; } - -// Selectors - -export type Combinator = '+' | '~' | '>' | ' '; - -export interface UniversalSelector { - type: '*'; -} -export interface TypeSelector { - type: ''; - identifier: string; -} -export interface ClassSelector { - type: '.'; - identifier: string; -} -export interface IdSelector { - type: '#'; - identifier: string; -} -export interface PseudoClassSelector { - type: ':'; - identifier: string; -} -export type AttributeSelectorTest = '=' | '^=' | '$=' | '*=' | '~=' | '|='; -export interface AttributeSelector { - type: '[]'; - property: string; - test?: AttributeSelectorTest; - value?: string; -} - -export type SimpleSelector = UniversalSelector | TypeSelector | ClassSelector | IdSelector | PseudoClassSelector | AttributeSelector; -export type SimpleSelectorSequence = SimpleSelector[]; -export type SelectorCombinatorPair = [SimpleSelectorSequence, Combinator]; -export type Selector = SelectorCombinatorPair[]; - -const universalSelectorRegEx = /\*/gy; -export function parseUniversalSelector(text: string, start = 0): Parsed { - universalSelectorRegEx.lastIndex = start; - const result = universalSelectorRegEx.exec(text); - if (!result) { - return null; - } - const end = universalSelectorRegEx.lastIndex; - - return { start, end, value: { type: '*' } }; -} - -const simpleIdentifierSelectorRegEx = /(#|\.|:|\b)((?:[\w_-]|\\.)(?:[\w\d_-]|\\.)*)/guy; -const unicodeEscapeRegEx = /\\([0-9a-fA-F]{1,5}\s|[0-9a-fA-F]{6})/g; -export function parseSimpleIdentifierSelector(text: string, start = 0): Parsed { - simpleIdentifierSelectorRegEx.lastIndex = start; - const result = simpleIdentifierSelectorRegEx.exec(text.replace(unicodeEscapeRegEx, (_, c) => '\\' + String.fromCodePoint(parseInt(c.trim(), 16)))); - if (!result) { - return null; - } - const end = simpleIdentifierSelectorRegEx.lastIndex; - const type = <'#' | '.' | ':' | ''>result[1]; - const identifier: string = result[2].replace(/\\/g, ''); - const value = { type, identifier }; - - return { start, end, value }; -} - -const attributeSelectorRegEx = /\[\s*([_\-\w][_\-\w\d]*)\s*(?:(=|\^=|\$=|\*=|\~=|\|=)\s*(?:([_\-\w][_\-\w\d]*)|"((?:[^\\"]|\\(?:"|n|r|f|\\|0-9a-f))*)"|'((?:[^\\']|\\(?:'|n|r|f|\\|0-9a-f))*)')\s*)?\]/gy; -export function parseAttributeSelector(text: string, start: number): Parsed { - attributeSelectorRegEx.lastIndex = start; - const result = attributeSelectorRegEx.exec(text); - if (!result) { - return null; - } - const end = attributeSelectorRegEx.lastIndex; - const property = result[1]; - if (result[2]) { - const test = result[2]; - const value = result[3] || result[4] || result[5]; - - return { start, end, value: { type: '[]', property, test, value } }; - } - - return { start, end, value: { type: '[]', property } }; -} - -export function parseSimpleSelector(text: string, start = 0): Parsed { - return parseUniversalSelector(text, start) || parseSimpleIdentifierSelector(text, start) || parseAttributeSelector(text, start); -} - -export function parseSimpleSelectorSequence(text: string, start: number): Parsed { - let simpleSelector = parseSimpleSelector(text, start); - if (!simpleSelector) { - return null; - } - let end = simpleSelector.end; - const value = []; - while (simpleSelector) { - value.push(simpleSelector.value); - end = simpleSelector.end; - simpleSelector = parseSimpleSelector(text, end); - } - - return { start, end, value }; -} - -const combinatorRegEx = /\s*([+~>])?\s*/gy; -export function parseCombinator(text: string, start = 0): Parsed { - combinatorRegEx.lastIndex = start; - const result = combinatorRegEx.exec(text); - if (!result) { - return null; - } - const end = combinatorRegEx.lastIndex; - const value = result[1] || ' '; - - return { start, end, value }; -} - -const whiteSpaceRegEx = /\s*/gy; -export function parseSelector(text: string, start = 0): Parsed { - let end = start; - whiteSpaceRegEx.lastIndex = end; - const leadingWhiteSpace = whiteSpaceRegEx.exec(text); - if (leadingWhiteSpace) { - end = whiteSpaceRegEx.lastIndex; - } - const value = []; - let combinator: Parsed; - let expectSimpleSelector = true; // Must have at least one - let pair: SelectorCombinatorPair; - do { - const simpleSelectorSequence = parseSimpleSelectorSequence(text, end); - if (!simpleSelectorSequence) { - if (expectSimpleSelector) { - return null; - } else { - break; - } - } - end = simpleSelectorSequence.end; - if (combinator) { - // This logic looks weird; this `if` statement would occur on the next LOOP, so it effects the prior `pair` - // variable which is already pushed into the `value` array is going to have its `undefined` set to this - // value before the following statement creates a new `pair` memory variable. - // noinspection JSUnusedAssignment - pair[1] = combinator.value; - } - pair = [simpleSelectorSequence.value, undefined]; - value.push(pair); - - combinator = parseCombinator(text, end); - if (combinator) { - end = combinator.end; - } - expectSimpleSelector = combinator && combinator.value !== ' '; // Simple selector must follow non trailing white space combinator - } while (combinator); - - return { start, end, value }; -} diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 1c75a5b48e..18548cc73b 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -1,11 +1,10 @@ -import * as CSSWhat from 'css-what'; +import { parse as convertToCSSWhatSelector, Selector as CSSWhatSelector, DataType as CSSWhatDataType } from 'css-what'; import '../../globals'; import { isCssVariable } from '../core/properties'; import { Trace } from '../../trace'; import { isNullOrUndefined, isUndefined } from '../../utils/types'; import * as ReworkCSS from '../../css'; -import { Combinator as ICombinator, SimpleSelectorSequence as ISimpleSelectorSequence, Selector as ISelector, SimpleSelector as ISimpleSelector, parseSelector } from '../../css/parser'; /** * An interface describing the shape of a type on which the selectors may apply. @@ -368,7 +367,7 @@ export class FunctionalPseudoClassSelector extends PseudoClassSelector { protected selectorSequences: SimpleSelectorSequence[]; protected selectorListType?: PseudoClassSelectorList; - constructor(cssPseudoClass: string, dataType: CSSWhat.DataType) { + constructor(cssPseudoClass: string, dataType: CSSWhatDataType) { super(cssPseudoClass); const selectorSequences: SimpleSelectorSequence[] = []; @@ -689,7 +688,7 @@ function createDeclaration(decl: ReworkCSS.Declaration): any { return { property: isCssVariable(decl.property) ? decl.property : decl.property.toLowerCase(), value: decl.value }; } -function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { +function createSimpleSelectorFromAst(ast: CSSWhatSelector): SimpleSelector { if (ast.type === 'attribute') { if (ast.name === 'class') { return new ClassSelector(ast.value); @@ -729,11 +728,11 @@ function createSimpleSelectorFromAst(ast: CSSWhat.Selector): SimpleSelector { return new InvalidSelector(new Error(ast.type)); } -function initSimpleSelectorSequenceWithSelectors(selectors: Array): SimpleSelectorSequence { +function initSimpleSelectorSequenceWithSelectors(selectors: Array): SimpleSelectorSequence { return new SimpleSelectorSequence(selectors.map(createSimpleSelectorFromAst)); } -function createSelectorFromAst(asts: CSSWhat.Selector[]): SimpleSelector | SimpleSelectorSequence | Selector { +function createSelectorFromAst(asts: CSSWhatSelector[]): SimpleSelector | SimpleSelectorSequence | Selector { let result: SimpleSelector | SimpleSelectorSequence | Selector; if (asts.length === 0) { @@ -743,7 +742,7 @@ function createSelectorFromAst(asts: CSSWhat.Selector[]): SimpleSelector | Simpl } else { const simpleSelectorSequences: Array = []; - let pendingSelectorInstances: Array = []; + let pendingSelectorInstances: Array = []; let combinatorCount: number = 0; for (const ast of asts) { @@ -779,7 +778,7 @@ function createSelectorFromAst(asts: CSSWhat.Selector[]): SimpleSelector | Simpl export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector { try { - const result = CSSWhat.parse(sel); + const result = convertToCSSWhatSelector(sel); if (!result?.length) { return new InvalidSelector(new Error('Empty selector')); } From 3ba567ce38116789416c4aac80be6ab1b98450a1 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 21:28:20 +0000 Subject: [PATCH 15/31] chore: Base functional pseudo-class should be abstract --- packages/core/ui/styling/css-selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 18548cc73b..c6d7efd255 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -363,7 +363,7 @@ export class PseudoClassSelector extends SimpleSelector { } @FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) -export class FunctionalPseudoClassSelector extends PseudoClassSelector { +export abstract class FunctionalPseudoClassSelector extends PseudoClassSelector { protected selectorSequences: SimpleSelectorSequence[]; protected selectorListType?: PseudoClassSelectorList; From a34b7dea652f9f709d3b19dc2d2728b245ac460e Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 21:32:01 +0000 Subject: [PATCH 16/31] chore: Removed unused method --- packages/core/ui/styling/css-selector.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index c6d7efd255..9c02f01d3c 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -436,11 +436,6 @@ export abstract class FunctionalPseudoClassSelector extends PseudoClassSelector public mayMatch(node: Node): boolean { return true; } - public trackChanges(node, map): void { - for (const sequence of this.selectorSequences) { - sequence.trackChanges(node, map); - } - } } @FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) From 35b72cbfd1ef65d737b8c78e7d2ff3b775e4b5ab Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 14 Apr 2024 21:33:47 +0000 Subject: [PATCH 17/31] chore: Re-added the previously removed method --- packages/core/ui/styling/css-selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 9c02f01d3c..c6d7efd255 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -436,6 +436,11 @@ export abstract class FunctionalPseudoClassSelector extends PseudoClassSelector public mayMatch(node: Node): boolean { return true; } + public trackChanges(node, map): void { + for (const sequence of this.selectorSequences) { + sequence.trackChanges(node, map); + } + } } @FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) From 55d281549889d6408d96b741a2b2370c1f05bfea Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 15 Apr 2024 00:08:40 +0000 Subject: [PATCH 18/31] perf: Do not generate selector sequences for a single selectors --- packages/core/ui/styling/css-selector.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index c6d7efd255..4f8cc04c1d 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -728,7 +728,15 @@ function createSimpleSelectorFromAst(ast: CSSWhatSelector): SimpleSelector { return new InvalidSelector(new Error(ast.type)); } -function initSimpleSelectorSequenceWithSelectors(selectors: Array): SimpleSelectorSequence { +function createSimpleSelectorSequenceFromAst(selectors: Array): SimpleSelectorSequence | SimpleSelector { + if (selectors.length === 0) { + return new InvalidSelector(new Error('Empty simple selector sequence.')); + } + + if (selectors.length === 1) { + return createSimpleSelectorFromAst(selectors[0]); + } + return new SimpleSelectorSequence(selectors.map(createSimpleSelectorFromAst)); } @@ -740,7 +748,7 @@ function createSelectorFromAst(asts: CSSWhatSelector[]): SimpleSelector | Simple } else if (asts.length === 1) { result = createSimpleSelectorFromAst(asts[0]); } else { - const simpleSelectorSequences: Array = []; + const simpleSelectorSequences: Array = []; let pendingSelectorInstances: Array = []; let combinatorCount: number = 0; @@ -750,7 +758,7 @@ function createSelectorFromAst(asts: CSSWhatSelector[]): SimpleSelector | Simple // Combinator means the end of a sequence if (combinator != null) { - const simpleSelectorSequence = initSimpleSelectorSequenceWithSelectors(pendingSelectorInstances); + const simpleSelectorSequence = createSimpleSelectorSequenceFromAst(pendingSelectorInstances); simpleSelectorSequence.combinator = combinator; simpleSelectorSequences.push(simpleSelectorSequence); @@ -765,11 +773,11 @@ function createSelectorFromAst(asts: CSSWhatSelector[]): SimpleSelector | Simple if (combinatorCount > 0) { // Create a sequence using the remaining selectors after the last combinator if (pendingSelectorInstances.length) { - simpleSelectorSequences.push(initSimpleSelectorSequenceWithSelectors(pendingSelectorInstances)); + simpleSelectorSequences.push(createSimpleSelectorSequenceFromAst(pendingSelectorInstances)); } result = new Selector(simpleSelectorSequences); } else { - result = initSimpleSelectorSequenceWithSelectors(pendingSelectorInstances); + result = createSimpleSelectorSequenceFromAst(pendingSelectorInstances); } } From aeb4fd31bcd36606dca2dec97fb36b372b88c3c9 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 15 Apr 2024 13:13:15 +0000 Subject: [PATCH 19/31] feat: Added combinator support for pseudo-class selector list --- packages/core/ui/styling/css-selector.ts | 169 ++++++++++++----------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 4f8cc04c1d..9fa0c3a0d8 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -165,6 +165,7 @@ export abstract class SelectorCore { * If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state. */ public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean; + public abstract trackChanges(node: Node, map: ChangeAccumulator): void; public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); } @@ -362,73 +363,49 @@ export class PseudoClassSelector extends SimpleSelector { } } -@FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) export abstract class FunctionalPseudoClassSelector extends PseudoClassSelector { - protected selectorSequences: SimpleSelectorSequence[]; + protected selectors: Array; protected selectorListType?: PseudoClassSelectorList; constructor(cssPseudoClass: string, dataType: CSSWhatDataType) { super(cssPseudoClass); - const selectorSequences: SimpleSelectorSequence[] = []; + const selectors: Array = []; const needsHighestSpecificity: boolean = this.specificity === Specificity.SelectorListHighest; let specificity: number = 0; if (Array.isArray(dataType)) { - let hasInvalidSelector: boolean = false; - for (const asts of dataType) { - const selectors: SimpleSelector[] = []; - - for (const ast of asts) { - const selector = createSimpleSelectorFromAst(ast); - if (selector instanceof InvalidSelector) { - if (Trace.isEnabled()) { - let message = `Invalid :${this.cssPseudoClass}() list selector '${selectors.join('') + selector}`; - if (Combinator[ast.type] != null) { - message += '. Pseudo-class selector list does not currently accept combinators'; - } + const selector: SimpleSelector | SimpleSelectorSequence | Selector = createSelectorFromAst(asts); - Trace.write(message, Trace.categories.Style, Trace.messageType.warn); - } - - hasInvalidSelector = true; - break; - } - - selectors.push(selector); - } - - if (hasInvalidSelector) { + if (selector instanceof InvalidSelector) { // Only forgiving selector list can ignore invalid selectors if (this.selectorListType !== PseudoClassSelectorList.Forgiving) { - selectorSequences.splice(0); + selectors.splice(0); specificity = 0; break; } - } else { - if (selectors.length) { - const selectorSequence = new SimpleSelectorSequence(selectors); - // The specificity of some pseudo-classes is replaced by the specificity of the most specific selector in its comma-separated argument of selectors - if (needsHighestSpecificity && selectorSequence.specificity > specificity) { - specificity = selectorSequence.specificity; - } + continue; + } - selectorSequences.push(selectorSequence); - } + // The specificity of some pseudo-classes is replaced by the specificity of the most specific selector in its comma-separated argument of selectors + if (needsHighestSpecificity && selector.specificity > specificity) { + specificity = selector.specificity; } + + selectors.push(selector); } } - this.selectorSequences = selectorSequences; + this.selectors = selectors; this.specificity = specificity; // Functional pseudo-classes become dynamic based on selectors in selector list - this.dynamic = this.selectorSequences.some((sequence) => sequence.dynamic); + this.dynamic = this.selectors.some((sel) => sel.dynamic); } public toString(): string { - return `:${this.cssPseudoClass}(${this.selectorSequences.join(', ')})${wrap(this.combinator)}`; + return `:${this.cssPseudoClass}(${this.selectors.join(', ')})${wrap(this.combinator)}`; } public match(node: Node): boolean { return false; @@ -436,31 +413,29 @@ export abstract class FunctionalPseudoClassSelector extends PseudoClassSelector public mayMatch(node: Node): boolean { return true; } - public trackChanges(node, map): void { - for (const sequence of this.selectorSequences) { - sequence.trackChanges(node, map); - } + public trackChanges(node: Node, map: ChangeAccumulator): void { + this.selectors.forEach((sel) => sel.trackChanges(node, map)); } } @FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Regular) export class NotFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { - return !this.selectorSequences.some((sequence) => sequence.match(node)); + return !this.selectors.some((sel) => sel.match(node)); } } @FunctionalPseudoClassProperties(Specificity.SelectorListHighest, Rarity.PseudoClass, PseudoClassSelectorList.Forgiving) export class IsFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { - return this.selectorSequences.some((sequence) => sequence.match(node)); + return this.selectors.some((sel) => sel.match(node)); } } @FunctionalPseudoClassProperties(Specificity.Zero, Rarity.PseudoClass, PseudoClassSelectorList.Forgiving) export class WhereFunctionalPseudoClassSelector extends FunctionalPseudoClassSelector { public match(node: Node): boolean { - return this.selectorSequences.some((sequence) => sequence.match(node)); + return this.selectors.some((sel) => sel.match(node)); } } @@ -481,7 +456,7 @@ export class SimpleSelectorSequence extends SimpleSelector { public mayMatch(node: Node): boolean { return this.selectors.every((sel) => sel.mayMatch(node)); } - public trackChanges(node, map): void { + public trackChanges(node: Node, map: ChangeAccumulator): void { this.selectors.forEach((sel) => sel.trackChanges(node, map)); } public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { @@ -553,6 +528,10 @@ export class Selector extends SelectorCore { }); } + public trackChanges(node: Node, map: ChangeAccumulator): void { + this.selectors.forEach((sel) => sel.trackChanges(node, map)); + } + public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { this.last.lookupSort(sorter, this); } @@ -728,60 +707,82 @@ function createSimpleSelectorFromAst(ast: CSSWhatSelector): SimpleSelector { return new InvalidSelector(new Error(ast.type)); } -function createSimpleSelectorSequenceFromAst(selectors: Array): SimpleSelectorSequence | SimpleSelector { - if (selectors.length === 0) { +function createSimpleSelectorSequenceFromAst(asts: CSSWhatSelector[]): SimpleSelectorSequence | SimpleSelector { + if (asts.length === 0) { return new InvalidSelector(new Error('Empty simple selector sequence.')); } - if (selectors.length === 1) { - return createSimpleSelectorFromAst(selectors[0]); + if (asts.length === 1) { + return createSimpleSelectorFromAst(asts[0]); } - return new SimpleSelectorSequence(selectors.map(createSimpleSelectorFromAst)); + const sequenceSelectors: SimpleSelector[] = []; + + for (const ast of asts) { + const selector = createSimpleSelectorFromAst(ast); + if (selector instanceof InvalidSelector) { + return selector; + } + + sequenceSelectors.push(selector); + } + + return new SimpleSelectorSequence(sequenceSelectors); } -function createSelectorFromAst(asts: CSSWhatSelector[]): SimpleSelector | SimpleSelectorSequence | Selector { +function createSelectorFromAst(asts: Array): SimpleSelector | SimpleSelectorSequence | Selector { let result: SimpleSelector | SimpleSelectorSequence | Selector; if (asts.length === 0) { - result = new InvalidSelector(new Error('Empty selector.')); - } else if (asts.length === 1) { - result = createSimpleSelectorFromAst(asts[0]); - } else { - const simpleSelectorSequences: Array = []; - - let pendingSelectorInstances: Array = []; - let combinatorCount: number = 0; - - for (const ast of asts) { - const combinator = Combinator[ast.type]; - - // Combinator means the end of a sequence - if (combinator != null) { - const simpleSelectorSequence = createSimpleSelectorSequenceFromAst(pendingSelectorInstances); - simpleSelectorSequence.combinator = combinator; - simpleSelectorSequences.push(simpleSelectorSequence); - - combinatorCount++; - // Cleanup stored selectors for the new sequence to take place - pendingSelectorInstances = []; - } else { - pendingSelectorInstances.push(ast); + return new InvalidSelector(new Error('Empty selector.')); + } + + if (asts.length === 1) { + return createSimpleSelectorFromAst(asts[0]); + } + + const simpleSelectorSequences: Array = []; + + let sequenceAsts: CSSWhatSelector[] = []; + let combinatorCount: number = 0; + + for (const ast of asts) { + const combinator = Combinator[ast.type]; + + // Combinator means the end of a sequence + if (combinator != null) { + const selector = createSimpleSelectorSequenceFromAst(sequenceAsts); + + if (selector instanceof InvalidSelector) { + return selector; } + + selector.combinator = combinator; + simpleSelectorSequences.push(selector); + + combinatorCount++; + // Cleanup stored selectors for the new sequence to take place + sequenceAsts = []; + } else { + sequenceAsts.push(ast); } + } + + if (combinatorCount > 0) { + // Create a sequence using the remaining selectors after the last combinator + if (sequenceAsts.length) { + const selector = createSimpleSelectorSequenceFromAst(sequenceAsts); - if (combinatorCount > 0) { - // Create a sequence using the remaining selectors after the last combinator - if (pendingSelectorInstances.length) { - simpleSelectorSequences.push(createSimpleSelectorSequenceFromAst(pendingSelectorInstances)); + if (selector instanceof InvalidSelector) { + return selector; } - result = new Selector(simpleSelectorSequences); - } else { - result = createSimpleSelectorSequenceFromAst(pendingSelectorInstances); + + simpleSelectorSequences.push(selector); } + return new Selector(simpleSelectorSequences); } - return result; + return createSimpleSelectorSequenceFromAst(sequenceAsts); } export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector { From e36ce666c37c1eef3e06950817a28dc96a020850 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Tue, 16 Apr 2024 12:57:51 +0000 Subject: [PATCH 20/31] feat: Added support for CSS general sibling combination (~) --- packages/core/ui/styling/css-selector.ts | 137 ++++++++++++++++++++--- 1 file changed, 124 insertions(+), 13 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 9fa0c3a0d8..0cbec1a46b 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -21,6 +21,7 @@ export interface Node { cssPseudoClasses?: Set; getChildIndex?(node: Node): number; getChildAt?(index: number): Node; + getChildrenCount?(): number; } export interface Declaration { @@ -111,9 +112,28 @@ namespace Match { export const Static = false; } -const SUPPORTED_COMBINATORS: Array = [Combinator.descendant, Combinator.child, Combinator.adjacent]; +const SUPPORTED_COMBINATORS: Array = [Combinator.descendant, Combinator.child, Combinator.adjacent, Combinator.sibling]; -function getNodeDirectSibling(node): null | Node { +function eachNodePreviousGeneralSibling(node: Node, callback: (sibling: Node) => boolean): void { + if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt || !node.parent.getChildrenCount) { + return; + } + + const nodeIndex = node.parent.getChildIndex(node); + if (nodeIndex === 0) { + return; + } + + const count = node.parent.getChildrenCount(); + let retVal: boolean = true; + + for (let i = nodeIndex - 1; i >= 0 && retVal; i--) { + const sibling = node.parent.getChildAt(i); + retVal = callback(sibling); + } +} + +function getNodePreviousDirectSibling(node: Node): null | Node { if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt) { return null; } @@ -486,10 +506,15 @@ export class Selector extends SelectorCore { throw new Error(`Unsupported combinator "${sel.combinator}" for selector ${sel}.`); } if (!isCombinatorSet || sel.combinator === ' ') { - groups.push((lastGroup = [(siblingGroup = [])])); + siblingGroup = []; + lastGroup = [siblingGroup]; + + groups.push(lastGroup); } if (sel.combinator === '>') { - lastGroup.push((siblingGroup = [])); + siblingGroup = []; + + lastGroup.push(siblingGroup); } this.specificity += sel.specificity; @@ -593,10 +618,17 @@ export class Selector extends SelectorCore { } export namespace Selector { // Non-spec. Selector sequences are grouped by ancestor then by child combinators for easier backtracking. - export class ChildGroup { + export abstract class Group { public dynamic: boolean; + public abstract match(node: Node): Node; + public abstract mayMatch(node: Node): Node; + public abstract trackChanges(node: Node, map: ChangeAccumulator): void; + } + export class ChildGroup extends Group { constructor(private selectors: SiblingGroup[]) { + super(); + this.dynamic = selectors.some((sel) => sel.dynamic); } @@ -608,29 +640,108 @@ export namespace Selector { return this.selectors.every((sel, i) => (node = i === 0 ? node : node.parent) && sel.mayMatch(node)) ? node : null; } - public trackChanges(node: Node, map: ChangeAccumulator) { - this.selectors.forEach((sel, i) => (node = i === 0 ? node : node.parent) && sel.trackChanges(node, map)); + public trackChanges(node: Node, map: ChangeAccumulator): void { + this.selectors.forEach((sel, i) => { + if (i === 0) { + node && sel.trackChanges(node, map); + } else { + node = node.parent; + + if (node && sel.mayMatch(node)) { + sel.trackChanges(node, map); + } + } + }); } } - export class SiblingGroup { - public dynamic: boolean; + export class SiblingGroup extends Group { constructor(private selectors: SimpleSelector[]) { + super(); + this.dynamic = selectors.some((sel) => sel.dynamic); } public match(node: Node): Node { - return this.selectors.every((sel, i) => (node = i === 0 ? node : getNodeDirectSibling(node)) && sel.match(node)) ? node : null; + return this.selectors.every((sel, i) => { + if (i === 0) { + return node && sel.match(node); + } + + if (sel.combinator === Combinator.adjacent) { + node = getNodePreviousDirectSibling(node); + return node && sel.match(node); + } + + // Sibling combinator + let isMatching: boolean = false; + + eachNodePreviousGeneralSibling(node, (sibling) => { + isMatching = sel.match(sibling); + return !isMatching; + }); + + return isMatching; + }) + ? node + : null; } public mayMatch(node: Node): Node { - return this.selectors.every((sel, i) => (node = i === 0 ? node : getNodeDirectSibling(node)) && sel.mayMatch(node)) ? node : null; + return this.selectors.every((sel, i) => { + if (i === 0) { + return node && sel.mayMatch(node); + } + + if (sel.combinator === Combinator.adjacent) { + node = getNodePreviousDirectSibling(node); + return node && sel.mayMatch(node); + } + + // Sibling combinator + let isMatching: boolean = false; + + eachNodePreviousGeneralSibling(node, (sibling) => { + isMatching = sel.mayMatch(sibling); + return !isMatching; + }); + + return isMatching; + }) + ? node + : null; } - public trackChanges(node: Node, map: ChangeAccumulator) { - this.selectors.forEach((sel, i) => (node = i === 0 ? node : getNodeDirectSibling(node)) && sel.trackChanges(node, map)); + public trackChanges(node: Node, map: ChangeAccumulator): void { + this.selectors.forEach((sel, i) => { + if (i === 0) { + node && sel.trackChanges(node, map); + } else { + if (sel.combinator === Combinator.adjacent) { + node = getNodePreviousDirectSibling(node); + if (node && sel.mayMatch(node)) { + sel.trackChanges(node, map); + } + } else { + // Sibling combinator + let matchingSibling: Node; + + eachNodePreviousGeneralSibling(node, (sibling) => { + const isMatching = sel.mayMatch(sibling); + if (isMatching) { + matchingSibling = sibling; + } + + return !isMatching; + }); + + matchingSibling && sel.trackChanges(matchingSibling, map); + } + } + }); } } + export interface Bound { left: Node; right: Node; From 4a7aa626e41218d71c0a8248d3338b7667d94e68 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Tue, 16 Apr 2024 16:33:05 +0000 Subject: [PATCH 21/31] chore: Better CSS selector combinator readability --- packages/core/ui/styling/css-selector.ts | 42 +++++++++++++----------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 0cbec1a46b..58f8e6567c 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -2,7 +2,7 @@ import { parse as convertToCSSWhatSelector, Selector as CSSWhatSelector, DataTyp import '../../globals'; import { isCssVariable } from '../core/properties'; import { Trace } from '../../trace'; -import { isNullOrUndefined, isUndefined } from '../../utils/types'; +import { isNullOrUndefined } from '../../utils/types'; import * as ReworkCSS from '../../css'; @@ -112,8 +112,6 @@ namespace Match { export const Static = false; } -const SUPPORTED_COMBINATORS: Array = [Combinator.descendant, Combinator.child, Combinator.adjacent, Combinator.sibling]; - function eachNodePreviousGeneralSibling(node: Node, callback: (sibling: Node) => boolean): void { if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt || !node.parent.getChildrenCount) { return; @@ -485,36 +483,40 @@ export class SimpleSelectorSequence extends SimpleSelector { } export class Selector extends SelectorCore { - // Grouped by ancestor combinators, then by direct child combinators. + // Grouped by ancestor combinators, then by child combinators. private groups: Selector.ChildGroup[]; private last: SelectorCore; constructor(public selectors: SimpleSelector[]) { super(); let siblingGroup: SimpleSelector[]; - let lastGroup: SimpleSelector[][]; + let currentGroup: SimpleSelector[][]; const groups: SimpleSelector[][][] = []; this.specificity = 0; this.dynamic = false; - for (let i = selectors.length - 1; i > -1; i--) { + for (let i = selectors.length - 1; i >= 0; i--) { const sel = selectors[i]; - const isCombinatorSet = !isUndefined(sel.combinator); - - if (isCombinatorSet && !SUPPORTED_COMBINATORS.includes(sel.combinator)) { - throw new Error(`Unsupported combinator "${sel.combinator}" for selector ${sel}.`); - } - if (!isCombinatorSet || sel.combinator === ' ') { - siblingGroup = []; - lastGroup = [siblingGroup]; - - groups.push(lastGroup); - } - if (sel.combinator === '>') { - siblingGroup = []; - lastGroup.push(siblingGroup); + switch (sel.combinator) { + case undefined: + case Combinator.descendant: + siblingGroup = []; + currentGroup = [siblingGroup]; + + groups.push(currentGroup); + break; + case Combinator.child: + siblingGroup = []; + + currentGroup.push(siblingGroup); + break; + case Combinator.adjacent: + case Combinator.sibling: + break; + default: + throw new Error(`Unsupported combinator "${sel.combinator}" for selector ${sel}.`); } this.specificity += sel.specificity; From dce0159f61cac357fc4eb7c54b03d0077a65b583 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Tue, 16 Apr 2024 17:00:13 +0000 Subject: [PATCH 22/31] chore: Renaming selector arrays --- packages/core/ui/styling/css-selector.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 58f8e6567c..9677dd74b4 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -489,8 +489,9 @@ export class Selector extends SelectorCore { constructor(public selectors: SimpleSelector[]) { super(); - let siblingGroup: SimpleSelector[]; - let currentGroup: SimpleSelector[][]; + + let siblingsToGroup: SimpleSelector[]; + let childrenToGroup: SimpleSelector[][]; const groups: SimpleSelector[][][] = []; this.specificity = 0; @@ -502,15 +503,15 @@ export class Selector extends SelectorCore { switch (sel.combinator) { case undefined: case Combinator.descendant: - siblingGroup = []; - currentGroup = [siblingGroup]; + siblingsToGroup = []; + childrenToGroup = [siblingsToGroup]; - groups.push(currentGroup); + groups.push(childrenToGroup); break; case Combinator.child: - siblingGroup = []; + siblingsToGroup = []; - currentGroup.push(siblingGroup); + childrenToGroup.push(siblingsToGroup); break; case Combinator.adjacent: case Combinator.sibling: @@ -525,10 +526,10 @@ export class Selector extends SelectorCore { this.dynamic = true; } - siblingGroup.push(sel); + siblingsToGroup.push(sel); } - this.groups = groups.map((g) => new Selector.ChildGroup(g.map((sg) => new Selector.SiblingGroup(sg)))); + this.groups = groups.map((cg) => new Selector.ChildGroup(cg.map((sg) => new Selector.SiblingGroup(sg)))); this.last = selectors[selectors.length - 1]; } From 59b4c97aaae1563c35ca47bccd40c3840a41dc84 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 20 Apr 2024 10:44:33 +0000 Subject: [PATCH 23/31] perf: Reduce the number of instances and arrays for complex selectors --- packages/core/ui/styling/css-selector.ts | 106 ++++++++++++----------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts index 9677dd74b4..1fea5a487f 100644 --- a/packages/core/ui/styling/css-selector.ts +++ b/packages/core/ui/styling/css-selector.ts @@ -166,24 +166,29 @@ function FunctionalPseudoClassProperties(specificity: Specificity, rarity: Rarit }; } +export abstract class SelectorBase { + /** + * Dynamic selectors depend on attributes and pseudo classes. + */ + public dynamic: boolean; + public abstract match(node: Node): boolean; + public abstract mayMatch(node: Node): boolean; + public abstract trackChanges(node: Node, map: ChangeAccumulator): void; +} + @SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static) -export abstract class SelectorCore { +export abstract class SelectorCore extends SelectorBase { public pos: number; public specificity: number; public rarity: Rarity; public combinator: Combinator; public ruleset: RuleSet; - /** - * Dynamic selectors depend on attributes and pseudo classes. - */ - public dynamic: boolean; - public abstract match(node: Node): boolean; + /** * If the selector is static returns if it matches the node. * If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state. */ public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean; - public abstract trackChanges(node: Node, map: ChangeAccumulator): void; public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); } @@ -382,20 +387,20 @@ export class PseudoClassSelector extends SimpleSelector { } export abstract class FunctionalPseudoClassSelector extends PseudoClassSelector { - protected selectors: Array; + protected selectors: Array; protected selectorListType?: PseudoClassSelectorList; constructor(cssPseudoClass: string, dataType: CSSWhatDataType) { super(cssPseudoClass); - const selectors: Array = []; + const selectors: Array = []; const needsHighestSpecificity: boolean = this.specificity === Specificity.SelectorListHighest; let specificity: number = 0; if (Array.isArray(dataType)) { for (const asts of dataType) { - const selector: SimpleSelector | SimpleSelectorSequence | Selector = createSelectorFromAst(asts); + const selector: SimpleSelector | SimpleSelectorSequence | ComplexSelector = createSelectorFromAst(asts); if (selector instanceof InvalidSelector) { // Only forgiving selector list can ignore invalid selectors @@ -459,10 +464,11 @@ export class WhereFunctionalPseudoClassSelector extends FunctionalPseudoClassSel export class SimpleSelectorSequence extends SimpleSelector { private head: SimpleSelector; + constructor(public selectors: SimpleSelector[]) { super(); this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0); - this.head = this.selectors.reduce((prev, curr) => (!prev || curr.rarity > prev.rarity ? curr : prev), null); + this.head = selectors.reduce((prev, curr) => (!prev || curr.rarity > prev.rarity ? curr : prev), null); this.dynamic = selectors.some((sel) => sel.dynamic); } public toString(): string { @@ -482,7 +488,7 @@ export class SimpleSelectorSequence extends SimpleSelector { } } -export class Selector extends SelectorCore { +export class ComplexSelector extends SelectorCore { // Grouped by ancestor combinators, then by child combinators. private groups: Selector.ChildGroup[]; private last: SelectorCore; @@ -491,7 +497,7 @@ export class Selector extends SelectorCore { super(); let siblingsToGroup: SimpleSelector[]; - let childrenToGroup: SimpleSelector[][]; + let currentGroup: SimpleSelector[][]; const groups: SimpleSelector[][][] = []; this.specificity = 0; @@ -504,14 +510,14 @@ export class Selector extends SelectorCore { case undefined: case Combinator.descendant: siblingsToGroup = []; - childrenToGroup = [siblingsToGroup]; + currentGroup = [siblingsToGroup]; - groups.push(childrenToGroup); + groups.push(currentGroup); break; case Combinator.child: siblingsToGroup = []; - childrenToGroup.push(siblingsToGroup); + currentGroup.push(siblingsToGroup); break; case Combinator.adjacent: case Combinator.sibling: @@ -529,7 +535,7 @@ export class Selector extends SelectorCore { siblingsToGroup.push(sel); } - this.groups = groups.map((cg) => new Selector.ChildGroup(cg.map((sg) => new Selector.SiblingGroup(sg)))); + this.groups = groups.map((g) => new Selector.ChildGroup(g.map((selectors) => (selectors.length > 1 ? new Selector.SiblingGroup(selectors) : selectors[0])))); this.last = selectors[selectors.length - 1]; } @@ -540,13 +546,13 @@ export class Selector extends SelectorCore { public match(node: Node): boolean { return this.groups.every((group, i) => { if (i === 0) { - node = group.match(node); + node = group.getMatchingNode(node, true); return !!node; } else { let ancestor = node; while ((ancestor = ancestor.parent ?? ancestor._modalParent)) { - if ((node = group.match(ancestor))) { + if ((node = group.getMatchingNode(ancestor, true))) { return true; } } @@ -556,6 +562,10 @@ export class Selector extends SelectorCore { }); } + public mayMatch(node: Node): boolean { + return false; + } + public trackChanges(node: Node, map: ChangeAccumulator): void { this.selectors.forEach((sel) => sel.trackChanges(node, map)); } @@ -572,7 +582,7 @@ export class Selector extends SelectorCore { const bounds: Selector.Bound[] = []; const mayMatch = this.groups.every((group, i) => { if (i === 0) { - const nextNode = group.mayMatch(node); + const nextNode = group.getMatchingNode(node, false); bounds.push({ left: node, right: node }); node = nextNode; @@ -580,7 +590,7 @@ export class Selector extends SelectorCore { } else { let ancestor = node; while ((ancestor = ancestor.parent)) { - const nextNode = group.mayMatch(ancestor); + const nextNode = group.getMatchingNode(ancestor, false); if (nextNode) { bounds.push({ left: ancestor, right: null }); node = nextNode; @@ -621,26 +631,24 @@ export class Selector extends SelectorCore { } export namespace Selector { // Non-spec. Selector sequences are grouped by ancestor then by child combinators for easier backtracking. - export abstract class Group { - public dynamic: boolean; - public abstract match(node: Node): Node; - public abstract mayMatch(node: Node): Node; - public abstract trackChanges(node: Node, map: ChangeAccumulator): void; - } - - export class ChildGroup extends Group { - constructor(private selectors: SiblingGroup[]) { + export class ChildGroup extends SelectorBase { + constructor(private selectors: SelectorBase[]) { super(); this.dynamic = selectors.some((sel) => sel.dynamic); } - public match(node: Node): Node { - return this.selectors.every((sel, i) => (node = i === 0 ? node : node.parent) && sel.match(node)) ? node : null; + public getMatchingNode(node: Node, strict: boolean) { + const funcName = strict ? 'match' : 'mayMatch'; + return this.selectors.every((sel, i) => (node = i === 0 ? node : node.parent) && sel[funcName](node)) ? node : null; } - public mayMatch(node: Node): Node { - return this.selectors.every((sel, i) => (node = i === 0 ? node : node.parent) && sel.mayMatch(node)) ? node : null; + public match(node: Node): boolean { + return this.getMatchingNode(node, true) != null; + } + + public mayMatch(node: Node): boolean { + return this.getMatchingNode(node, false) != null; } public trackChanges(node: Node, map: ChangeAccumulator): void { @@ -658,14 +666,14 @@ export namespace Selector { } } - export class SiblingGroup extends Group { + export class SiblingGroup extends SelectorBase { constructor(private selectors: SimpleSelector[]) { super(); this.dynamic = selectors.some((sel) => sel.dynamic); } - public match(node: Node): Node { + public match(node: Node): boolean { return this.selectors.every((sel, i) => { if (i === 0) { return node && sel.match(node); @@ -685,12 +693,10 @@ export namespace Selector { }); return isMatching; - }) - ? node - : null; + }); } - public mayMatch(node: Node): Node { + public mayMatch(node: Node): boolean { return this.selectors.every((sel, i) => { if (i === 0) { return node && sel.mayMatch(node); @@ -710,15 +716,15 @@ export namespace Selector { }); return isMatching; - }) - ? node - : null; + }); } public trackChanges(node: Node, map: ChangeAccumulator): void { this.selectors.forEach((sel, i) => { if (i === 0) { - node && sel.trackChanges(node, map); + if (node) { + sel.trackChanges(node, map); + } } else { if (sel.combinator === Combinator.adjacent) { node = getNodePreviousDirectSibling(node); @@ -738,7 +744,9 @@ export namespace Selector { return !isMatching; }); - matchingSibling && sel.trackChanges(matchingSibling, map); + if (matchingSibling) { + sel.trackChanges(matchingSibling, map); + } } } }); @@ -844,8 +852,8 @@ function createSimpleSelectorSequenceFromAst(asts: CSSWhatSelector[]): SimpleSel return new SimpleSelectorSequence(sequenceSelectors); } -function createSelectorFromAst(asts: Array): SimpleSelector | SimpleSelectorSequence | Selector { - let result: SimpleSelector | SimpleSelectorSequence | Selector; +function createSelectorFromAst(asts: Array): SimpleSelector | SimpleSelectorSequence | ComplexSelector { + let result: SimpleSelector | SimpleSelectorSequence | ComplexSelector; if (asts.length === 0) { return new InvalidSelector(new Error('Empty selector.')); @@ -893,13 +901,13 @@ function createSelectorFromAst(asts: Array): SimpleSelector | S simpleSelectorSequences.push(selector); } - return new Selector(simpleSelectorSequences); + return new ComplexSelector(simpleSelectorSequences); } return createSimpleSelectorSequenceFromAst(sequenceAsts); } -export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector { +export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | ComplexSelector { try { const result = convertToCSSWhatSelector(sel); if (!result?.length) { From f965bfbe6810e170e88917d9598ef518ddd9d50f Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 20 Apr 2024 11:13:00 +0000 Subject: [PATCH 24/31] test: Added UI sample for general sibling selector --- apps/ui/src/css/combinators-page.css | 23 +++++++++++++++++++ apps/ui/src/css/combinators-page.xml | 33 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/apps/ui/src/css/combinators-page.css b/apps/ui/src/css/combinators-page.css index dd7b757965..5a51daac9a 100644 --- a/apps/ui/src/css/combinators-page.css +++ b/apps/ui/src/css/combinators-page.css @@ -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; +} \ No newline at end of file diff --git a/apps/ui/src/css/combinators-page.xml b/apps/ui/src/css/combinators-page.xml index b7ee5b3676..dc27cb4e10 100644 --- a/apps/ui/src/css/combinators-page.xml +++ b/apps/ui/src/css/combinators-page.xml @@ -61,6 +61,39 @@