299 lines
12 KiB
TypeScript
299 lines
12 KiB
TypeScript
|
import {MapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||
|
import {
|
||
|
RegExpWrapper,
|
||
|
isPresent,
|
||
|
StringWrapper,
|
||
|
BaseException,
|
||
|
StringJoiner,
|
||
|
stringify,
|
||
|
assertionsEnabled,
|
||
|
isBlank
|
||
|
} from 'angular2/src/core/facade/lang';
|
||
|
|
||
|
import {Parser, AST, ASTWithSource} from 'angular2/src/core/change_detection/change_detection';
|
||
|
|
||
|
import {DirectiveMetadata, ComponentMetadata} from './api';
|
||
|
import {
|
||
|
ElementAst,
|
||
|
BoundPropertyAst,
|
||
|
BoundEventAst,
|
||
|
VariableAst,
|
||
|
TemplateAst,
|
||
|
TextAst,
|
||
|
BoundTextAst,
|
||
|
EmbeddedTemplateAst,
|
||
|
AttrAst,
|
||
|
NgContentAst
|
||
|
} from './template_ast';
|
||
|
import {CssSelector, SelectorMatcher} from 'angular2/src/core/render/dom/compiler/selector';
|
||
|
|
||
|
import {
|
||
|
HtmlAstVisitor,
|
||
|
HtmlAst,
|
||
|
HtmlElementAst,
|
||
|
HtmlAttrAst,
|
||
|
HtmlTextAst,
|
||
|
htmlVisitAll
|
||
|
} from './html_ast';
|
||
|
|
||
|
import {dashCaseToCamelCase} from './util';
|
||
|
|
||
|
// Group 1 = "bind-"
|
||
|
// Group 2 = "var-" or "#"
|
||
|
// Group 3 = "on-"
|
||
|
// Group 4 = "bindon-"
|
||
|
// Group 5 = the identifier after "bind-", "var-/#", or "on-"
|
||
|
// Group 6 = idenitifer inside [()]
|
||
|
// Group 7 = idenitifer inside []
|
||
|
// Group 8 = identifier inside ()
|
||
|
var BIND_NAME_REGEXP =
|
||
|
/^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;
|
||
|
|
||
|
const NG_CONTENT_SELECT_ATTR = 'select';
|
||
|
const NG_CONTENT_ELEMENT = 'ng-content';
|
||
|
const TEMPLATE_ELEMENT = 'template';
|
||
|
const TEMPLATE_ATTR = 'template';
|
||
|
const TEMPLATE_ATTR_PREFIX = '*';
|
||
|
const CLASS_ATTR = 'class';
|
||
|
const IMPLICIT_VAR_NAME = '$implicit';
|
||
|
|
||
|
export class TemplateParser {
|
||
|
constructor(private _exprParser: Parser) {}
|
||
|
|
||
|
parse(domNodes: HtmlAst[], directives: DirectiveMetadata[]): TemplateAst[] {
|
||
|
var parseVisitor = new TemplateParseVisitor(directives, this._exprParser);
|
||
|
return htmlVisitAll(parseVisitor, domNodes);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class TemplateParseVisitor implements HtmlAstVisitor {
|
||
|
selectorMatcher: SelectorMatcher;
|
||
|
constructor(directives: DirectiveMetadata[], private _exprParser: Parser) {
|
||
|
this.selectorMatcher = new SelectorMatcher();
|
||
|
directives.forEach(directive => {
|
||
|
var selector = CssSelector.parse(directive.selector);
|
||
|
this.selectorMatcher.addSelectables(selector, directive);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
visitText(ast: HtmlTextAst): any {
|
||
|
var expr = this._exprParser.parseInterpolation(ast.value, ast.sourceInfo);
|
||
|
if (isPresent(expr)) {
|
||
|
return new BoundTextAst(expr, ast.sourceInfo);
|
||
|
} else {
|
||
|
return new TextAst(ast.value, ast.sourceInfo);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
visitAttr(ast: HtmlAttrAst): any { return new AttrAst(ast.name, ast.value, ast.sourceInfo); }
|
||
|
|
||
|
visitElement(element: HtmlElementAst): any {
|
||
|
var nodeName = element.name;
|
||
|
var matchableAttrs: string[][] = [];
|
||
|
var props: BoundPropertyAst[] = [];
|
||
|
var vars: VariableAst[] = [];
|
||
|
var events: BoundEventAst[] = [];
|
||
|
|
||
|
var templateProps: BoundPropertyAst[] = [];
|
||
|
var templateVars: VariableAst[] = [];
|
||
|
var templateMatchableAttrs: string[][] = [];
|
||
|
var hasInlineTemplates = false;
|
||
|
var attrs = [];
|
||
|
var selectAttr = null;
|
||
|
element.attrs.forEach(attr => {
|
||
|
matchableAttrs.push([attr.name, attr.value]);
|
||
|
if (attr.name == NG_CONTENT_SELECT_ATTR) {
|
||
|
selectAttr = attr.value;
|
||
|
}
|
||
|
var hasBinding = this._parseAttr(attr, matchableAttrs, props, events, vars);
|
||
|
var hasTemplateBinding = this._parseInlineTemplateBinding(attr, templateMatchableAttrs,
|
||
|
templateProps, templateVars);
|
||
|
if (!hasBinding && !hasTemplateBinding) {
|
||
|
// don't include the bindings as attributes as well in the AST
|
||
|
attrs.push(this.visitAttr(attr));
|
||
|
}
|
||
|
if (hasTemplateBinding) {
|
||
|
hasInlineTemplates = true;
|
||
|
}
|
||
|
});
|
||
|
var directives = this._parseDirectives(this.selectorMatcher, nodeName, matchableAttrs);
|
||
|
var children = htmlVisitAll(this, element.children);
|
||
|
var parsedElement;
|
||
|
if (nodeName == NG_CONTENT_ELEMENT) {
|
||
|
parsedElement = new NgContentAst(selectAttr, element.sourceInfo);
|
||
|
} else if (nodeName == TEMPLATE_ELEMENT) {
|
||
|
parsedElement =
|
||
|
new EmbeddedTemplateAst(attrs, props, vars, directives, children, element.sourceInfo);
|
||
|
} else {
|
||
|
parsedElement =
|
||
|
new ElementAst(attrs, props, events, vars, directives, children, element.sourceInfo);
|
||
|
}
|
||
|
if (hasInlineTemplates) {
|
||
|
var templateDirectives =
|
||
|
this._parseDirectives(this.selectorMatcher, TEMPLATE_ELEMENT, templateMatchableAttrs);
|
||
|
parsedElement = new EmbeddedTemplateAst([], templateProps, templateVars, templateDirectives,
|
||
|
[parsedElement], element.sourceInfo);
|
||
|
}
|
||
|
return parsedElement;
|
||
|
}
|
||
|
|
||
|
private _parseInlineTemplateBinding(attr: HtmlAttrAst, matchableAttrs: string[][],
|
||
|
props: BoundPropertyAst[], vars: VariableAst[]): boolean {
|
||
|
var templateBindingsSource = null;
|
||
|
if (attr.name == TEMPLATE_ATTR) {
|
||
|
templateBindingsSource = attr.value;
|
||
|
} else if (StringWrapper.startsWith(attr.name, TEMPLATE_ATTR_PREFIX)) {
|
||
|
var key = StringWrapper.substring(attr.name, TEMPLATE_ATTR_PREFIX.length); // remove the star
|
||
|
templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value;
|
||
|
}
|
||
|
if (isPresent(templateBindingsSource)) {
|
||
|
var bindings =
|
||
|
this._exprParser.parseTemplateBindings(templateBindingsSource, attr.sourceInfo);
|
||
|
for (var i = 0; i < bindings.length; i++) {
|
||
|
var binding = bindings[i];
|
||
|
if (binding.keyIsVar) {
|
||
|
vars.push(
|
||
|
new VariableAst(dashCaseToCamelCase(binding.key), binding.name, attr.sourceInfo));
|
||
|
matchableAttrs.push([binding.key, binding.name]);
|
||
|
} else if (isPresent(binding.expression)) {
|
||
|
props.push(new BoundPropertyAst(dashCaseToCamelCase(binding.key), binding.expression,
|
||
|
attr.sourceInfo));
|
||
|
matchableAttrs.push([binding.key, binding.expression.source]);
|
||
|
} else {
|
||
|
matchableAttrs.push([binding.key, '']);
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private _parseAttr(attr: HtmlAttrAst, matchableAttrs: string[][], props: BoundPropertyAst[],
|
||
|
events: BoundEventAst[], vars: VariableAst[]): boolean {
|
||
|
var attrName = this._normalizeAttributeName(attr.name);
|
||
|
var attrValue = attr.value;
|
||
|
var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
|
||
|
var hasBinding = false;
|
||
|
if (isPresent(bindParts)) {
|
||
|
hasBinding = true;
|
||
|
if (isPresent(bindParts[1])) { // match: bind-prop
|
||
|
this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, props);
|
||
|
|
||
|
} else if (isPresent(
|
||
|
bindParts[2])) { // match: var-name / var-name="iden" / #name / #name="iden"
|
||
|
var identifier = bindParts[5];
|
||
|
var value = attrValue.length === 0 ? IMPLICIT_VAR_NAME : attrValue;
|
||
|
this._parseVariable(identifier, value, attr.sourceInfo, matchableAttrs, vars);
|
||
|
|
||
|
} else if (isPresent(bindParts[3])) { // match: on-event
|
||
|
this._parseEvent(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, events);
|
||
|
|
||
|
} else if (isPresent(bindParts[4])) { // match: bindon-prop
|
||
|
this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, props);
|
||
|
this._parseAssignmentEvent(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs,
|
||
|
events);
|
||
|
|
||
|
} else if (isPresent(bindParts[6])) { // match: [(expr)]
|
||
|
this._parseProperty(bindParts[6], attrValue, attr.sourceInfo, matchableAttrs, props);
|
||
|
this._parseAssignmentEvent(bindParts[6], attrValue, attr.sourceInfo, matchableAttrs,
|
||
|
events);
|
||
|
|
||
|
} else if (isPresent(bindParts[7])) { // match: [expr]
|
||
|
this._parseProperty(bindParts[7], attrValue, attr.sourceInfo, matchableAttrs, props);
|
||
|
|
||
|
} else if (isPresent(bindParts[8])) { // match: (event)
|
||
|
this._parseEvent(bindParts[8], attrValue, attr.sourceInfo, matchableAttrs, events);
|
||
|
}
|
||
|
} else {
|
||
|
hasBinding = this._parsePropertyInterpolation(attrName, attrValue, attr.sourceInfo,
|
||
|
matchableAttrs, props);
|
||
|
}
|
||
|
return hasBinding;
|
||
|
}
|
||
|
|
||
|
private _normalizeAttributeName(attrName: string): string {
|
||
|
return StringWrapper.startsWith(attrName, 'data-') ? StringWrapper.substring(attrName, 5) :
|
||
|
attrName;
|
||
|
}
|
||
|
|
||
|
private _parseVariable(identifier: string, value: string, sourceInfo: any,
|
||
|
matchableAttrs: string[][], vars: VariableAst[]) {
|
||
|
vars.push(new VariableAst(dashCaseToCamelCase(identifier), value, sourceInfo));
|
||
|
matchableAttrs.push([identifier, value]);
|
||
|
}
|
||
|
|
||
|
private _parseProperty(name: string, expression: string, sourceInfo: any,
|
||
|
matchableAttrs: string[][], props: BoundPropertyAst[]) {
|
||
|
this._parsePropertyAst(name, this._exprParser.parseBinding(expression, sourceInfo), sourceInfo,
|
||
|
matchableAttrs, props);
|
||
|
}
|
||
|
|
||
|
private _parsePropertyInterpolation(name: string, value: string, sourceInfo: any,
|
||
|
matchableAttrs: string[][],
|
||
|
props: BoundPropertyAst[]): boolean {
|
||
|
var expr = this._exprParser.parseInterpolation(value, sourceInfo);
|
||
|
if (isPresent(expr)) {
|
||
|
this._parsePropertyAst(name, expr, sourceInfo, matchableAttrs, props);
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private _parsePropertyAst(name: string, ast: ASTWithSource, sourceInfo: any,
|
||
|
matchableAttrs: string[][], props: BoundPropertyAst[]) {
|
||
|
props.push(new BoundPropertyAst(dashCaseToCamelCase(name), ast, sourceInfo));
|
||
|
matchableAttrs.push([name, ast.source]);
|
||
|
}
|
||
|
|
||
|
private _parseAssignmentEvent(name: string, expression: string, sourceInfo: string,
|
||
|
matchableAttrs: string[][], events: BoundEventAst[]) {
|
||
|
this._parseEvent(name, `${expression}=$event`, sourceInfo, matchableAttrs, events);
|
||
|
}
|
||
|
|
||
|
private _parseEvent(name: string, expression: string, sourceInfo: string,
|
||
|
matchableAttrs: string[][], events: BoundEventAst[]) {
|
||
|
events.push(new BoundEventAst(dashCaseToCamelCase(name),
|
||
|
this._exprParser.parseAction(expression, sourceInfo),
|
||
|
sourceInfo));
|
||
|
// Don't detect directives for event names for now,
|
||
|
// so don't add the event name to the matchableAttrs
|
||
|
}
|
||
|
|
||
|
private _parseDirectives(selectorMatcher: SelectorMatcher, elementName: string,
|
||
|
matchableAttrs: string[][]): DirectiveMetadata[] {
|
||
|
var cssSelector = new CssSelector();
|
||
|
|
||
|
cssSelector.setElement(elementName);
|
||
|
for (var i = 0; i < matchableAttrs.length; i++) {
|
||
|
var attrName = matchableAttrs[i][0].toLowerCase();
|
||
|
var attrValue = matchableAttrs[i][1];
|
||
|
cssSelector.addAttribute(attrName, attrValue);
|
||
|
if (attrName == CLASS_ATTR) {
|
||
|
var classes = splitClasses(attrValue);
|
||
|
classes.forEach(className => cssSelector.addClassName(className));
|
||
|
}
|
||
|
}
|
||
|
var directives = [];
|
||
|
selectorMatcher.match(cssSelector, (selector, directive) => { directives.push(directive); });
|
||
|
// Need to sort the directives so that we get consistent results throughout,
|
||
|
// as selectorMatcher uses Maps inside.
|
||
|
// Also need to make components the first directive in the array
|
||
|
ListWrapper.sort(directives, (dir1: DirectiveMetadata, dir2: DirectiveMetadata) => {
|
||
|
var dir1Comp = dir1 instanceof ComponentMetadata;
|
||
|
var dir2Comp = dir2 instanceof ComponentMetadata;
|
||
|
if (dir1Comp && !dir2Comp) {
|
||
|
return -1;
|
||
|
} else if (!dir1Comp && dir2Comp) {
|
||
|
return 1;
|
||
|
} else {
|
||
|
return StringWrapper.compare(dir1.type.typeName, dir2.type.typeName);
|
||
|
}
|
||
|
});
|
||
|
return directives;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function splitClasses(classAttrValue: string): string[] {
|
||
|
return StringWrapper.split(classAttrValue.trim(), /\s+/g);
|
||
|
}
|