Skip to content
Closed
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
90 changes: 67 additions & 23 deletions modules/angular2/src/core/compiler/selector.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper, BaseException} from 'angular2/src/facade/lang';

const _EMPTY_ATTR_VALUE = '';

// TODO: Can't use `const` here as
// in Dart this is not transpiled into `final` yet...
var _SELECTOR_REGEXP =
RegExpWrapper.create('^([-\\w]+)|' + // "tag"
RegExpWrapper.create('(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class"
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])'); // "[name]", "[name=value]" or "[name*=value]"

Expand All @@ -19,28 +20,43 @@ export class CssSelector {
element:string;
classNames:List;
attrs:List;
static parse(selector:string):CssSelector {
notSelector: CssSelector;
static parse(selector:string): CssSelector {
var cssSelector = new CssSelector();
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
var match;
var current = cssSelector;
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
if (isPresent(match[1])) {
cssSelector.setElement(match[1]);
if (isPresent(cssSelector.notSelector)) {
throw new BaseException('Nesting :not is not allowed in a selector');
}
current.notSelector = new CssSelector();
current = current.notSelector;
}
if (isPresent(match[2])) {
cssSelector.addClassName(match[2]);
current.setElement(match[2]);
}
if (isPresent(match[3])) {
cssSelector.addAttribute(match[3], match[4]);
current.addClassName(match[3]);
}
if (isPresent(match[4])) {
current.addAttribute(match[4], match[5]);
}
}
if (isPresent(cssSelector.notSelector) && isBlank(cssSelector.element)
&& ListWrapper.isEmpty(cssSelector.classNames) && ListWrapper.isEmpty(cssSelector.attrs)) {
cssSelector.element = "*";
}

return cssSelector;
}

constructor() {
this.element = null;
this.classNames = ListWrapper.create();
this.attrs = ListWrapper.create();
this.notSelector = null;
}

setElement(element:string = null) {
Expand Down Expand Up @@ -85,6 +101,9 @@ export class CssSelector {
res += ']';
}
}
if (isPresent(this.notSelector)) {
res += ":not(" + this.notSelector.toString() + ")";
}
return res;
}
}
Expand Down Expand Up @@ -188,20 +207,22 @@ export class SelectorMatcher {
* whose css selector is contained in the given css selector.
* @param cssSelector A css selector
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
* @return boolean true if a match was found
*/
match(cssSelector:CssSelector, matchedCallback:Function) {
match(cssSelector:CssSelector, matchedCallback:Function):boolean {
var result = false;
var element = cssSelector.element;
var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs;

this._matchTerminal(this._elementMap, element, matchedCallback);
this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback);
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result;

if (isPresent(classNames)) {
for (var index = 0; index<classNames.length; index++) {
var className = classNames[index];
this._matchTerminal(this._classMap, className, matchedCallback);
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback);
result = this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
result = this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) || result;
}
}

Expand All @@ -212,54 +233,77 @@ export class SelectorMatcher {

var valuesMap = MapWrapper.get(this._attrValueMap, attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
this._matchTerminal(valuesMap, _EMPTY_ATTR_VALUE, matchedCallback);
result = this._matchTerminal(valuesMap, _EMPTY_ATTR_VALUE, cssSelector, matchedCallback) || result;
}
this._matchTerminal(valuesMap, attrValue, matchedCallback);
result = this._matchTerminal(valuesMap, attrValue, cssSelector, matchedCallback) || result;

valuesMap = MapWrapper.get(this._attrValuePartialMap, attrName)
this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback);
result = this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback) || result;
}
}
return result;
}

_matchTerminal(map:Map<string,string> = null, name, matchedCallback) {
_matchTerminal(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
if (isBlank(map) || isBlank(name)) {
return;
return false;
}

var selectables = MapWrapper.get(map, name);
var starSelectables = MapWrapper.get(map, "*");
if (isPresent(starSelectables)) {
selectables = ListWrapper.concat(selectables, starSelectables);
}
var selectables = MapWrapper.get(map, name)
if (isBlank(selectables)) {
return;
return false;
}
var selectable;
var result = false;
for (var index=0; index<selectables.length; index++) {
selectable = selectables[index];
matchedCallback(selectable.selector, selectable.cbContext);
result = selectable.finalize(cssSelector, matchedCallback) || result;
}
return result;
}

_matchPartial(map:Map<string,string> = null, name, cssSelector, matchedCallback) {
_matchPartial(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
if (isBlank(map) || isBlank(name)) {
return;
return false;
}
var nestedSelector = MapWrapper.get(map, name)
if (isBlank(nestedSelector)) {
return;
return false;
}
// TODO(perf): get rid of recursion and measure again
// TODO(perf): don't pass the whole selector into the recursion,
// but only the not processed parts
nestedSelector.match(cssSelector, matchedCallback);
return nestedSelector.match(cssSelector, matchedCallback);
}
}


// Store context to pass back selector and context when a selector is matched
class SelectorContext {
selector:CssSelector;
notSelector:CssSelector;
cbContext; // callback context

constructor(selector:CssSelector, cbContext) {
this.selector = selector;
this.notSelector = selector.notSelector;
this.cbContext = cbContext;
}

finalize(cssSelector: CssSelector, callback) {
var result = true;
if (isPresent(this.notSelector)) {
var notMatcher = new SelectorMatcher();
notMatcher.addSelectable(this.notSelector, null);
result = !notMatcher.match(cssSelector, null);
}
if (result && isPresent(callback)) {
callback(this.selector, this.cbContext);
}
return result;
}
}
92 changes: 72 additions & 20 deletions modules/angular2/test/core/compiler/selector_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,44 @@ export function main() {
it('should select by element name case insensitive', () => {
matcher.addSelectable(s1 = CssSelector.parse('someTag'), 1);

matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector);
expect(matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('SOMETAG'), selectableCollector);
expect(matcher.match(CssSelector.parse('SOMETAG'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1]);
});

it('should select by class name case insensitive', () => {
matcher.addSelectable(s1 = CssSelector.parse('.someClass'), 1);
matcher.addSelectable(s2 = CssSelector.parse('.someClass.class2'), 2);

matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector);
expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector);
expect(matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1]);

reset();
matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector);
expect(matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1,s2,2]);
});

