fix(selector): support multiple `:not` clauses

Fixes #2243
This commit is contained in:
Tobias Bosch 2015-06-01 14:24:19 -07:00
parent c8d83dba7d
commit 62a95823e0
2 changed files with 42 additions and 22 deletions

View File

@ -17,7 +17,7 @@ var _SELECTOR_REGEXP = RegExpWrapper.create(
'([-\\w]+)|' + // "tag" '([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class" '(?:\\.([-\\w]+))|' + // ".class"
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]" '(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]"
'(?:\\))|' + // ")" '(\\))|' + // ")"
'(\\s*,\\s*)'); // "," '(\\s*,\\s*)'); // ","
/** /**
@ -29,11 +29,11 @@ export class CssSelector {
element: string; element: string;
classNames: List<string>; classNames: List<string>;
attrs: List<string>; attrs: List<string>;
notSelector: CssSelector; notSelectors: List<CssSelector>;
static parse(selector: string): List<CssSelector> { static parse(selector: string): List<CssSelector> {
var results = ListWrapper.create(); var results = ListWrapper.create();
var _addResult = (res, cssSel) => { var _addResult = (res, cssSel) => {
if (isPresent(cssSel.notSelector) && isBlank(cssSel.element) && if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) &&
ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) { ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
cssSel.element = "*"; cssSel.element = "*";
} }
@ -43,13 +43,15 @@ export class CssSelector {
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector); var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
var match; var match;
var current = cssSelector; var current = cssSelector;
var inNot = false;
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) { while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
if (isPresent(match[1])) { if (isPresent(match[1])) {
if (isPresent(cssSelector.notSelector)) { if (inNot) {
throw new BaseException('Nesting :not is not allowed in a selector'); throw new BaseException('Nesting :not is not allowed in a selector');
} }
current.notSelector = new CssSelector(); inNot = true;
current = current.notSelector; current = new CssSelector();
ListWrapper.push(cssSelector.notSelectors, current);
} }
if (isPresent(match[2])) { if (isPresent(match[2])) {
current.setElement(match[2]); current.setElement(match[2]);
@ -61,6 +63,13 @@ export class CssSelector {
current.addAttribute(match[4], match[5]); current.addAttribute(match[4], match[5]);
} }
if (isPresent(match[6])) { if (isPresent(match[6])) {
inNot = false;
current = cssSelector;
}
if (isPresent(match[7])) {
if (inNot) {
throw new BaseException('Multiple selectors in :not are not supported');
}
_addResult(results, cssSelector); _addResult(results, cssSelector);
cssSelector = current = new CssSelector(); cssSelector = current = new CssSelector();
} }
@ -73,12 +82,12 @@ export class CssSelector {
this.element = null; this.element = null;
this.classNames = ListWrapper.create(); this.classNames = ListWrapper.create();
this.attrs = ListWrapper.create(); this.attrs = ListWrapper.create();
this.notSelector = null; this.notSelectors = ListWrapper.create();
} }
isElementSelector(): boolean { isElementSelector(): boolean {
return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) && return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) &&
ListWrapper.isEmpty(this.attrs) && isBlank(this.notSelector); ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0;
} }
setElement(element: string = null) { setElement(element: string = null) {
@ -121,9 +130,8 @@ export class CssSelector {
res += ']'; res += ']';
} }
} }
if (isPresent(this.notSelector)) { ListWrapper.forEach(this.notSelectors,
res += ":not(" + this.notSelector.toString() + ")"; (notSelector) => { res += ":not(" + notSelector.toString() + ")"; });
}
return res; return res;
} }
} }
@ -133,9 +141,9 @@ export class CssSelector {
* are contained in a given CssSelector. * are contained in a given CssSelector.
*/ */
export class SelectorMatcher { export class SelectorMatcher {
static createNotMatcher(notSelector: CssSelector) { static createNotMatcher(notSelectors: List<CssSelector>) {
var notMatcher = new SelectorMatcher(); var notMatcher = new SelectorMatcher();
notMatcher._addSelectable(notSelector, null, null); notMatcher.addSelectables(notSelectors, null);
return notMatcher; return notMatcher;
} }
@ -357,22 +365,22 @@ class SelectorListContext {
// Store context to pass back selector and context when a selector is matched // Store context to pass back selector and context when a selector is matched
class SelectorContext { class SelectorContext {
selector: CssSelector; selector: CssSelector;
notSelector: CssSelector; notSelectors: List<CssSelector>;
cbContext; // callback context cbContext; // callback context
listContext: SelectorListContext; listContext: SelectorListContext;
constructor(selector: CssSelector, cbContext: any, listContext: SelectorListContext) { constructor(selector: CssSelector, cbContext: any, listContext: SelectorListContext) {
this.selector = selector; this.selector = selector;
this.notSelector = selector.notSelector; this.notSelectors = selector.notSelectors;
this.cbContext = cbContext; this.cbContext = cbContext;
this.listContext = listContext; this.listContext = listContext;
} }
finalize(cssSelector: CssSelector, callback /*: (CssSelector, any) => void*/) { finalize(cssSelector: CssSelector, callback /*: (CssSelector, any) => void*/) {
var result = true; var result = true;
if (isPresent(this.notSelector) && if (this.notSelectors.length > 0 &&
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) { (isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelector); var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
result = !notMatcher.match(cssSelector, null); result = !notMatcher.match(cssSelector, null);
} }
if (result && isPresent(callback) && if (result && isPresent(callback) &&

View File

@ -184,6 +184,13 @@ export function main() {
expect(matched).toEqual([s1[0], 1, s2[0], 2, s3[0], 3, s4[0], 4]); expect(matched).toEqual([s1[0], 1, s2[0], 2, s3[0], 3, s4[0], 4]);
}); });
it('should match with multiple :not selectors', () => {
matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1);
expect(matcher.match(CssSelector.parse('div[a]')[0], selectableCollector)).toBe(false);
expect(matcher.match(CssSelector.parse('div[b]')[0], selectableCollector)).toBe(false);
expect(matcher.match(CssSelector.parse('div[c]')[0], selectableCollector)).toBe(true);
});
it('should select with one match in a list', () => { it('should select with one match in a list', () => {
matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1); matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1);
@ -256,7 +263,7 @@ export function main() {
expect(cssSelector.attrs.length).toEqual(0); expect(cssSelector.attrs.length).toEqual(0);
expect(cssSelector.classNames.length).toEqual(0); expect(cssSelector.classNames.length).toEqual(0);
var notSelector = cssSelector.notSelector; var notSelector = cssSelector.notSelectors[0];
expect(notSelector.element).toEqual(null); expect(notSelector.element).toEqual(null);
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']); expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']); expect(notSelector.classNames).toEqual(['someclass']);
@ -268,7 +275,7 @@ export function main() {
var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0]; var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0];
expect(cssSelector.element).toEqual("*"); expect(cssSelector.element).toEqual("*");
var notSelector = cssSelector.notSelector; var notSelector = cssSelector.notSelectors[0];
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']); expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']); expect(notSelector.classNames).toEqual(['someclass']);
@ -280,6 +287,11 @@ export function main() {
.toThrowError('Nesting :not is not allowed in a selector'); .toThrowError('Nesting :not is not allowed in a selector');
}); });
it('should throw when multiple selectors in :not', () => {
expect(() => { CssSelector.parse('sometag:not(a,b)'); })
.toThrowError('Multiple selectors in :not are not supported');
});
it('should detect lists of selectors', () => { it('should detect lists of selectors', () => {
var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag'); var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag');
expect(cssSelectors.length).toEqual(3); expect(cssSelectors.length).toEqual(3);
@ -298,10 +310,10 @@ export function main() {
expect(cssSelectors[0].attrs).toEqual(['type', 'text']); expect(cssSelectors[0].attrs).toEqual(['type', 'text']);
expect(cssSelectors[1].element).toEqual('*'); expect(cssSelectors[1].element).toEqual('*');
expect(cssSelectors[1].notSelector.element).toEqual('textarea'); expect(cssSelectors[1].notSelectors[0].element).toEqual('textarea');
expect(cssSelectors[2].element).toEqual('textbox'); expect(cssSelectors[2].element).toEqual('textbox');
expect(cssSelectors[2].notSelector.classNames).toEqual(['special']); expect(cssSelectors[2].notSelectors[0].classNames).toEqual(['special']);
}); });
}); });
} }