Files
angular-docs-cn/modules/@angular/compiler/src/selector.ts
T

368 lines
12 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getHtmlTagDefinition} from './ml_parser/html_tags';
2014-10-02 20:39:27 -07:00
const _SELECTOR_REGEXP = new RegExp(
'(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class"
'(?:\\[([-.\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]"
'(\\))|' + // ")"
'(\\s*,\\s*)', // ","
'g');
/**
* A css selector contains an element name,
* css classes and attribute/value pairs with the purpose
* of selecting subsets out of them.
*/
export class CssSelector {
2015-06-12 23:11:11 +02:00
element: string = null;
classNames: string[] = [];
attrs: string[] = [];
notSelectors: CssSelector[] = [];
2015-06-12 23:11:11 +02:00
static parse(selector: string): CssSelector[] {
2016-09-25 11:28:47 -07:00
const results: CssSelector[] = [];
const _addResult = (res: CssSelector[], cssSel: CssSelector) => {
if (cssSel.notSelectors.length > 0 && !cssSel.element && cssSel.classNames.length == 0 &&
cssSel.attrs.length == 0) {
2016-06-08 16:38:52 -07:00
cssSel.element = '*';
}
res.push(cssSel);
2015-05-21 16:42:19 +02:00
};
2016-09-25 11:28:47 -07:00
let cssSelector = new CssSelector();
let match: string[];
let current = cssSelector;
let inNot = false;
_SELECTOR_REGEXP.lastIndex = 0;
2016-09-25 11:28:47 -07:00
while (match = _SELECTOR_REGEXP.exec(selector)) {
if (match[1]) {
if (inNot) {
throw new Error('Nesting :not is not allowed in a selector');
2015-05-21 16:42:19 +02:00
}
inNot = true;
current = new CssSelector();
cssSelector.notSelectors.push(current);
2015-05-21 16:42:19 +02:00
}
2016-09-25 11:28:47 -07:00
if (match[2]) {
2015-05-21 16:42:19 +02:00
current.setElement(match[2]);
}
2016-09-25 11:28:47 -07:00
if (match[3]) {
2015-05-21 16:42:19 +02:00
current.addClassName(match[3]);
}
2016-09-25 11:28:47 -07:00
if (match[4]) {
2015-05-21 16:42:19 +02:00
current.addAttribute(match[4], match[5]);
}
2016-09-25 11:28:47 -07:00
if (match[6]) {
inNot = false;
current = cssSelector;
}
2016-09-25 11:28:47 -07:00
if (match[7]) {
if (inNot) {
throw new Error('Multiple selectors in :not are not supported');
}
2015-05-21 16:42:19 +02:00
_addResult(results, cssSelector);
cssSelector = current = new CssSelector();
}
}
2015-05-21 16:42:19 +02:00
_addResult(results, cssSelector);
return results;
}
2015-05-21 16:42:19 +02:00
isElementSelector(): boolean {
return this.hasElementSelector() && this.classNames.length == 0 && this.attrs.length == 0 &&
this.notSelectors.length === 0;
2015-05-21 16:42:19 +02:00
}
hasElementSelector(): boolean { return !!this.element; }
setElement(element: string = null) { this.element = element; }
/** Gets a template string for an element that matches the selector. */
getMatchingElementTemplate(): string {
2016-09-07 15:38:44 -07:00
const tagName = this.element || 'div';
const classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : '';
let attrs = '';
for (let i = 0; i < this.attrs.length; i += 2) {
2016-09-07 15:38:44 -07:00
const attrName = this.attrs[i];
const attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : '';
attrs += ` ${attrName}${attrValue}`;
}
return getHtmlTagDefinition(tagName).isVoid ? `<${tagName}${classAttr}${attrs}/>` :
`<${tagName}${classAttr}${attrs}></${tagName}>`;
}
2016-09-25 11:28:47 -07:00
addAttribute(name: string, value: string = '') {
this.attrs.push(name, value && value.toLowerCase() || '');
}
addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
2015-05-21 16:42:19 +02:00
toString(): string {
2016-09-25 11:28:47 -07:00
let res: string = this.element || '';
if (this.classNames) {
this.classNames.forEach(klass => res += `.${klass}`);
}
2016-09-25 11:28:47 -07:00
if (this.attrs) {
for (let i = 0; i < this.attrs.length; i += 2) {
const name = this.attrs[i];
const value = this.attrs[i + 1];
res += `[${name}${value ? '=' + value : ''}]`;
2015-05-21 16:42:19 +02:00
}
}
this.notSelectors.forEach(notSelector => res += `:not(${notSelector})`);
2015-05-21 16:42:19 +02:00
return res;
}
}
/**
* Reads a list of CssSelectors and allows to calculate which ones
* are contained in a given CssSelector.
*/
export class SelectorMatcher {
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
2016-09-25 11:28:47 -07:00
const notMatcher = new SelectorMatcher();
notMatcher.addSelectables(notSelectors, null);
return notMatcher;
2015-05-18 11:57:20 -07:00
}
2016-10-18 01:19:18 +03:00
private _elementMap = new Map<string, SelectorContext[]>();
private _elementPartialMap = new Map<string, SelectorMatcher>();
private _classMap = new Map<string, SelectorContext[]>();
private _classPartialMap = new Map<string, SelectorMatcher>();
private _attrValueMap = new Map<string, Map<string, SelectorContext[]>>();
private _attrValuePartialMap = new Map<string, Map<string, SelectorMatcher>>();
private _listContexts: SelectorListContext[] = [];
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
2016-09-25 11:28:47 -07:00
let listContext: SelectorListContext = null;
if (cssSelectors.length > 1) {
2015-05-18 11:57:20 -07:00
listContext = new SelectorListContext(cssSelectors);
this._listContexts.push(listContext);
}
2016-09-25 11:28:47 -07:00
for (let i = 0; i < cssSelectors.length; i++) {
2015-05-18 11:57:20 -07:00
this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
}
2014-10-02 20:39:27 -07:00
}
/**
* Add an object that can be found later on by calling `match`.
* @param cssSelector A css selector
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
2014-10-02 20:39:27 -07:00
*/
2016-06-08 16:38:52 -07:00
private _addSelectable(
cssSelector: CssSelector, callbackCtxt: any, listContext: SelectorListContext) {
2016-09-25 11:28:47 -07:00
let matcher: SelectorMatcher = this;
const element = cssSelector.element;
const classNames = cssSelector.classNames;
const attrs = cssSelector.attrs;
const selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
if (element) {
const isTerminal = attrs.length === 0 && classNames.length === 0;
if (isTerminal) {
this._addTerminal(matcher._elementMap, element, selectable);
} else {
matcher = this._addPartial(matcher._elementPartialMap, element);
}
}
2016-09-25 11:28:47 -07:00
if (classNames) {
for (let i = 0; i < classNames.length; i++) {
const isTerminal = attrs.length === 0 && i === classNames.length - 1;
const className = classNames[i];
if (isTerminal) {
this._addTerminal(matcher._classMap, className, selectable);
} else {
matcher = this._addPartial(matcher._classPartialMap, className);
}
}
}
2016-09-25 11:28:47 -07:00
if (attrs) {
for (let i = 0; i < attrs.length; i += 2) {
const isTerminal = i === attrs.length - 2;
const name = attrs[i];
const value = attrs[i + 1];
if (isTerminal) {
2016-09-25 11:28:47 -07:00
const terminalMap = matcher._attrValueMap;
2016-10-18 01:19:18 +03:00
let terminalValuesMap = terminalMap.get(name);
if (!terminalValuesMap) {
2016-10-18 01:19:18 +03:00
terminalValuesMap = new Map<string, SelectorContext[]>();
terminalMap.set(name, terminalValuesMap);
2015-05-20 09:48:15 -07:00
}
2016-09-25 11:28:47 -07:00
this._addTerminal(terminalValuesMap, value, selectable);
} else {
const partialMap = matcher._attrValuePartialMap;
2016-10-18 01:19:18 +03:00
let partialValuesMap = partialMap.get(name);
if (!partialValuesMap) {
2016-10-18 01:19:18 +03:00
partialValuesMap = new Map<string, SelectorMatcher>();
partialMap.set(name, partialValuesMap);
2015-05-20 09:48:15 -07:00
}
2016-09-25 11:28:47 -07:00
matcher = this._addPartial(partialValuesMap, value);
}
}
}
}
2016-06-08 16:38:52 -07:00
private _addTerminal(
2016-10-18 01:19:18 +03:00
map: Map<string, SelectorContext[]>, name: string, selectable: SelectorContext) {
let terminalList = map.get(name);
if (!terminalList) {
terminalList = [];
2016-10-18 01:19:18 +03:00
map.set(name, terminalList);
}
terminalList.push(selectable);
}
2016-10-18 01:19:18 +03:00
private _addPartial(map: Map<string, SelectorMatcher>, name: string): SelectorMatcher {
let matcher = map.get(name);
if (!matcher) {
matcher = new SelectorMatcher();
2016-10-18 01:19:18 +03:00
map.set(name, matcher);
}
return matcher;
}
/**
* Find the objects that have been added via `addSelectable`
* 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`
2015-03-12 09:44:49 +01:00
* @return boolean true if a match was found
*/
2015-10-09 09:28:12 -07:00
match(cssSelector: CssSelector, matchedCallback: (c: CssSelector, a: any) => void): boolean {
2016-09-25 11:28:47 -07:00
let result = false;
const element = cssSelector.element;
const classNames = cssSelector.classNames;
const attrs = cssSelector.attrs;
2016-09-25 11:28:47 -07:00
for (let i = 0; i < this._listContexts.length; i++) {
this._listContexts[i].alreadyMatched = false;
}
2015-03-12 09:44:49 +01:00
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
2015-05-18 11:57:20 -07:00
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) ||
2016-06-08 16:38:52 -07:00
result;
2016-09-25 11:28:47 -07:00
if (classNames) {
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
2015-05-18 11:57:20 -07:00
result =
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
result =
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
result;
}
}
2016-09-25 11:28:47 -07:00
if (attrs) {
for (let i = 0; i < attrs.length; i += 2) {
const name = attrs[i];
const value = attrs[i + 1];
2016-10-18 01:19:18 +03:00
const terminalValuesMap = this._attrValueMap.get(name);
2016-09-25 11:28:47 -07:00
if (value) {
result =
this._matchTerminal(terminalValuesMap, '', cssSelector, matchedCallback) || result;
}
2016-09-25 11:28:47 -07:00
result =
this._matchTerminal(terminalValuesMap, value, cssSelector, matchedCallback) || result;
2016-10-18 01:19:18 +03:00
const partialValuesMap = this._attrValuePartialMap.get(name);
2016-09-25 11:28:47 -07:00
if (value) {
result = this._matchPartial(partialValuesMap, '', cssSelector, matchedCallback) || result;
}
2015-05-20 09:48:15 -07:00
result =
2016-09-25 11:28:47 -07:00
this._matchPartial(partialValuesMap, value, cssSelector, matchedCallback) || result;
}
}
2015-03-12 09:44:49 +01:00
return result;
}
/** @internal */
2016-06-08 16:38:52 -07:00
_matchTerminal(
2016-10-18 01:19:18 +03:00
map: Map<string, SelectorContext[]>, name: string, cssSelector: CssSelector,
2016-06-08 16:38:52 -07:00
matchedCallback: (c: CssSelector, a: any) => void): boolean {
2016-09-25 11:28:47 -07:00
if (!map || typeof name !== 'string') {
2015-03-12 09:44:49 +01:00
return false;
}
let selectables: SelectorContext[] = map.get(name) || [];
const starSelectables: SelectorContext[] = map.get('*');
2016-09-25 11:28:47 -07:00
if (starSelectables) {
2015-08-28 11:29:19 -07:00
selectables = selectables.concat(starSelectables);
}
if (selectables.length === 0) {
2015-03-12 09:44:49 +01:00
return false;
}
2016-09-25 11:28:47 -07:00
let selectable: SelectorContext;
let result = false;
for (let i = 0; i < selectables.length; i++) {
selectable = selectables[i];
2015-03-12 09:44:49 +01:00
result = selectable.finalize(cssSelector, matchedCallback) || result;
}
2015-03-12 09:44:49 +01:00
return result;
}
/** @internal */
2016-06-08 16:38:52 -07:00
_matchPartial(
2016-10-18 01:19:18 +03:00
map: Map<string, SelectorMatcher>, name: string, cssSelector: CssSelector,
2016-06-11 21:23:37 -07:00
matchedCallback: (c: CssSelector, a: any) => void): boolean {
2016-09-25 11:28:47 -07:00
if (!map || typeof name !== 'string') {
2015-03-12 09:44:49 +01:00
return false;
}
2016-09-25 11:28:47 -07:00
2016-10-18 01:19:18 +03:00
const nestedSelector = map.get(name);
if (!nestedSelector) {
2015-03-12 09:44:49 +01:00
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
2015-03-12 09:44:49 +01:00
return nestedSelector.match(cssSelector, matchedCallback);
2014-10-02 20:39:27 -07:00
}
}
export class SelectorListContext {
2015-06-12 23:11:11 +02:00
alreadyMatched: boolean = false;
constructor(public selectors: CssSelector[]) {}
}
// Store context to pass back selector and context when a selector is matched
export class SelectorContext {
notSelectors: CssSelector[];
2016-06-08 16:38:52 -07:00
constructor(
public selector: CssSelector, public cbContext: any,
public listContext: SelectorListContext) {
this.notSelectors = selector.notSelectors;
}
2015-03-12 09:44:49 +01:00
2015-10-09 09:28:12 -07:00
finalize(cssSelector: CssSelector, callback: (c: CssSelector, a: any) => void): boolean {
2016-09-25 11:28:47 -07:00
let result = true;
if (this.notSelectors.length > 0 && (!this.listContext || !this.listContext.alreadyMatched)) {
2016-09-25 11:28:47 -07:00
const notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
2015-03-12 09:44:49 +01:00
result = !notMatcher.match(cssSelector, null);
}
2016-09-25 11:28:47 -07:00
if (result && callback && (!this.listContext || !this.listContext.alreadyMatched)) {
if (this.listContext) {
this.listContext.alreadyMatched = true;
}
2015-03-12 09:44:49 +01:00
callback(this.selector, this.cbContext);
}
return result;
}
}