refactor(CssSelector): misc cleanup

This commit is contained in:
Victor Berchet 2016-09-25 11:28:47 -07:00 committed by Chuck Jazdzewski
parent 38c5304b7f
commit 1c012a035f
1 changed files with 107 additions and 129 deletions

View File

@ -6,13 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ListWrapper} from './facade/collection';
import {StringWrapper, isBlank, isPresent} from './facade/lang';
import {getHtmlTagDefinition} from './ml_parser/html_tags'; import {getHtmlTagDefinition} from './ml_parser/html_tags';
const _EMPTY_ATTR_VALUE = '';
const _SELECTOR_REGEXP = new RegExp( const _SELECTOR_REGEXP = new RegExp(
'(\\:not\\()|' + //":not(" '(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag" '([-\\w]+)|' + // "tag"
@ -34,21 +29,21 @@ export class CssSelector {
notSelectors: CssSelector[] = []; notSelectors: CssSelector[] = [];
static parse(selector: string): CssSelector[] { static parse(selector: string): CssSelector[] {
var results: CssSelector[] = []; const results: CssSelector[] = [];
var _addResult = (res: CssSelector[], cssSel: CssSelector) => { const _addResult = (res: CssSelector[], cssSel: CssSelector) => {
if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) && if (cssSel.notSelectors.length > 0 && !cssSel.element && cssSel.classNames.length == 0 &&
ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) { cssSel.attrs.length == 0) {
cssSel.element = '*'; cssSel.element = '*';
} }
res.push(cssSel); res.push(cssSel);
}; };
var cssSelector = new CssSelector(); let cssSelector = new CssSelector();
var match: string[]; let match: string[];
var current = cssSelector; let current = cssSelector;
var inNot = false; let inNot = false;
_SELECTOR_REGEXP.lastIndex = 0; _SELECTOR_REGEXP.lastIndex = 0;
while (isPresent(match = _SELECTOR_REGEXP.exec(selector))) { while (match = _SELECTOR_REGEXP.exec(selector)) {
if (isPresent(match[1])) { if (match[1]) {
if (inNot) { if (inNot) {
throw new Error('Nesting :not is not allowed in a selector'); throw new Error('Nesting :not is not allowed in a selector');
} }
@ -56,20 +51,20 @@ export class CssSelector {
current = new CssSelector(); current = new CssSelector();
cssSelector.notSelectors.push(current); cssSelector.notSelectors.push(current);
} }
if (isPresent(match[2])) { if (match[2]) {
current.setElement(match[2]); current.setElement(match[2]);
} }
if (isPresent(match[3])) { if (match[3]) {
current.addClassName(match[3]); current.addClassName(match[3]);
} }
if (isPresent(match[4])) { if (match[4]) {
current.addAttribute(match[4], match[5]); current.addAttribute(match[4], match[5]);
} }
if (isPresent(match[6])) { if (match[6]) {
inNot = false; inNot = false;
current = cssSelector; current = cssSelector;
} }
if (isPresent(match[7])) { if (match[7]) {
if (inNot) { if (inNot) {
throw new Error('Multiple selectors in :not are not supported'); throw new Error('Multiple selectors in :not are not supported');
} }
@ -106,37 +101,22 @@ export class CssSelector {
`<${tagName}${classAttr}${attrs}></${tagName}>`; `<${tagName}${classAttr}${attrs}></${tagName}>`;
} }
addAttribute(name: string, value: string = _EMPTY_ATTR_VALUE) { addAttribute(name: string, value: string = '') {
this.attrs.push(name); this.attrs.push(name, value && value.toLowerCase() || '');
if (isPresent(value)) {
value = value.toLowerCase();
} else {
value = _EMPTY_ATTR_VALUE;
}
this.attrs.push(value);
} }
addClassName(name: string) { this.classNames.push(name.toLowerCase()); } addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
toString(): string { toString(): string {
var res = ''; let res: string = this.element || '';
if (isPresent(this.element)) { if (this.classNames) {
res += this.element; this.classNames.forEach(klass => res += `.${klass}`);
} }
if (isPresent(this.classNames)) { if (this.attrs) {
for (var i = 0; i < this.classNames.length; i++) { for (let i = 0; i < this.attrs.length; i += 2) {
res += '.' + this.classNames[i]; const name = this.attrs[i];
} const value = this.attrs[i + 1];
} res += `[${name}${value ? '=' + value : ''}]`;
if (isPresent(this.attrs)) {
for (var i = 0; i < this.attrs.length;) {
var attrName = this.attrs[i++];
var attrValue = this.attrs[i++];
res += '[' + attrName;
if (attrValue.length > 0) {
res += '=' + attrValue;
}
res += ']';
} }
} }
this.notSelectors.forEach(notSelector => res += `:not(${notSelector})`); this.notSelectors.forEach(notSelector => res += `:not(${notSelector})`);
@ -150,26 +130,26 @@ export class CssSelector {
*/ */
export class SelectorMatcher { export class SelectorMatcher {
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher { static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
var notMatcher = new SelectorMatcher(); const notMatcher = new SelectorMatcher();
notMatcher.addSelectables(notSelectors, null); notMatcher.addSelectables(notSelectors, null);
return notMatcher; return notMatcher;
} }
private _elementMap = new Map<string, SelectorContext[]>(); private _elementMap: {[k: string]: SelectorContext[]} = {};
private _elementPartialMap = new Map<string, SelectorMatcher>(); private _elementPartialMap: {[k: string]: SelectorMatcher} = {};
private _classMap = new Map<string, SelectorContext[]>(); private _classMap: {[k: string]: SelectorContext[]} = {};
private _classPartialMap = new Map<string, SelectorMatcher>(); private _classPartialMap: {[k: string]: SelectorMatcher} = {};
private _attrValueMap = new Map<string, Map<string, SelectorContext[]>>(); private _attrValueMap: {[k: string]: {[k: string]: SelectorContext[]}} = {};
private _attrValuePartialMap = new Map<string, Map<string, SelectorMatcher>>(); private _attrValuePartialMap: {[k: string]: {[k: string]: SelectorMatcher}} = {};
private _listContexts: SelectorListContext[] = []; private _listContexts: SelectorListContext[] = [];
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) { addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
var listContext: SelectorListContext = null; let listContext: SelectorListContext = null;
if (cssSelectors.length > 1) { if (cssSelectors.length > 1) {
listContext = new SelectorListContext(cssSelectors); listContext = new SelectorListContext(cssSelectors);
this._listContexts.push(listContext); this._listContexts.push(listContext);
} }
for (var i = 0; i < cssSelectors.length; i++) { for (let i = 0; i < cssSelectors.length; i++) {
this._addSelectable(cssSelectors[i], callbackCtxt, listContext); this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
} }
} }
@ -181,14 +161,14 @@ export class SelectorMatcher {
*/ */
private _addSelectable( private _addSelectable(
cssSelector: CssSelector, callbackCtxt: any, listContext: SelectorListContext) { cssSelector: CssSelector, callbackCtxt: any, listContext: SelectorListContext) {
var matcher: SelectorMatcher = this; let matcher: SelectorMatcher = this;
var element = cssSelector.element; const element = cssSelector.element;
var classNames = cssSelector.classNames; const classNames = cssSelector.classNames;
var attrs = cssSelector.attrs; const attrs = cssSelector.attrs;
var selectable = new SelectorContext(cssSelector, callbackCtxt, listContext); const selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
if (isPresent(element)) { if (element) {
var isTerminal = attrs.length === 0 && classNames.length === 0; const isTerminal = attrs.length === 0 && classNames.length === 0;
if (isTerminal) { if (isTerminal) {
this._addTerminal(matcher._elementMap, element, selectable); this._addTerminal(matcher._elementMap, element, selectable);
} else { } else {
@ -196,10 +176,10 @@ export class SelectorMatcher {
} }
} }
if (isPresent(classNames)) { if (classNames) {
for (var index = 0; index < classNames.length; index++) { for (let i = 0; i < classNames.length; i++) {
var isTerminal = attrs.length === 0 && index === classNames.length - 1; const isTerminal = attrs.length === 0 && i === classNames.length - 1;
var className = classNames[index]; const className = classNames[i];
if (isTerminal) { if (isTerminal) {
this._addTerminal(matcher._classMap, className, selectable); this._addTerminal(matcher._classMap, className, selectable);
} else { } else {
@ -208,47 +188,47 @@ export class SelectorMatcher {
} }
} }
if (isPresent(attrs)) { if (attrs) {
for (var index = 0; index < attrs.length;) { for (let i = 0; i < attrs.length; i += 2) {
var isTerminal = index === attrs.length - 2; const isTerminal = i === attrs.length - 2;
var attrName = attrs[index++]; const name = attrs[i];
var attrValue = attrs[index++]; const value = attrs[i + 1];
if (isTerminal) { if (isTerminal) {
var terminalMap = matcher._attrValueMap; const terminalMap = matcher._attrValueMap;
var terminalValuesMap = terminalMap.get(attrName); let terminalValuesMap = terminalMap[name];
if (!terminalValuesMap) { if (!terminalValuesMap) {
terminalValuesMap = new Map<string, SelectorContext[]>(); terminalValuesMap = {};
terminalMap.set(attrName, terminalValuesMap); terminalMap[name] = terminalValuesMap;
} }
this._addTerminal(terminalValuesMap, attrValue, selectable); this._addTerminal(terminalValuesMap, value, selectable);
} else { } else {
var parttialMap = matcher._attrValuePartialMap; let partialMap = matcher._attrValuePartialMap;
var partialValuesMap = parttialMap.get(attrName); let partialValuesMap = partialMap[name];
if (!partialValuesMap) { if (!partialValuesMap) {
partialValuesMap = new Map<string, SelectorMatcher>(); partialValuesMap = {};
parttialMap.set(attrName, partialValuesMap); partialMap[name] = partialValuesMap;
} }
matcher = this._addPartial(partialValuesMap, attrValue); matcher = this._addPartial(partialValuesMap, value);
} }
} }
} }
} }
private _addTerminal( private _addTerminal(
map: Map<string, SelectorContext[]>, name: string, selectable: SelectorContext) { map: {[k: string]: SelectorContext[]}, name: string, selectable: SelectorContext) {
var terminalList = map.get(name); let terminalList = map[name];
if (!terminalList) { if (!terminalList) {
terminalList = []; terminalList = [];
map.set(name, terminalList); map[name] = terminalList;
} }
terminalList.push(selectable); terminalList.push(selectable);
} }
private _addPartial(map: Map<string, SelectorMatcher>, name: string): SelectorMatcher { private _addPartial(map: {[k: string]: SelectorMatcher}, name: string): SelectorMatcher {
var matcher = map.get(name); let matcher = map[name];
if (!matcher) { if (!matcher) {
matcher = new SelectorMatcher(); matcher = new SelectorMatcher();
map.set(name, matcher); map[name] = matcher;
} }
return matcher; return matcher;
} }
@ -261,12 +241,12 @@ export class SelectorMatcher {
* @return boolean true if a match was found * @return boolean true if a match was found
*/ */
match(cssSelector: CssSelector, matchedCallback: (c: CssSelector, a: any) => void): boolean { match(cssSelector: CssSelector, matchedCallback: (c: CssSelector, a: any) => void): boolean {
var result = false; let result = false;
var element = cssSelector.element; const element = cssSelector.element;
var classNames = cssSelector.classNames; const classNames = cssSelector.classNames;
var attrs = cssSelector.attrs; const attrs = cssSelector.attrs;
for (var i = 0; i < this._listContexts.length; i++) { for (let i = 0; i < this._listContexts.length; i++) {
this._listContexts[i].alreadyMatched = false; this._listContexts[i].alreadyMatched = false;
} }
@ -274,9 +254,9 @@ export class SelectorMatcher {
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) ||
result; result;
if (isPresent(classNames)) { if (classNames) {
for (var index = 0; index < classNames.length; index++) { for (let i = 0; i < classNames.length; i++) {
var className = classNames[index]; const className = classNames[i];
result = result =
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result; this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
result = result =
@ -285,28 +265,25 @@ export class SelectorMatcher {
} }
} }
if (isPresent(attrs)) { if (attrs) {
for (var index = 0; index < attrs.length;) { for (let i = 0; i < attrs.length; i += 2) {
var attrName = attrs[index++]; const name = attrs[i];
var attrValue = attrs[index++]; const value = attrs[i + 1];
var terminalValuesMap = this._attrValueMap.get(attrName); const terminalValuesMap = this._attrValueMap[name];
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) { if (value) {
result = this._matchTerminal( result =
terminalValuesMap, _EMPTY_ATTR_VALUE, cssSelector, matchedCallback) || this._matchTerminal(terminalValuesMap, '', cssSelector, matchedCallback) || result;
result;
}
result = this._matchTerminal(terminalValuesMap, attrValue, cssSelector, matchedCallback) ||
result;
var partialValuesMap = this._attrValuePartialMap.get(attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
result = this._matchPartial(
partialValuesMap, _EMPTY_ATTR_VALUE, cssSelector, matchedCallback) ||
result;
} }
result = result =
this._matchPartial(partialValuesMap, attrValue, cssSelector, matchedCallback) || result; this._matchTerminal(terminalValuesMap, value, cssSelector, matchedCallback) || result;
const partialValuesMap = this._attrValuePartialMap[name];
if (value) {
result = this._matchPartial(partialValuesMap, '', cssSelector, matchedCallback) || result;
}
result =
this._matchPartial(partialValuesMap, value, cssSelector, matchedCallback) || result;
} }
} }
return result; return result;
@ -314,24 +291,24 @@ export class SelectorMatcher {
/** @internal */ /** @internal */
_matchTerminal( _matchTerminal(
map: Map<string, SelectorContext[]>, name: string, cssSelector: CssSelector, map: {[k: string]: SelectorContext[]}, name: string, cssSelector: CssSelector,
matchedCallback: (c: CssSelector, a: any) => void): boolean { matchedCallback: (c: CssSelector, a: any) => void): boolean {
if (!map || isBlank(name)) { if (!map || typeof name !== 'string') {
return false; return false;
} }
var selectables = map.get(name); let selectables = map[name];
var starSelectables = map.get('*'); const starSelectables = map['*'];
if (isPresent(starSelectables)) { if (starSelectables) {
selectables = selectables.concat(starSelectables); selectables = selectables.concat(starSelectables);
} }
if (!selectables) { if (!selectables) {
return false; return false;
} }
var selectable: SelectorContext; let selectable: SelectorContext;
var result = false; let result = false;
for (var index = 0; index < selectables.length; index++) { for (let i = 0; i < selectables.length; i++) {
selectable = selectables[index]; selectable = selectables[i];
result = selectable.finalize(cssSelector, matchedCallback) || result; result = selectable.finalize(cssSelector, matchedCallback) || result;
} }
return result; return result;
@ -339,12 +316,13 @@ export class SelectorMatcher {
/** @internal */ /** @internal */
_matchPartial( _matchPartial(
map: Map<string, SelectorMatcher>, name: string, cssSelector: CssSelector, map: {[k: string]: SelectorMatcher}, name: string, cssSelector: CssSelector,
matchedCallback: (c: CssSelector, a: any) => void): boolean { matchedCallback: (c: CssSelector, a: any) => void): boolean {
if (!map || isBlank(name)) { if (!map || typeof name !== 'string') {
return false; return false;
} }
var nestedSelector = map.get(name);
const nestedSelector = map[name];
if (!nestedSelector) { if (!nestedSelector) {
return false; return false;
} }
@ -373,13 +351,13 @@ export class SelectorContext {
} }
finalize(cssSelector: CssSelector, callback: (c: CssSelector, a: any) => void): boolean { finalize(cssSelector: CssSelector, callback: (c: CssSelector, a: any) => void): boolean {
var result = true; let result = true;
if (this.notSelectors.length > 0 && (!this.listContext || !this.listContext.alreadyMatched)) { if (this.notSelectors.length > 0 && (!this.listContext || !this.listContext.alreadyMatched)) {
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors); const notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
result = !notMatcher.match(cssSelector, null); result = !notMatcher.match(cssSelector, null);
} }
if (result && isPresent(callback) && (!this.listContext || !this.listContext.alreadyMatched)) { if (result && callback && (!this.listContext || !this.listContext.alreadyMatched)) {
if (isPresent(this.listContext)) { if (this.listContext) {
this.listContext.alreadyMatched = true; this.listContext.alreadyMatched = true;
} }
callback(this.selector, this.cbContext); callback(this.selector, this.cbContext);