Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions nativescript-core/ui/styling/css-selector/css-selector.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ export interface Node {
parent?: Node;

id?: string;
nodeName?: string;
cssType?: string;
cssClasses?: Set<string>;
cssPseudoClasses?: Set<string>;
getChildIndex?(node: Node): number
getChildAt?(index: number): Node
getChildIndex?(node: Node): number;
getChildAt?(index: number): Node;
}

export interface Declaration {
Expand Down
191 changes: 102 additions & 89 deletions nativescript-core/ui/styling/css-selector/css-selector.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { Node, Declaration, Changes, ChangeMap } from ".";
import { isNullOrUndefined } from "../../../utils/types";
import { escapeRegexSymbols } from "../../../utils/utils-common";

import * as cssParser from "../../../css";
import * as parser from "../../../css/parser";

const enum Specificity {
Inline = 0x01000000,
Id = 0x00010000,
Attribute = 0x00000100,
Class = 0x00000100,
PseudoClass = 0x00000100,
Type = 0x00000001,
Universal = 0x00000000,
Invalid = 0x00000000
Inline = 1000,
Id = 100,
Attribute = 10,
Class = 10,
PseudoClass = 10,
Type = 1,
Universal = 0,
Invalid = 0
}

const enum Rarity {
Expand Down Expand Up @@ -71,6 +70,7 @@ function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: b
declare type Combinator = "+" | ">" | "~" | " ";
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export abstract class SelectorCore {
public pos: number;
public specificity: number;
public rarity: Rarity;
public combinator: Combinator;
Expand Down Expand Up @@ -167,43 +167,35 @@ export class AttributeSelector extends SimpleSelector {
this.match = node => false;
}

let escapedValue = escapeRegexSymbols(value);
let regexp: RegExp = null;
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;
}
regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
break;
case "|=": // DashMatch
regexp = new RegExp("^" + escapedValue + "(-|$)");
break;
}
this.match = node => {
const attr = node[attribute] + "";

if (regexp) {
this.match = node => regexp.test(node[attribute] + "");
if (test === "=") { // Equals
return attr === value;
}

return;
} else {
this.match = node => false;
if (test === "^=") { // PrefixMatch
return attr.startsWith(value);
}

return;
}
if (test === "$=") { // SuffixMatch
return attr.endsWith(value);
}

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 match(node: Node): boolean { return false; }
Expand Down Expand Up @@ -252,7 +244,13 @@ export class Selector extends SelectorCore {
let siblingGroup: SimpleSelector[];
let lastGroup: 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) {
throw new Error(`Unsupported combinator "${sel.combinator}".`);
}
Expand All @@ -262,16 +260,22 @@ export class Selector extends SelectorCore {
if (sel.combinator === ">") {
lastGroup.push(siblingGroup = []);
}

this.specificity += sel.specificity;

if (sel.dynamic) {
this.dynamic = true;
}

siblingGroup.push(sel);
});
}

this.groups = groups.map(g =>
new Selector.ChildGroup(g.map(sg =>
new Selector.SiblingGroup(sg)
))
);
this.last = selectors[0];
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
this.dynamic = selectors.some(sel => sel.dynamic);
this.last = selectors[selectors.length - 1];
}

