mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 19:26:42 +08:00
refactor(css): attribute selectors match web counterparts (#7848)
* Improve CSS selector parsing/matching by 30% - 40% with some JavaScript optimization and excluding ProxyViewContainer from the process Change the specificity to be divisible to 10 * fix: selector match * fix: lint errors * refactor: restore processing of ProxyViewContainer * chore: lower the number of expected cycles * fix: some css selector fixes Co-authored-by: Manol Donev <manol.donev@gmail.com> Co-authored-by: Manol Donev <manoldonev@users.noreply.github.com> Co-authored-by: Vasil Trifonov <v.trifonov@gmail.com>
This commit is contained in:

committed by
GitHub

parent
5c3ba11d95
commit
c9cea472ca
@ -12,11 +12,12 @@ export interface Node {
|
|||||||
parent?: Node;
|
parent?: Node;
|
||||||
|
|
||||||
id?: string;
|
id?: string;
|
||||||
|
nodeName?: string;
|
||||||
cssType?: string;
|
cssType?: string;
|
||||||
cssClasses?: Set<string>;
|
cssClasses?: Set<string>;
|
||||||
cssPseudoClasses?: Set<string>;
|
cssPseudoClasses?: Set<string>;
|
||||||
getChildIndex?(node: Node): number
|
getChildIndex?(node: Node): number;
|
||||||
getChildAt?(index: number): Node
|
getChildAt?(index: number): Node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Declaration {
|
export interface Declaration {
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import { Node, Declaration, Changes, ChangeMap } from ".";
|
import { Node, Declaration, Changes, ChangeMap } from ".";
|
||||||
import { isNullOrUndefined } from "../../../utils/types";
|
import { isNullOrUndefined } from "../../../utils/types";
|
||||||
import { escapeRegexSymbols } from "../../../utils/utils-common";
|
|
||||||
|
|
||||||
import * as cssParser from "../../../css";
|
import * as cssParser from "../../../css";
|
||||||
import * as parser from "../../../css/parser";
|
import * as parser from "../../../css/parser";
|
||||||
|
|
||||||
const enum Specificity {
|
const enum Specificity {
|
||||||
Inline = 0x01000000,
|
Inline = 1000,
|
||||||
Id = 0x00010000,
|
Id = 100,
|
||||||
Attribute = 0x00000100,
|
Attribute = 10,
|
||||||
Class = 0x00000100,
|
Class = 10,
|
||||||
PseudoClass = 0x00000100,
|
PseudoClass = 10,
|
||||||
Type = 0x00000001,
|
Type = 1,
|
||||||
Universal = 0x00000000,
|
Universal = 0,
|
||||||
Invalid = 0x00000000
|
Invalid = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const enum Rarity {
|
const enum Rarity {
|
||||||
@ -71,6 +70,7 @@ function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: b
|
|||||||
declare type Combinator = "+" | ">" | "~" | " ";
|
declare type Combinator = "+" | ">" | "~" | " ";
|
||||||
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
|
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
|
||||||
export abstract class SelectorCore {
|
export abstract class SelectorCore {
|
||||||
|
public pos: number;
|
||||||
public specificity: number;
|
public specificity: number;
|
||||||
public rarity: Rarity;
|
public rarity: Rarity;
|
||||||
public combinator: Combinator;
|
public combinator: Combinator;
|
||||||
@ -167,43 +167,35 @@ export class AttributeSelector extends SimpleSelector {
|
|||||||
this.match = node => false;
|
this.match = node => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let escapedValue = escapeRegexSymbols(value);
|
this.match = node => {
|
||||||
let regexp: RegExp = null;
|
const attr = node[attribute] + "";
|
||||||
switch (test) {
|
|
||||||
case "^=": // PrefixMatch
|
|
||||||
regexp = new RegExp("^" + escapedValue);
|
|
||||||
break;
|
|
||||||
case "$=": // SuffixMatch
|
|
||||||
regexp = new RegExp(escapedValue + "$");
|
|
||||||
break;
|
|
||||||
case "*=": // SubstringMatch
|
|
||||||
regexp = new RegExp(escapedValue);
|
|
||||||
break;
|
|
||||||
case "=": // Equals
|
|
||||||
regexp = new RegExp("^" + escapedValue + "$");
|
|
||||||
break;
|
|
||||||
case "~=": // Includes
|
|
||||||
if (/\s/.test(value)) {
|
|
||||||
this.match = node => false;
|
|
||||||
|
|
||||||
return;
|
if (test === "=") { // Equals
|
||||||
}
|
return attr === value;
|
||||||
regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
|
}
|
||||||
break;
|
|
||||||
case "|=": // DashMatch
|
|
||||||
regexp = new RegExp("^" + escapedValue + "(-|$)");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regexp) {
|
if (test === "^=") { // PrefixMatch
|
||||||
this.match = node => regexp.test(node[attribute] + "");
|
return attr.startsWith(value);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
if (test === "$=") { // SuffixMatch
|
||||||
} else {
|
return attr.endsWith(value);
|
||||||
this.match = node => false;
|
}
|
||||||
|
|
||||||
return;
|
if (test === "*=") { // SubstringMatch
|
||||||
}
|
return attr.indexOf(value) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (test === "~=") { // Includes
|
||||||
|
const words = attr.split(" ");
|
||||||
|
|
||||||
|
return words && words.indexOf(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 toString(): string { return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ""}]${wrap(this.combinator)}`; }
|
||||||
public match(node: Node): boolean { return false; }
|
public match(node: Node): boolean { return false; }
|
||||||
@ -252,7 +244,13 @@ export class Selector extends SelectorCore {
|
|||||||
let siblingGroup: SimpleSelector[];
|
let siblingGroup: SimpleSelector[];
|
||||||
let lastGroup: SimpleSelector[][];
|
let lastGroup: SimpleSelector[][];
|
||||||
let groups: SimpleSelector[][][] = [];
|
let groups: SimpleSelector[][][] = [];
|
||||||
selectors.reverse().forEach(sel => {
|
|
||||||
|
this.specificity = 0;
|
||||||
|
this.dynamic = false;
|
||||||
|
|
||||||
|
for (let i = selectors.length - 1; i > -1; i--) {
|
||||||
|
const sel = selectors[i];
|
||||||
|
|
||||||
if (supportedCombinator.indexOf(sel.combinator) === -1) {
|
if (supportedCombinator.indexOf(sel.combinator) === -1) {
|
||||||
throw new Error(`Unsupported combinator "${sel.combinator}".`);
|
throw new Error(`Unsupported combinator "${sel.combinator}".`);
|
||||||
}
|
}
|
||||||
@ -262,16 +260,22 @@ export class Selector extends SelectorCore {
|
|||||||
if (sel.combinator === ">") {
|
if (sel.combinator === ">") {
|
||||||
lastGroup.push(siblingGroup = []);
|
lastGroup.push(siblingGroup = []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.specificity += sel.specificity;
|
||||||
|
|
||||||
|
if (sel.dynamic) {
|
||||||
|
this.dynamic = true;
|
||||||
|
}
|
||||||
|
|
||||||
siblingGroup.push(sel);
|
siblingGroup.push(sel);
|
||||||
});
|
}
|
||||||
|
|
||||||
this.groups = groups.map(g =>
|
this.groups = groups.map(g =>
|
||||||
new Selector.ChildGroup(g.map(sg =>
|
new Selector.ChildGroup(g.map(sg =>
|
||||||
new Selector.SiblingGroup(sg)
|
new Selector.SiblingGroup(sg)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
this.last = selectors[0];
|
this.last = selectors[selectors.length - 1];
|
||||||
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
|
||||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string { return this.selectors.join(""); }
|
public toString(): string { return this.selectors.join(""); }
|
||||||
@ -364,15 +368,15 @@ export namespace Selector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public match(node: Node): Node {
|
public match(node: Node): Node {
|
||||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && !!sel.match(node)) ? node : null;
|
return this.selectors.every((sel, i) => (node = (i === 0 ? node : node.parent)) && sel.match(node)) ? node : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public mayMatch(node: Node): Node {
|
public mayMatch(node: Node): Node {
|
||||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && !!sel.mayMatch(node)) ? node : null;
|
return this.selectors.every((sel, i) => (node = (i === 0 ? node : node.parent)) && sel.mayMatch(node)) ? node : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackChanges(node: Node, map: ChangeAccumulator) {
|
public trackChanges(node: Node, map: ChangeAccumulator) {
|
||||||
this.selectors.forEach((sel, i) => (i === 0 ? node : node = node.parent) && sel.trackChanges(node, map));
|
this.selectors.forEach((sel, i) => (node = (i === 0 ? node : node.parent)) && sel.trackChanges(node, map));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class SiblingGroup {
|
export class SiblingGroup {
|
||||||
@ -383,15 +387,15 @@ export namespace Selector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public match(node: Node): Node {
|
public match(node: Node): Node {
|
||||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.match(node)) ? node : null;
|
return this.selectors.every((sel, i) => (node = (i === 0 ? node : getNodeDirectSibling(node))) && sel.match(node)) ? node : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public mayMatch(node: Node): Node {
|
public mayMatch(node: Node): Node {
|
||||||
return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.mayMatch(node)) ? node : null;
|
return this.selectors.every((sel, i) => (node = (i === 0 ? node : getNodeDirectSibling(node))) && sel.mayMatch(node)) ? node : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackChanges(node: Node, map: ChangeAccumulator) {
|
public trackChanges(node: Node, map: ChangeAccumulator) {
|
||||||
this.selectors.forEach((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.trackChanges(node, map));
|
this.selectors.forEach((sel, i) => (node = (i === 0 ? node : getNodeDirectSibling(node))) && sel.trackChanges(node, map));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface Bound {
|
export interface Bound {
|
||||||
@ -412,9 +416,8 @@ export function fromAstNodes(astRules: cssParser.Node[]): RuleSet[] {
|
|||||||
return (<cssParser.Rule[]>astRules.filter(isRule)).map(rule => {
|
return (<cssParser.Rule[]>astRules.filter(isRule)).map(rule => {
|
||||||
let declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
|
let declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
|
||||||
let selectors = rule.selectors.map(createSelector);
|
let selectors = rule.selectors.map(createSelector);
|
||||||
let ruleset = new RuleSet(selectors, declarations);
|
|
||||||
|
|
||||||
return ruleset;
|
return new RuleSet(selectors, declarations);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,13 +426,28 @@ function createDeclaration(decl: cssParser.Declaration): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createSimpleSelectorFromAst(ast: parser.SimpleSelector): SimpleSelector {
|
function createSimpleSelectorFromAst(ast: parser.SimpleSelector): SimpleSelector {
|
||||||
switch (ast.type) {
|
if (ast.type === ".") {
|
||||||
case "*": return new UniversalSelector();
|
return new ClassSelector(ast.identifier);
|
||||||
case "#": return new IdSelector(ast.identifier);
|
}
|
||||||
case "": return new TypeSelector(ast.identifier.replace(/-/, "").toLowerCase());
|
|
||||||
case ".": return new ClassSelector(ast.identifier);
|
if (ast.type === "") {
|
||||||
case ":": return new PseudoClassSelector(ast.identifier);
|
return new TypeSelector(ast.identifier.replace("-", "").toLowerCase());
|
||||||
case "[]": return ast.test ? new AttributeSelector(ast.property, ast.test, ast.value) : new AttributeSelector(ast.property);
|
}
|
||||||
|
|
||||||
|
if (ast.type === "#") {
|
||||||
|
return new IdSelector(ast.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.type === "[]") {
|
||||||
|
return new AttributeSelector(ast.property, ast.test, ast.test && ast.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.type === ":") {
|
||||||
|
return new PseudoClassSelector(ast.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.type === "*") {
|
||||||
|
return new UniversalSelector();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,18 +503,14 @@ function isDeclaration(node: cssParser.Node): node is cssParser.Declaration {
|
|||||||
return node.type === "declaration";
|
return node.type === "declaration";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectorInDocument {
|
|
||||||
pos: number;
|
|
||||||
sel: SelectorCore;
|
|
||||||
}
|
|
||||||
interface SelectorMap {
|
interface SelectorMap {
|
||||||
[key: string]: SelectorInDocument[];
|
[key: string]: SelectorCore[];
|
||||||
}
|
}
|
||||||
export class SelectorsMap<T extends Node> implements LookupSorter {
|
export class SelectorsMap<T extends Node> implements LookupSorter {
|
||||||
private id: SelectorMap = {};
|
private id: SelectorMap = {};
|
||||||
private class: SelectorMap = {};
|
private class: SelectorMap = {};
|
||||||
private type: SelectorMap = {};
|
private type: SelectorMap = {};
|
||||||
private universal: SelectorInDocument[] = [];
|
private universal: SelectorCore[] = [];
|
||||||
|
|
||||||
private position = 0;
|
private position = 0;
|
||||||
|
|
||||||
@ -505,24 +519,23 @@ export class SelectorsMap<T extends Node> implements LookupSorter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query(node: T): SelectorsMatch<T> {
|
query(node: T): SelectorsMatch<T> {
|
||||||
let selectorClasses = [
|
const selectorsMatch = new SelectorsMatch<T>();
|
||||||
|
const { cssClasses, id, cssType } = node;
|
||||||
|
const selectorClasses = [
|
||||||
this.universal,
|
this.universal,
|
||||||
this.id[node.id],
|
this.id[id],
|
||||||
this.type[node.cssType]
|
this.type[cssType]
|
||||||
];
|
];
|
||||||
if (node.cssClasses) {
|
|
||||||
node.cssClasses.forEach(c => selectorClasses.push(this.class[c]));
|
|
||||||
}
|
|
||||||
let selectors = selectorClasses
|
|
||||||
.filter(arr => !!arr)
|
|
||||||
.reduce((cur, next) => cur.concat(next), []);
|
|
||||||
|
|
||||||
let selectorsMatch = new SelectorsMatch<T>();
|
if (cssClasses && cssClasses.size) {
|
||||||
|
cssClasses.forEach(c => selectorClasses.push(this.class[c]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectors = selectorClasses.reduce((cur, next) => cur.concat(next || []), []);
|
||||||
|
|
||||||
selectorsMatch.selectors = selectors
|
selectorsMatch.selectors = selectors
|
||||||
.filter(sel => sel.sel.accumulateChanges(node, selectorsMatch))
|
.filter(sel => sel.accumulateChanges(node, selectorsMatch))
|
||||||
.sort((a, b) => a.sel.specificity - b.sel.specificity || a.pos - b.pos)
|
.sort((a, b) => a.specificity - b.specificity || a.pos - b.pos);
|
||||||
.map(docSel => docSel.sel);
|
|
||||||
|
|
||||||
return selectorsMatch;
|
return selectorsMatch;
|
||||||
}
|
}
|
||||||
@ -537,17 +550,17 @@ export class SelectorsMap<T extends Node> implements LookupSorter {
|
|||||||
sortAsUniversal(sel: SelectorCore): void { this.universal.push(this.makeDocSelector(sel)); }
|
sortAsUniversal(sel: SelectorCore): void { this.universal.push(this.makeDocSelector(sel)); }
|
||||||
|
|
||||||
private addToMap(map: SelectorMap, head: string, sel: SelectorCore): void {
|
private addToMap(map: SelectorMap, head: string, sel: SelectorCore): void {
|
||||||
this.position++;
|
if (!map[head]) {
|
||||||
let list = map[head];
|
map[head] = [];
|
||||||
if (list) {
|
|
||||||
list.push(this.makeDocSelector(sel));
|
|
||||||
} else {
|
|
||||||
map[head] = [this.makeDocSelector(sel)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map[head].push(this.makeDocSelector(sel));
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeDocSelector(sel: SelectorCore): SelectorInDocument {
|
private makeDocSelector(sel: SelectorCore): SelectorCore {
|
||||||
return { sel, pos: this.position++ };
|
sel.pos = this.position++;
|
||||||
|
|
||||||
|
return sel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -801,7 +801,7 @@ export class StyleScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toMerge.length > 0) {
|
if (toMerge.length > 0) {
|
||||||
this._mergedCssSelectors = toMerge.filter(m => !!m).reduce((merged, next) => merged.concat(next), []);
|
this._mergedCssSelectors = toMerge.reduce((merged, next) => merged.concat(next || []), []);
|
||||||
this._applyKeyframesOnSelectors();
|
this._applyKeyframesOnSelectors();
|
||||||
this._selectors = new SelectorsMap(this._mergedCssSelectors);
|
this._selectors = new SelectorsMap(this._mergedCssSelectors);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeS
|
|||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const hashSalt = Date.now().toString();
|
const hashSalt = Date.now().toString();
|
||||||
|
|
||||||
const ANDROID_MAX_CYCLES = 66;
|
const ANDROID_MAX_CYCLES = 65;
|
||||||
const IOS_MAX_CYCLES = 32;
|
const IOS_MAX_CYCLES = 32;
|
||||||
let numCyclesDetected = 0;
|
let numCyclesDetected = 0;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user