feat(core): css-what parser for CSS selectors + support for :not(), :is(), and :where() Level 4 and ~ (#10514)

This commit is contained in:
Dimitris-Rafail Katsampas
2024-06-28 23:57:29 +03:00
committed by GitHub
parent 88a047254b
commit 2fb4f23670
10 changed files with 750 additions and 449 deletions

View File

@@ -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]] });
(<AttributeSelectorTest[]>['=', '^=', '$=', '*=', '=', '~=', '|=']).forEach((attributeTest) => {
test(parseSelector, `[src ${attributeTest} "val"]`, { start: 0, end: 12 + attributeTest.length, value: [[[{ type: '[]', property: 'src', test: attributeTest, value: 'val' }], undefined]] });
});
test(parseSelector, 'listview > .image', {
start: 0,
end: 17,
value: [
[[{ type: '', identifier: 'listview' }], '>'],
[[{ type: '.', identifier: 'image' }], undefined],
],
});
test(parseSelector, 'listview .image', {
start: 0,
end: 16,
value: [
[[{ type: '', identifier: 'listview' }], ' '],
[[{ type: '.', identifier: 'image' }], undefined],
],
});
test(parseSelector, 'button:hover', {
start: 0,
end: 12,
value: [
[
[
{ type: '', identifier: 'button' },
{ type: ':', identifier: 'hover' },
],
undefined,
],
],
});
test(parseSelector, 'listview>:selected image.product', {
start: 0,
end: 32,
value: [
[[{ type: '', identifier: 'listview' }], '>'],
[[{ type: ':', identifier: 'selected' }], ' '],
[
[
{ type: '', identifier: 'image' },
{ type: '.', identifier: 'product' },
],
undefined,
],
],
});
test(parseSelector, 'button[testAttr]', {
start: 0,
end: 16,
value: [
[
[
{ type: '', identifier: 'button' },
{ type: '[]', property: 'testAttr' },
],
undefined,
],
],
});
test(parseSelector, 'button#login[user][pass]:focused:hovered', {
start: 0,
end: 40,
value: [
[
[
{ type: '', identifier: 'button' },
{ type: '#', identifier: 'login' },
{ type: '[]', property: 'user' },
{ type: '[]', property: 'pass' },
{ type: ':', identifier: 'focused' },
{ type: ':', identifier: 'hovered' },
],
undefined,
],
],
});
});
describe('css3', () => {
let themeCoreLightIos: string;
let whatIsNewIos: string;
@@ -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);

View File

@@ -612,161 +612,3 @@ export function parseBackground(text: string, start = 0): Parsed<Background> {
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<UniversalSelector> {
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<TypeSelector | ClassSelector | IdSelector | PseudoClassSelector> {
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 = <TypeSelector | ClassSelector | IdSelector | PseudoClassSelector>{ 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<AttributeSelector> {
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 = <AttributeSelectorTest>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<SimpleSelector> {
return parseUniversalSelector(text, start) || parseSimpleIdentifierSelector(text, start) || parseAttributeSelector(text, start);
}
export function parseSimpleSelectorSequence(text: string, start: number): Parsed<SimpleSelector[]> {
let simpleSelector = parseSimpleSelector(text, start);
if (!simpleSelector) {
return null;
}
let end = simpleSelector.end;
const value = <SimpleSelectorSequence>[];
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<Combinator> {
combinatorRegEx.lastIndex = start;
const result = combinatorRegEx.exec(text);
if (!result) {
return null;
}
const end = combinatorRegEx.lastIndex;
const value = <Combinator>result[1] || ' ';
return { start, end, value };
}
const whiteSpaceRegEx = /\s*/gy;
export function parseSelector(text: string, start = 0): Parsed<Selector> {
let end = start;
whiteSpaceRegEx.lastIndex = end;
const leadingWhiteSpace = whiteSpaceRegEx.exec(text);
if (leadingWhiteSpace) {
end = whiteSpaceRegEx.lastIndex;
}
const value = <Selector>[];
let combinator: Parsed<Combinator>;
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 };
}