public toString(): string { return this.selectors.join(""); }
Expand Down Expand Up @@ -364,15 +368,15 @@ export namespace Selector {
}

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 {
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) {
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 {
Expand All @@ -383,15 +387,15 @@ export namespace Selector {
}

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 {
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) {
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 {
Expand All @@ -412,9 +416,8 @@ export function fromAstNodes(astRules: cssParser.Node[]): RuleSet[] {
return (<cssParser.Rule[]>astRules.filter(isRule)).map(rule => {
let declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
let selectors = rule.selectors.map(createSelector);
let ruleset = new RuleSet(selectors, declarations);

return ruleset;
return new RuleSet(selectors, declarations);
});
}

Expand All @@ -423,13 +426,28 @@ function createDeclaration(decl: cssParser.Declaration): any {
}

function createSimpleSelectorFromAst(ast: parser.SimpleSelector): SimpleSelector {
switch (ast.type) {
case "*": return new UniversalSelector();
case "#": return new IdSelector(ast.identifier);
case "": return new TypeSelector(ast.identifier.replace(/-/, "").toLowerCase());
case ".": return new ClassSelector(ast.identifier);
case ":": return new PseudoClassSelector(ast.identifier);
case "[]": return ast.test ? new AttributeSelector(ast.property, ast.test, ast.value) : new AttributeSelector(ast.property);
if (ast.type === ".") {
return new ClassSelector(ast.identifier);
}

if (ast.type === "") {
return new TypeSelector(ast.identifier.replace("-", "").toLowerCase());
}

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();
}
}

Expand Down Expand Up @@ -485,18 +503,14 @@ function isDeclaration(node: cssParser.Node): node is cssParser.Declaration {
return node.type === "declaration";
}

interface SelectorInDocument {
pos: number;
sel: SelectorCore;
}
interface SelectorMap {
[key: string]: SelectorInDocument[];
[key: string]: SelectorCore[];
}
export class SelectorsMap<T extends Node> implements LookupSorter {
private id: SelectorMap = {};
private class: SelectorMap = {};
private type: SelectorMap = {};
private universal: SelectorInDocument[] = [];
private universal: SelectorCore[] = [];

private position = 0;

Expand All @@ -505,24 +519,23 @@ export class SelectorsMap<T extends Node> implements LookupSorter {
}

query(node: T): SelectorsMatch<T> {
let selectorClasses = [
const selectorsMatch = new SelectorsMatch<T>();
const { cssClasses, id, cssType } = node;
const selectorClasses = [
this.universal,
this.id[node.id],
this.type[node.cssType]
this.id[id],
this.type[cssType]
];
if (node.cssClasses) {
node.cssClasses.forEach(c => selectorClasses.push(this.class[c]));

if (cssClasses && cssClasses.size) {
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>();
const selectors = selectorClasses.reduce((cur, next) => cur.concat(next || []), []);

selectorsMatch.selectors = selectors
.filter(sel => sel.sel.accumulateChanges(node, selectorsMatch))
.sort((a, b) => a.sel.specificity - b.sel.specificity || a.pos - b.pos)
.map(docSel => docSel.sel);
.filter(sel => sel.accumulateChanges(node, selectorsMatch))
.sort((a, b) => a.specificity - b.specificity || a.pos - b.pos);

return selectorsMatch;
}
Expand All @@ -537,17 +550,17 @@ export class SelectorsMap<T extends Node> implements LookupSorter {
sortAsUniversal(sel: SelectorCore): void { this.universal.push(this.makeDocSelector(sel)); }

private addToMap(map: SelectorMap, head: string, sel: SelectorCore): void {
this.position++;
let list = map[head];
if (list) {
list.push(this.makeDocSelector(sel));
} else {
map[head] = [this.makeDocSelector(sel)];
if (!map[head]) {
map[head] = [];
}

map[head].push(this.makeDocSelector(sel));
}

private makeDocSelector(sel: SelectorCore): SelectorInDocument {
return { sel, pos: this.position++ };
private makeDocSelector(sel: SelectorCore): SelectorCore {
sel.pos = this.position++;

return sel;
}
}

Expand Down
2 changes: 1 addition & 1 deletion nativescript-core/ui/styling/style-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ export class StyleScope {
}

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._selectors = new SelectorsMap(this._mergedCssSelectors);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeS
const TerserPlugin = require("terser-webpack-plugin");
const hashSalt = Date.now().toString();

const ANDROID_MAX_CYCLES = 66;
const ANDROID_MAX_CYCLES = 65;
const IOS_MAX_CYCLES = 32;
let numCyclesDetected = 0;

Expand Down