it('should select by attr name case insensitive independent of the value', () => {
matcher.addSelectable(s1 = CssSelector.parse('[someAttr]'), 1);
matcher.addSelectable(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);

matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector);
expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector);
expect(matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1]);

reset();
matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector);
expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1]);

reset();
matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector);
expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1,s2,2]);
});

Expand All @@ -80,29 +80,29 @@ export function main() {
it('should select by attr name and value case insensitive', () => {
matcher.addSelectable(s1 = CssSelector.parse('[someAttr=someValue]'), 1);

matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector);
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector);
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1]);
});

it('should select by element name, class name and attribute name with value', () => {
matcher.addSelectable(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1);

matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector);
expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector);
expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector);
expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector);
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);

matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector);
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1]);
});

Expand All @@ -112,21 +112,42 @@ export function main() {
matcher.addSelectable(s3 = CssSelector.parse('.class1.class2'), 3);
matcher.addSelectable(s4 = CssSelector.parse('.class2.class1'), 4);

matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector);
expect(matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1,s2,2]);

reset();
matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector);
expect(matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1,s2,2]);

reset();
matcher.match(CssSelector.parse('.class1.class2'), selectableCollector);
expect(matcher.match(CssSelector.parse('.class1.class2'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s3,3,s4,4]);

reset();
matcher.match(CssSelector.parse('.class2.class1'), selectableCollector);
expect(matcher.match(CssSelector.parse('.class2.class1'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s4,4,s3,3]);
});

it('should not select with a matching :not selector', () => {
matcher.addSelectable(CssSelector.parse('p:not(.someClass)'), 1);
matcher.addSelectable(CssSelector.parse('p:not([someAttr])'), 2);
matcher.addSelectable(CssSelector.parse(':not(.someClass)'), 3);
matcher.addSelectable(CssSelector.parse(':not(p)'), 4);
matcher.addSelectable(CssSelector.parse(':not(p[someAttr])'), 5);

expect(matcher.match(CssSelector.parse('p.someClass[someAttr]'), selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
});

it('should select with a non matching :not selector', () => {
matcher.addSelectable(s1 = CssSelector.parse('p:not(.someClass)'), 1);
matcher.addSelectable(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2);
matcher.addSelectable(s3 = CssSelector.parse(':not(.someClass)'), 3);
matcher.addSelectable(s4 = CssSelector.parse(':not(.someOtherClass[someAttr])'), 4);

expect(matcher.match(CssSelector.parse('p[someOtherAttr].someOtherClass'), selectableCollector)).toEqual(true);
expect(matched).toEqual([s1,1,s2,2,s3,3,s4,4]);
});
});

describe('CssSelector.parse', () => {
Expand Down Expand Up @@ -164,5 +185,36 @@ export function main() {

expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]');
});

it('should detect :not', () => {
var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)');
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.attrs.length).toEqual(0);
expect(cssSelector.classNames.length).toEqual(0);

var notSelector = cssSelector.notSelector;
expect(notSelector.element).toEqual(null);
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']);

expect(cssSelector.toString()).toEqual('sometag:not(.someclass[attrname=attrvalue])');
});

it('should detect :not without truthy', () => {
var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)');
expect(cssSelector.element).toEqual("*");

var notSelector = cssSelector.notSelector;
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']);

expect(cssSelector.toString()).toEqual('*:not(.someclass[attrname=attrvalue])');
});

it('should throw when nested :not', () => {
expect(() => {
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')
}).toThrowError('Nesting :not is not allowed in a selector');
});
});
}