From a8edc1eb58b2f70b3cb2e427e5b0ee4484fd4ea4 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 10 Nov 2015 15:56:25 -0800 Subject: [PATCH] feat(Compiler): case sensitive html parser close #4417 Closes #5264 --- modules/angular2/src/compiler/html_lexer.ts | 31 +- modules/angular2/src/compiler/html_parser.ts | 81 ++-- modules/angular2/src/compiler/html_tags.ts | 114 +++++- modules/angular2/src/compiler/parse_util.ts | 34 +- .../angular2/src/compiler/template_parser.ts | 38 +- .../src/compiler/template_preparser.ts | 11 +- .../src/core/render/dom/dom_renderer.ts | 123 ++---- .../angular2/test/compiler/html_lexer_spec.ts | 49 ++- .../test/compiler/html_parser_spec.ts | 182 +++++++-- .../test/compiler/template_parser_spec.ts | 374 +++++++++++++----- .../test/core/linker/integration_spec.ts | 40 +- .../linker/projection_integration_spec.ts | 2 +- .../playground/src/zippy_component/zippy.html | 2 +- 13 files changed, 737 insertions(+), 344 deletions(-) diff --git a/modules/angular2/src/compiler/html_lexer.ts b/modules/angular2/src/compiler/html_lexer.ts index 5b56a9b94d..827010d6b3 100644 --- a/modules/angular2/src/compiler/html_lexer.ts +++ b/modules/angular2/src/compiler/html_lexer.ts @@ -6,7 +6,6 @@ import { CONST_EXPR, serializeEnum } from 'angular2/src/facade/lang'; -import {BaseException} from 'angular2/src/facade/exceptions'; import {ParseLocation, ParseError, ParseSourceFile, ParseSourceSpan} from './parse_util'; import {getHtmlTagDefinition, HtmlTagContentType, NAMED_ENTITIES} from './html_tags'; @@ -50,6 +49,7 @@ export function tokenizeHtml(sourceContent: string, sourceUrl: string): HtmlToke const $EOF = 0; const $TAB = 9; const $LF = 10; +const $FF = 12; const $CR = 13; const $SPACE = 32; @@ -247,17 +247,22 @@ class _HtmlTokenizer { } } - private _readChar(decodeEntities: boolean): string { + private _readChar(decodeEntities: boolean, extraNotCharRef: number = null): string { if (decodeEntities && this.peek === $AMPERSAND) { var start = this._getLocation(); - this._attemptUntilChar($SEMICOLON); this._advance(); - var entitySrc = this.input.substring(start.offset + 1, this.index - 1); - var decodedEntity = decodeEntity(entitySrc); - if (isPresent(decodedEntity)) { - return decodedEntity; + if (isCharRefStart(this.peek, extraNotCharRef)) { + this._attemptUntilChar($SEMICOLON); + this._advance(); + var entitySrc = this.input.substring(start.offset + 1, this.index - 1); + var decodedEntity = decodeEntity(entitySrc); + if (isPresent(decodedEntity)) { + return decodedEntity; + } else { + throw this._createError(unknownEntityErrorMsg(entitySrc), start); + } } else { - throw this._createError(unknownEntityErrorMsg(entitySrc), start); + return '&'; } } else { var index = this.index; @@ -389,7 +394,7 @@ class _HtmlTokenizer { this._advance(); var parts = []; while (this.peek !== quoteChar) { - parts.push(this._readChar(true)); + parts.push(this._readChar(true, quoteChar)); } value = parts.join(''); this._advance(); @@ -440,7 +445,13 @@ function isWhitespace(code: number): boolean { function isNameEnd(code: number): boolean { return isWhitespace(code) || code === $GT || code === $SLASH || code === $SQ || code === $DQ || - code === $EQ + code === $EQ; +} + +// http://www.w3.org/TR/html5/syntax.html#consume-a-character-reference +function isCharRefStart(code: number, extraNotCharRef: number): boolean { + return code != $TAB && code != $LF && code != $FF && code != $SPACE && code != $LT && + code != $AMPERSAND && code != $EOF && code !== extraNotCharRef; } function isPrefixEnd(code: number): boolean { diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index ea303e71da..698ada573d 100644 --- a/modules/angular2/src/compiler/html_parser.ts +++ b/modules/angular2/src/compiler/html_parser.ts @@ -9,34 +9,22 @@ import { serializeEnum, CONST_EXPR } from 'angular2/src/facade/lang'; -import {DOM} from 'angular2/src/core/dom/dom_adapter'; + import {ListWrapper} from 'angular2/src/facade/collection'; import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlElementAst} from './html_ast'; -import {escapeDoubleQuoteString} from './util'; import {Injectable} from 'angular2/src/core/di'; import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer'; import {ParseError, ParseLocation, ParseSourceSpan} from './parse_util'; import {HtmlTagDefinition, getHtmlTagDefinition} from './html_tags'; -// TODO: remove this, just provide a plain error message! -export enum HtmlTreeErrorType { - UnexpectedClosingTag -} - -const HTML_ERROR_TYPE_MSGS = CONST_EXPR(['Unexpected closing tag']); - - export class HtmlTreeError extends ParseError { - static create(type: HtmlTreeErrorType, elementName: string, - location: ParseLocation): HtmlTreeError { - return new HtmlTreeError(type, HTML_ERROR_TYPE_MSGS[serializeEnum(type)], elementName, - location); + static create(elementName: string, location: ParseLocation, msg: string): HtmlTreeError { + return new HtmlTreeError(elementName, location, msg); } - constructor(public type: HtmlTreeErrorType, msg: string, public elementName: string, - location: ParseLocation) { + constructor(public elementName: string, location: ParseLocation, msg: string) { super(location, msg); } } @@ -55,11 +43,8 @@ export class HtmlParser { } } -var NS_PREFIX_RE = /^@[^:]+/g; - class TreeBuilder { private index: number = -1; - private length: number; private peek: HtmlToken; private rootNodes: HtmlAst[] = []; @@ -129,7 +114,7 @@ class TreeBuilder { while (this.peek.type === HtmlTokenType.ATTR_NAME) { attrs.push(this._consumeAttr(this._advance())); } - var fullName = elementName(prefix, name, this._getParentElement()); + var fullName = getElementFullName(prefix, name, this._getParentElement()); var voidElement = false; // Note: There could have been a tokenizer error // so that we don't get a token for the end tag... @@ -150,15 +135,13 @@ class TreeBuilder { } private _pushElement(el: HtmlElementAst) { - var stackIndex = this.elementStack.length - 1; - while (stackIndex >= 0) { + for (var stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) { var parentEl = this.elementStack[stackIndex]; - if (!getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) { + if (getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) { + ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex); break; } - stackIndex--; } - this.elementStack.splice(stackIndex, this.elementStack.length - 1 - stackIndex); var tagDef = getHtmlTagDefinition(el.name); var parentEl = this._getParentElement(); @@ -175,35 +158,29 @@ class TreeBuilder { private _consumeEndTag(endTagToken: HtmlToken) { var fullName = - elementName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement()); + getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement()); if (!this._popElement(fullName)) { - this.errors.push(HtmlTreeError.create(HtmlTreeErrorType.UnexpectedClosingTag, fullName, - endTagToken.sourceSpan.start)); + this.errors.push(HtmlTreeError.create(fullName, endTagToken.sourceSpan.start, + `Unexpected closing tag "${endTagToken.parts[1]}"`)); } } private _popElement(fullName: string): boolean { - var stackIndex = this.elementStack.length - 1; - var hasError = false; - while (stackIndex >= 0) { + for (let stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) { var el = this.elementStack[stackIndex]; - if (el.name == fullName) { - break; + if (el.name.toLowerCase() == fullName.toLowerCase()) { + ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex); + return true; } if (!getHtmlTagDefinition(el.name).closedByParent) { - hasError = true; - break; + return false; } - stackIndex--; } - if (!hasError) { - this.elementStack.splice(stackIndex, this.elementStack.length - stackIndex); - } - return !hasError; + return false; } private _consumeAttr(attrName: HtmlToken): HtmlAttrAst { - var fullName = elementName(attrName.parts[0], attrName.parts[1], null); + var fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]); var end = attrName.sourceSpan.end; var value = ''; if (this.peek.type === HtmlTokenType.ATTR_VALUE) { @@ -228,20 +205,24 @@ class TreeBuilder { } } -function elementName(prefix: string, localName: string, parentElement: HtmlElementAst) { +function mergeNsAndName(prefix: string, localName: string): string { + return isPresent(prefix) ? `@${prefix}:${localName}` : localName; +} + +function getElementFullName(prefix: string, localName: string, + parentElement: HtmlElementAst): string { if (isBlank(prefix)) { prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix; + if (isBlank(prefix) && isPresent(parentElement)) { + prefix = namespacePrefix(parentElement.name); + } } - if (isBlank(prefix) && isPresent(parentElement)) { - prefix = namespacePrefix(parentElement.name); - } - if (isPresent(prefix)) { - return `@${prefix}:${localName}`; - } else { - return localName; - } + + return mergeNsAndName(prefix, localName); } +var NS_PREFIX_RE = /^@([^:]+)/g; + function namespacePrefix(elementName: string): string { var match = RegExpWrapper.firstMatch(NS_PREFIX_RE, elementName); return isBlank(match) ? null : match[1]; diff --git a/modules/angular2/src/compiler/html_tags.ts b/modules/angular2/src/compiler/html_tags.ts index a88bc363e4..b104cbbbc6 100644 --- a/modules/angular2/src/compiler/html_tags.ts +++ b/modules/angular2/src/compiler/html_tags.ts @@ -1,7 +1,61 @@ import {isPresent, isBlank, normalizeBool, CONST_EXPR} from 'angular2/src/facade/lang'; -// TODO: fill this! -export const NAMED_ENTITIES: {[key: string]: string} = CONST_EXPR({'amp': '&'}); +// see http://www.w3.org/TR/html51/syntax.html#named-character-references +// see https://html.spec.whatwg.org/multipage/entities.json +// This list is not exhaustive to keep the compiler footprint low. +// The `{` / `ƫ` syntax should be used when the named character reference does not exist. +export const NAMED_ENTITIES = CONST_EXPR({ + 'lt': '<', + 'gt': '>', + 'nbsp': '\u00A0', + 'amp': '&', + 'Aacute': '\u00C1', + 'Acirc': '\u00C2', + 'Agrave': '\u00C0', + 'Atilde': '\u00C3', + 'Auml': '\u00C4', + 'Ccedil': '\u00C7', + 'Eacute': '\u00C9', + 'Ecirc': '\u00CA', + 'Egrave': '\u00C8', + 'Euml': '\u00CB', + 'Iacute': '\u00CD', + 'Icirc': '\u00CE', + 'Igrave': '\u00CC', + 'Iuml': '\u00CF', + 'Oacute': '\u00D3', + 'Ocirc': '\u00D4', + 'Ograve': '\u00D2', + 'Otilde': '\u00D5', + 'Ouml': '\u00D6', + 'Uacute': '\u00DA', + 'Ucirc': '\u00DB', + 'Ugrave': '\u00D9', + 'Uuml': '\u00DC', + 'aacute': '\u00E1', + 'acirc': '\u00E2', + 'agrave': '\u00E0', + 'atilde': '\u00E3', + 'auml': '\u00E4', + 'ccedil': '\u00E7', + 'eacute': '\u00E9', + 'ecirc': '\u00EA', + 'egrave': '\u00E8', + 'euml': '\u00EB', + 'iacute': '\u00ED', + 'icirc': '\u00EE', + 'igrave': '\u00EC', + 'iuml': '\u00EF', + 'oacute': '\u00F3', + 'ocirc': '\u00F4', + 'ograve': '\u00F2', + 'otilde': '\u00F5', + 'ouml': '\u00F6', + 'uacute': '\u00FA', + 'ucirc': '\u00FB', + 'ugrave': '\u00F9', + 'uuml': '\u00FC', +}); export enum HtmlTagContentType { RAW_TEXT, @@ -11,54 +65,72 @@ export enum HtmlTagContentType { export class HtmlTagDefinition { private closedByChildren: {[key: string]: boolean} = {}; - public closedByParent: boolean; + public closedByParent: boolean = false; public requiredParent: string; public implicitNamespacePrefix: string; public contentType: HtmlTagContentType; constructor({closedByChildren, requiredParent, implicitNamespacePrefix, contentType}: { - closedByChildren?: string[], + closedByChildren?: string, requiredParent?: string, implicitNamespacePrefix?: string, contentType?: HtmlTagContentType } = {}) { - if (isPresent(closedByChildren)) { - closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true); + if (isPresent(closedByChildren) && closedByChildren.length > 0) { + closedByChildren.split(',').forEach(tagName => this.closedByChildren[tagName.trim()] = true); + this.closedByParent = true; } - this.closedByParent = isPresent(closedByChildren) && closedByChildren.length > 0; this.requiredParent = requiredParent; this.implicitNamespacePrefix = implicitNamespacePrefix; this.contentType = isPresent(contentType) ? contentType : HtmlTagContentType.PARSABLE_DATA; } - requireExtraParent(currentParent: string) { + requireExtraParent(currentParent: string): boolean { return isPresent(this.requiredParent) && - (isBlank(currentParent) || this.requiredParent != currentParent.toLocaleLowerCase()); + (isBlank(currentParent) || this.requiredParent != currentParent.toLowerCase()); } - isClosedByChild(name: string) { + isClosedByChild(name: string): boolean { return normalizeBool(this.closedByChildren['*']) || normalizeBool(this.closedByChildren[name.toLowerCase()]); } } -// TODO: Fill this table using -// https://github.com/greim/html-tokenizer/blob/master/parser.js -// and http://www.w3.org/TR/html51/syntax.html#optional-tags +// see http://www.w3.org/TR/html51/syntax.html#optional-tags +// This implementation does not fully conform to the HTML5 spec. var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = { - 'link': new HtmlTagDefinition({closedByChildren: ['*']}), - 'ng-content': new HtmlTagDefinition({closedByChildren: ['*']}), - 'img': new HtmlTagDefinition({closedByChildren: ['*']}), - 'input': new HtmlTagDefinition({closedByChildren: ['*']}), - 'p': new HtmlTagDefinition({closedByChildren: ['p']}), - 'tr': new HtmlTagDefinition({closedByChildren: ['tr'], requiredParent: 'tbody'}), - 'col': new HtmlTagDefinition({closedByChildren: ['col'], requiredParent: 'colgroup'}), + 'link': new HtmlTagDefinition({closedByChildren: '*'}), + 'ng-content': new HtmlTagDefinition({closedByChildren: '*'}), + 'img': new HtmlTagDefinition({closedByChildren: '*'}), + 'input': new HtmlTagDefinition({closedByChildren: '*'}), + 'hr': new HtmlTagDefinition({closedByChildren: '*'}), + 'br': new HtmlTagDefinition({closedByChildren: '*'}), + 'wbr': new HtmlTagDefinition({closedByChildren: '*'}), + 'p': new HtmlTagDefinition({ + closedByChildren: + 'address,article,aside,blockquote,div,dl,fieldset,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,main,nav,ol,p,pre,section,table,ul' + }), + 'thead': new HtmlTagDefinition({closedByChildren: 'tbody,tfoot'}), + 'tbody': new HtmlTagDefinition({closedByChildren: 'tbody,tfoot'}), + 'tfoot': new HtmlTagDefinition({closedByChildren: 'tbody'}), + 'tr': new HtmlTagDefinition({closedByChildren: 'tr', requiredParent: 'tbody'}), + 'td': new HtmlTagDefinition({closedByChildren: 'td,th'}), + 'th': new HtmlTagDefinition({closedByChildren: 'td,th'}), + 'col': new HtmlTagDefinition({closedByChildren: 'col', requiredParent: 'colgroup'}), 'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}), 'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}), + 'li': new HtmlTagDefinition({closedByChildren: 'li'}), + 'dt': new HtmlTagDefinition({closedByChildren: 'dt,dd'}), + 'dd': new HtmlTagDefinition({closedByChildren: 'dt,dd'}), + 'rb': new HtmlTagDefinition({closedByChildren: 'rb,rt,rtc,rp'}), + 'rt': new HtmlTagDefinition({closedByChildren: 'rb,rt,rtc,rp'}), + 'rtc': new HtmlTagDefinition({closedByChildren: 'rb,rtc,rp'}), + 'rp': new HtmlTagDefinition({closedByChildren: 'rb,rt,rtc,rp'}), + 'optgroup': new HtmlTagDefinition({closedByChildren: 'optgroup'}), 'style': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}), 'script': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}), 'title': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}), - 'textarea': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}) + 'textarea': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}), }; var DEFAULT_TAG_DEFINITION = new HtmlTagDefinition(); diff --git a/modules/angular2/src/compiler/parse_util.ts b/modules/angular2/src/compiler/parse_util.ts index 4c9413cce8..7e1da4dc43 100644 --- a/modules/angular2/src/compiler/parse_util.ts +++ b/modules/angular2/src/compiler/parse_util.ts @@ -1,10 +1,8 @@ -import {Math} from 'angular2/src/facade/math'; - export class ParseLocation { constructor(public file: ParseSourceFile, public offset: number, public line: number, public col: number) {} - toString() { return `${this.file.url}@${this.line}:${this.col}`; } + toString(): string { return `${this.file.url}@${this.line}:${this.col}`; } } export class ParseSourceFile { @@ -16,9 +14,33 @@ export abstract class ParseError { toString(): string { var source = this.location.file.content; - var ctxStart = Math.max(this.location.offset - 10, 0); - var ctxEnd = Math.min(this.location.offset + 10, source.length); - return `${this.msg} (${source.substring(ctxStart, ctxEnd)}): ${this.location}`; + var ctxStart = this.location.offset; + var ctxEnd = this.location.offset; + var ctxLen = 0; + var ctxLines = 0; + + while (ctxLen < 100 && ctxStart > 0) { + ctxStart--; + ctxLen++; + if (source[ctxStart] == "\n") { + if (++ctxLines == 3) { + break; + } + } + } + + ctxLen = 0; + ctxLines = 0; + while (ctxLen < 100 && ctxEnd < source.length - 1) { + ctxEnd++; + ctxLen++; + if (source[ctxEnd] == "\n") { + if (++ctxLines == 3) { + break; + } + } + } + return `${this.msg} ("${source.substring(ctxStart, ctxEnd + 1)}"): ${this.location}`; } } diff --git a/modules/angular2/src/compiler/template_parser.ts b/modules/angular2/src/compiler/template_parser.ts index 40d279b4b9..173e356dac 100644 --- a/modules/angular2/src/compiler/template_parser.ts +++ b/modules/angular2/src/compiler/template_parser.ts @@ -64,7 +64,7 @@ import {dashCaseToCamelCase, camelCaseToDashCase, splitAtColon} from './util'; // Group 7 = idenitifer inside [] // Group 8 = identifier inside () var BIND_NAME_REGEXP = - /^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g; + /^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/ig; const TEMPLATE_ELEMENT = 'template'; const TEMPLATE_ATTR = 'template'; @@ -218,7 +218,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { } }); - var isTemplateElement = nodeName == TEMPLATE_ELEMENT; + var isTemplateElement = nodeName.toLowerCase() == TEMPLATE_ELEMENT; var elementCssSelector = createElementCssSelector(nodeName, matchableAttrs); var directives = this._createDirectiveAsts( element.name, this._parseDirectives(this.selectorMatcher, elementCssSelector), @@ -266,7 +266,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean { var templateBindingsSource = null; - if (attr.name == TEMPLATE_ATTR) { + if (attr.name.toLowerCase() == TEMPLATE_ATTR) { templateBindingsSource = attr.value; } else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) { var key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length); // remove the star @@ -347,7 +347,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { } private _normalizeAttributeName(attrName: string): string { - return attrName.startsWith('data-') ? attrName.substring(5) : attrName; + return attrName.toLowerCase().startsWith('data-') ? attrName.substring(5) : attrName; } private _parseVariable(identifier: string, value: string, sourceSpan: ParseSourceSpan, @@ -542,21 +542,25 @@ class TemplateParseVisitor implements HtmlAstVisitor { `Can't bind to '${boundPropertyName}' since it isn't a known native property`, sourceSpan); } - } else if (parts[0] == ATTRIBUTE_PREFIX) { - boundPropertyName = dashCaseToCamelCase(parts[1]); - bindingType = PropertyBindingType.Attribute; - } else if (parts[0] == CLASS_PREFIX) { - // keep original case! - boundPropertyName = parts[1]; - bindingType = PropertyBindingType.Class; - } else if (parts[0] == STYLE_PREFIX) { - unit = parts.length > 2 ? parts[2] : null; - boundPropertyName = dashCaseToCamelCase(parts[1]); - bindingType = PropertyBindingType.Style; } else { - this._reportError(`Invalid property name ${name}`, sourceSpan); - bindingType = null; + let lcPrefix = parts[0].toLowerCase(); + if (lcPrefix == ATTRIBUTE_PREFIX) { + boundPropertyName = dashCaseToCamelCase(parts[1]); + bindingType = PropertyBindingType.Attribute; + } else if (lcPrefix == CLASS_PREFIX) { + // keep original case! + boundPropertyName = parts[1]; + bindingType = PropertyBindingType.Class; + } else if (lcPrefix == STYLE_PREFIX) { + unit = parts.length > 2 ? parts[2] : null; + boundPropertyName = dashCaseToCamelCase(parts[1]); + bindingType = PropertyBindingType.Style; + } else { + this._reportError(`Invalid property name ${name}`, sourceSpan); + bindingType = null; + } } + return new BoundElementPropertyAst(boundPropertyName, bindingType, ast, unit, sourceSpan); } diff --git a/modules/angular2/src/compiler/template_preparser.ts b/modules/angular2/src/compiler/template_preparser.ts index 28640b9fe0..25a8eb287c 100644 --- a/modules/angular2/src/compiler/template_preparser.ts +++ b/modules/angular2/src/compiler/template_preparser.ts @@ -17,18 +17,19 @@ export function preparseElement(ast: HtmlElementAst): PreparsedElement { var relAttr = null; var nonBindable = false; ast.attrs.forEach(attr => { - if (attr.name == NG_CONTENT_SELECT_ATTR) { + let attrName = attr.name.toLowerCase(); + if (attrName == NG_CONTENT_SELECT_ATTR) { selectAttr = attr.value; - } else if (attr.name == LINK_STYLE_HREF_ATTR) { + } else if (attrName == LINK_STYLE_HREF_ATTR) { hrefAttr = attr.value; - } else if (attr.name == LINK_STYLE_REL_ATTR) { + } else if (attrName == LINK_STYLE_REL_ATTR) { relAttr = attr.value; - } else if (attr.name == NG_NON_BINDABLE_ATTR) { + } else if (attrName == NG_NON_BINDABLE_ATTR) { nonBindable = true; } }); selectAttr = normalizeNgContentSelect(selectAttr); - var nodeName = ast.name; + var nodeName = ast.name.toLowerCase(); var type = PreparsedElementType.OTHER; if (nodeName == NG_CONTENT_ELEMENT) { type = PreparsedElementType.NG_CONTENT; diff --git a/modules/angular2/src/core/render/dom/dom_renderer.ts b/modules/angular2/src/core/render/dom/dom_renderer.ts index 633a2cb2d9..371f65e798 100644 --- a/modules/angular2/src/core/render/dom/dom_renderer.ts +++ b/modules/angular2/src/core/render/dom/dom_renderer.ts @@ -35,94 +35,8 @@ import {DefaultRenderView, DefaultRenderFragmentRef, DefaultProtoViewRef} from ' import {camelCaseToDashCase} from './util'; import {ViewEncapsulation} from 'angular2/src/core/metadata'; -// TODO(tbosch): solve SVG properly once https://github.com/angular/angular/issues/4417 is done -const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'; -const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; -const SVG_ELEMENT_NAMES = CONST_EXPR({ - 'altGlyph': true, - 'altGlyphDef': true, - 'altGlyphItem': true, - 'animate': true, - 'animateColor': true, - 'animateMotion': true, - 'animateTransform': true, - 'circle': true, - 'clipPath': true, - 'color-profile': true, - 'cursor': true, - 'defs': true, - 'desc': true, - 'ellipse': true, - 'feBlend': true, - 'feColorMatrix': true, - 'feComponentTransfer': true, - 'feComposite': true, - 'feConvolveMatrix': true, - 'feDiffuseLighting': true, - 'feDisplacementMap': true, - 'feDistantLight': true, - 'feFlood': true, - 'feFuncA': true, - 'feFuncB': true, - 'feFuncG': true, - 'feFuncR': true, - 'feGaussianBlur': true, - 'feImage': true, - 'feMerge': true, - 'feMergeNode': true, - 'feMorphology': true, - 'feOffset': true, - 'fePointLight': true, - 'feSpecularLighting': true, - 'feSpotLight': true, - 'feTile': true, - 'feTurbulence': true, - 'filter': true, - 'font': true, - 'font-face': true, - 'font-face-format': true, - 'font-face-name': true, - 'font-face-src': true, - 'font-face-uri': true, - 'foreignObject': true, - 'g': true, - // TODO(tbosch): this needs to be disabled - // because of an internal project. - // We will fix SVG soon, so this will go away... - // 'glyph': true, - 'glyphRef': true, - 'hkern': true, - 'image': true, - 'line': true, - 'linearGradient': true, - 'marker': true, - 'mask': true, - 'metadata': true, - 'missing-glyph': true, - 'mpath': true, - 'path': true, - 'pattern': true, - 'polygon': true, - 'polyline': true, - 'radialGradient': true, - 'rect': true, - 'set': true, - 'stop': true, - 'style': true, - 'svg': true, - 'switch': true, - 'symbol': true, - 'text': true, - 'textPath': true, - 'title': true, - 'tref': true, - 'tspan': true, - 'use': true, - 'view': true, - 'vkern': true -}); - -const SVG_ATTR_NAMESPACES = CONST_EXPR({'href': XLINK_NAMESPACE, 'xlink:href': XLINK_NAMESPACE}); +const NAMESPACE_URIS = + CONST_EXPR({'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg'}); export abstract class DomRenderer extends Renderer implements NodeFactory { abstract registerComponentTemplate(template: RenderComponentTemplate); @@ -364,24 +278,31 @@ export class DomRenderer_ extends DomRenderer { wtfLeave(s); } createElement(name: string, attrNameAndValues: string[]): Node { - var isSvg = SVG_ELEMENT_NAMES[name] == true; - var el = isSvg ? DOM.createElementNS(SVG_NAMESPACE, name) : DOM.createElement(name); - this._setAttributes(el, attrNameAndValues, isSvg); + var nsAndName = splitNamespace(name); + var el = isPresent(nsAndName[0]) ? + DOM.createElementNS(NAMESPACE_URIS[nsAndName[0]], nsAndName[1]) : + DOM.createElement(nsAndName[1]); + this._setAttributes(el, attrNameAndValues); return el; } mergeElement(existing: Node, attrNameAndValues: string[]) { DOM.clearNodes(existing); - this._setAttributes(existing, attrNameAndValues, false); + this._setAttributes(existing, attrNameAndValues); } - private _setAttributes(node: Node, attrNameAndValues: string[], isSvg: boolean) { + private _setAttributes(node: Node, attrNameAndValues: string[]) { for (var attrIdx = 0; attrIdx < attrNameAndValues.length; attrIdx += 2) { + var attrNs; var attrName = attrNameAndValues[attrIdx]; + var nsAndName = splitNamespace(attrName); + if (isPresent(nsAndName[0])) { + attrName = nsAndName[0] + ':' + nsAndName[1]; + attrNs = NAMESPACE_URIS[nsAndName[0]]; + } var attrValue = attrNameAndValues[attrIdx + 1]; - var attrNs = isSvg ? SVG_ATTR_NAMESPACES[attrName] : null; if (isPresent(attrNs)) { - DOM.setAttributeNS(node, XLINK_NAMESPACE, attrName, attrValue); + DOM.setAttributeNS(node, attrNs, attrName, attrValue); } else { - DOM.setAttribute(node, attrName, attrValue); + DOM.setAttribute(node, nsAndName[1], attrValue); } } } @@ -432,3 +353,13 @@ function decoratePreventDefault(eventHandler: Function): Function { } }; } + +var NS_PREFIX_RE = /^@([^:]+):(.+)/g; + +function splitNamespace(name: string): string[] { + if (name[0] != '@') { + return [null, name]; + } + let match = RegExpWrapper.firstMatch(NS_PREFIX_RE, name); + return [match[1], match[2]]; +} diff --git a/modules/angular2/test/compiler/html_lexer_spec.ts b/modules/angular2/test/compiler/html_lexer_spec.ts index 8c7d74d9d2..815a9da532 100644 --- a/modules/angular2/test/compiler/html_lexer_spec.ts +++ b/modules/angular2/test/compiler/html_lexer_spec.ts @@ -1,8 +1,22 @@ -import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from '../../test_lib'; -import {BaseException} from '../../src/facade/exceptions'; +import { + ddescribe, + describe, + it, + iit, + xit, + expect, + beforeEach, + afterEach +} from 'angular2/testing_internal'; +import {BaseException} from 'angular2/src/facade/exceptions'; -import {tokenizeHtml, HtmlToken, HtmlTokenType} from '../../src/compiler/html_lexer'; -import {ParseSourceSpan, ParseLocation} from '../../src/compiler/parse_util'; +import { + tokenizeHtml, + HtmlToken, + HtmlTokenType, + HtmlTokenError +} from 'angular2/src/compiler/html_lexer'; +import {ParseSourceSpan, ParseLocation, ParseSourceFile} from 'angular2/src/compiler/parse_util'; export function main() { describe('HtmlLexer', () => { @@ -263,6 +277,17 @@ export function main() { ]); }); + it('should parse attributes with "&" in values', () => { + expect(tokenizeAndHumanizeParts('')) + .toEqual([ + [HtmlTokenType.TAG_OPEN_START, null, 't'], + [HtmlTokenType.ATTR_NAME, null, 'a'], + [HtmlTokenType.ATTR_VALUE, 'b && c &'], + [HtmlTokenType.TAG_OPEN_END], + [HtmlTokenType.EOF] + ]); + }); + it('should store the locations', () => { expect(tokenizeAndHumanizeSourceSpans('')) .toEqual([ @@ -364,6 +389,11 @@ export function main() { .toEqual([[HtmlTokenType.TEXT, 'a&b'], [HtmlTokenType.EOF]]); }); + it('should parse text starting with "&"', () => { + expect(tokenizeAndHumanizeParts('a && b &')) + .toEqual([[HtmlTokenType.TEXT, 'a && b &'], [HtmlTokenType.EOF]]); + }); + it('should store the locations', () => { expect(tokenizeAndHumanizeSourceSpans('a')) .toEqual([[HtmlTokenType.TEXT, 'a'], [HtmlTokenType.EOF, '']]); @@ -486,6 +516,17 @@ export function main() { }); + describe('errors', () => { + it('should include 2 lines of context in message', () => { + let src = "111\n222\n333\nE\n444\n555\n666\n"; + let file = new ParseSourceFile(src, 'file://'); + let location = new ParseLocation(file, 12, 123, 456); + let error = new HtmlTokenError('**ERROR**', null, location); + expect(error.toString()) + .toEqual(`**ERROR** ("\n222\n333\nE\n444\n555\n"): file://@123:456`); + }); + }); + }); } diff --git a/modules/angular2/test/compiler/html_parser_spec.ts b/modules/angular2/test/compiler/html_parser_spec.ts index 21618378b2..68feda373d 100644 --- a/modules/angular2/test/compiler/html_parser_spec.ts +++ b/modules/angular2/test/compiler/html_parser_spec.ts @@ -9,8 +9,8 @@ import { afterEach } from 'angular2/testing_internal'; - -import {HtmlParser, HtmlParseTreeResult} from 'angular2/src/compiler/html_parser'; +import {HtmlTokenType} from 'angular2/src/compiler/html_lexer'; +import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from 'angular2/src/compiler/html_parser'; import { HtmlAst, HtmlAstVisitor, @@ -19,17 +19,15 @@ import { HtmlTextAst, htmlVisitAll } from 'angular2/src/compiler/html_ast'; +import {ParseError, ParseLocation, ParseSourceSpan} from 'angular2/src/compiler/parse_util'; + +import {BaseException} from 'angular2/src/facade/exceptions'; export function main() { describe('HtmlParser', () => { var parser: HtmlParser; beforeEach(() => { parser = new HtmlParser(); }); - // TODO: add more test cases - // TODO: separate tests for source spans from tests for tree parsing - // TODO: find a better way to assert the tree structure! - // -> maybe with arrays and object hashes!! - describe('parse', () => { describe('text nodes', () => { it('should parse root level text nodes', () => { @@ -38,37 +36,91 @@ export function main() { it('should parse text nodes inside regular elements', () => { expect(humanizeDom(parser.parse('
a
', 'TestComp'))) - .toEqual([[HtmlElementAst, 'div'], [HtmlTextAst, 'a']]); + .toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a']]); }); it('should parse text nodes inside template elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([[HtmlElementAst, 'template'], [HtmlTextAst, 'a']]); + .toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a']]); + }); + + it('should parse CDATA', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([[HtmlTextAst, 'text']]); }); }); describe('elements', () => { it('should parse root level elements', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))) - .toEqual([[HtmlElementAst, 'div']]); + .toEqual([[HtmlElementAst, 'div', 0]]); }); it('should parse elements inside of regular elements', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))) - .toEqual([[HtmlElementAst, 'div'], [HtmlElementAst, 'span']]); + .toEqual([[HtmlElementAst, 'div', 0], [HtmlElementAst, 'span', 1]]); }); it('should parse elements inside of template elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([[HtmlElementAst, 'template'], [HtmlElementAst, 'span']]); + .toEqual([[HtmlElementAst, 'template', 0], [HtmlElementAst, 'span', 1]]); + }); + + it('should support void elements', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'link', 0], + [HtmlAttrAst, 'rel', 'author license'], + [HtmlAttrAst, 'href', '/about'], + ]); + }); + + it('should support optional end tags', () => { + expect(humanizeDom(parser.parse('

1

2

', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'div', 0], + [HtmlElementAst, 'p', 1], + [HtmlTextAst, '1'], + [HtmlElementAst, 'p', 1], + [HtmlTextAst, '2'], + ]); + }); + + it('should add the requiredParent', () => { + expect(humanizeDom(parser.parse('
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'table', 0], + [HtmlElementAst, 'tbody', 1], + [HtmlElementAst, 'tr', 2], + ]); + }); + + it('should support explicit mamespace', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([[HtmlElementAst, '@myns:div', 0]]); + }); + + it('should support implicit mamespace', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([[HtmlElementAst, '@svg:svg', 0]]); + }); + + it('should propagate the namespace', () => { + expect(humanizeDom(parser.parse('

', 'TestComp'))) + .toEqual([[HtmlElementAst, '@myns:div', 0], [HtmlElementAst, '@myns:p', 1]]); + }); + + it('should match closing tags case insensitive', () => { + expect(humanizeDom(parser.parse('

', 'TestComp'))) + .toEqual([[HtmlElementAst, 'DiV', 0], [HtmlElementAst, 'P', 1]]); }); }); describe('attributes', () => { - it('should parse attributes on regular elements', () => { + it('should parse attributes on regular elements case sensitive', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))) .toEqual([ - [HtmlElementAst, 'div'], + [HtmlElementAst, 'div', 0], [HtmlAttrAst, 'kEy', 'v'], [HtmlAttrAst, 'key2', 'v2'], ]); @@ -76,51 +128,135 @@ export function main() { it('should parse attributes without values', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))) - .toEqual([[HtmlElementAst, 'div'], [HtmlAttrAst, 'k', '']]); + .toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'k', '']]); }); it('should parse attributes on svg elements case sensitive', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([[HtmlElementAst, '@svg:svg'], [HtmlAttrAst, 'viewBox', '0']]); + .toEqual([[HtmlElementAst, '@svg:svg', 0], [HtmlAttrAst, 'viewBox', '0']]); }); it('should parse attributes on template elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([[HtmlElementAst, 'template'], [HtmlAttrAst, 'k', 'v']]); + .toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]); }); + it('should support mamespace', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([[HtmlElementAst, 'use', 0], [HtmlAttrAst, '@xlink:href', 'Port']]); + }); + }); + + describe('comments', () => { + it('should ignore comments', () => { + expect(humanizeDom(parser.parse('
', 'TestComp'))) + .toEqual([[HtmlElementAst, 'div', 0]]); + }); + }); + + describe('source spans', () => { + it('should store the location', () => { + expect(humanizeDomSourceSpans(parser.parse( + '
\na\n
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'div', 0, '
'], + [HtmlAttrAst, '[prop]', 'v1', '[prop]="v1"'], + [HtmlAttrAst, '(e)', 'do()', '(e)="do()"'], + [HtmlAttrAst, 'attr', 'v2', 'attr="v2"'], + [HtmlAttrAst, 'noValue', '', 'noValue'], + [HtmlTextAst, '\na\n', '\na\n'], + ]); + }); + }); + + describe('errors', () => { + it('should report unexpected closing tags', () => { + let errors = parser.parse('

', 'TestComp').errors; + expect(errors.length).toEqual(1); + expect(humanizeErrors(errors)).toEqual([['p', 'Unexpected closing tag "p"', '0:5']]); + }); + + it('should also report lexer errors', () => { + let errors = parser.parse('

', 'TestComp').errors; + expect(errors.length).toEqual(2); + expect(humanizeErrors(errors)) + .toEqual([ + [HtmlTokenType.COMMENT_START, 'Unexpected character "e"', '0:3'], + ['p', 'Unexpected closing tag "p"', '0:14'] + ]); + }); }); }); }); } function humanizeDom(parseResult: HtmlParseTreeResult): any[] { - // TODO: humanize errors as well! if (parseResult.errors.length > 0) { - throw parseResult.errors; + var errorString = parseResult.errors.join('\n'); + throw new BaseException(`Unexpected parse errors:\n${errorString}`); } - var humanizer = new Humanizer(); + + var humanizer = new Humanizer(false); htmlVisitAll(humanizer, parseResult.rootNodes); return humanizer.result; } +function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] { + if (parseResult.errors.length > 0) { + var errorString = parseResult.errors.join('\n'); + throw new BaseException(`Unexpected parse errors:\n${errorString}`); + } + + var humanizer = new Humanizer(true); + htmlVisitAll(humanizer, parseResult.rootNodes); + return humanizer.result; +} + +function humanizeLineColumn(location: ParseLocation): string { + return `${location.line}:${location.col}`; +} + +function humanizeErrors(errors: ParseError[]): any[] { + return errors.map(error => { + if (error instanceof HtmlTreeError) { + // Parser errors + return [error.elementName, error.msg, humanizeLineColumn(error.location)]; + } + // Tokenizer errors + return [(error).tokenType, error.msg, humanizeLineColumn(error.location)]; + }); +} + class Humanizer implements HtmlAstVisitor { result: any[] = []; + elDepth: number = 0; + + constructor(private includeSourceSpan: boolean){}; visitElement(ast: HtmlElementAst, context: any): any { - this.result.push([HtmlElementAst, ast.name]); + var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]); + this.result.push(res); htmlVisitAll(this, ast.attrs); htmlVisitAll(this, ast.children); + this.elDepth--; return null; } visitAttr(ast: HtmlAttrAst, context: any): any { - this.result.push([HtmlAttrAst, ast.name, ast.value]); + var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]); + this.result.push(res); return null; } visitText(ast: HtmlTextAst, context: any): any { - this.result.push([HtmlTextAst, ast.value]); + var res = this._appendContext(ast, [HtmlTextAst, ast.value]); + this.result.push(res); return null; } + + private _appendContext(ast: HtmlAst, input: any[]): any[] { + if (!this.includeSourceSpan) return input; + input.push(ast.sourceSpan.toString()); + return input; + } } diff --git a/modules/angular2/test/compiler/template_parser_spec.ts b/modules/angular2/test/compiler/template_parser_spec.ts index 9e288b65f2..de2e2df116 100644 --- a/modules/angular2/test/compiler/template_parser_spec.ts +++ b/modules/angular2/test/compiler/template_parser_spec.ts @@ -45,9 +45,6 @@ import {Unparser} from '../core/change_detection/parser/unparser'; var expressionUnparser = new Unparser(); -// TODO(tbosch): add tests for checking that we -// keep the correct sourceSpans! - export function main() { describe('TemplateParser', () => { beforeEachProviders(() => [ @@ -76,27 +73,35 @@ export function main() { describe('nodes without bindings', () => { it('should parse text nodes', - () => { expect(humanizeTemplateAsts(parse('a', []))).toEqual([[TextAst, 'a']]); }); + () => { expect(humanizeTplAst(parse('a', []))).toEqual([[TextAst, 'a']]); }); it('should parse elements with attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [AttrAst, 'a', 'b']]); }); }); it('should parse ngContent', () => { var parsed = parse('', []); - expect(humanizeTemplateAsts(parsed)).toEqual([[NgContentAst]]); + expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); }); it('should parse bound text nodes', () => { - expect(humanizeTemplateAsts(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); + expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); }); describe('bound properties', () => { + it('should parse mixed case bound properties', () => { + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', 'v', null] + ]); + }); + it('should parse and camel case bound properties', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', 'v', null] @@ -104,31 +109,71 @@ export function main() { }); it('should normalize property names via the element schema', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'mappedProp', 'v', null] ]); }); - it('should parse and camel case bound attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + it('should parse mixed case bound attributes', () => { + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] ]); }); + it('should parse and camel case bound attributes', () => { + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] + ]); + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] + ]); + + }); + it('should parse and dash case bound classes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Class, 'some-class', 'v', null] + ]); + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Class, 'some-class', 'v', null] ]); }); + it('should parse mixed case bound classes', () => { + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Class, 'someClass', 'v', null] + ]); + }); + it('should parse and camel case bound styles', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] + ]); + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] + ]); + }); + + it('should parse and mixed case bound styles', () => { + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] @@ -136,7 +181,7 @@ export function main() { }); it('should parse bound properties via [...] and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] @@ -144,7 +189,12 @@ export function main() { }); it('should parse bound properties via bind- and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] + ]); + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] @@ -152,7 +202,7 @@ export function main() { }); it('should parse bound properties via {{...}} and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null] @@ -164,22 +214,29 @@ export function main() { describe('events', () => { it('should parse bound events with a target', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', 'window', 'v']]); }); it('should parse bound events via (...) and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); }); - it('should camel case event names', () => { - expect(humanizeTemplateAsts(parse('
', []))) + it('should parse and camel case event names', () => { + expect(humanizeTplAst(parse('
', []))) + .toEqual([[ElementAst, 'div'], [BoundEventAst, 'someEvent', null, 'v']]); + }); + + it('should parse mixed case event names', () => { + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [BoundEventAst, 'someEvent', null, 'v']]); }); it('should parse bound events via on- and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) + .toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); }); @@ -190,7 +247,7 @@ export function main() { outputs: ['e'], type: new CompileTypeMetadata({name: 'DirA'}) }); - expect(humanizeTemplateAsts(parse('', [dirA]))) + expect(humanizeTplAst(parse('', [dirA]))) .toEqual([ [EmbeddedTemplateAst], [BoundEventAst, 'e', null, 'f'], @@ -202,7 +259,7 @@ export function main() { describe('bindon', () => { it('should parse bound events and properties via [(...)] and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], @@ -212,7 +269,13 @@ export function main() { it('should parse bound events and properties via bindon- and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) + .toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], + [BoundEventAst, 'propChange', null, 'v = $event'] + ]); + expect(humanizeTplAst(parse('
', []))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], @@ -237,7 +300,7 @@ export function main() { type: new CompileTypeMetadata({name: 'ZComp'}), template: new CompileTemplateMetadata({ngContentSelectors: []}) }); - expect(humanizeTemplateAsts(parse('
', [dirA, dirB, dirC, comp]))) + expect(humanizeTplAst(parse('
', [dirA, dirB, dirC, comp]))) .toEqual([ [ElementAst, 'div'], [AttrAst, 'a', ''], @@ -255,7 +318,7 @@ export function main() { {selector: '[a=b]', type: new CompileTypeMetadata({name: 'DirA'})}); var dirB = CompileDirectiveMetadata.create( {selector: '[b]', type: new CompileTypeMetadata({name: 'DirB'})}); - expect(humanizeTemplateAsts(parse('
', [dirA, dirB]))) + expect(humanizeTplAst(parse('
', [dirA, dirB]))) .toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'b', null], @@ -269,7 +332,7 @@ export function main() { type: new CompileTypeMetadata({name: 'DirA'}), host: {'[a]': 'expr'} }); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], @@ -283,7 +346,7 @@ export function main() { type: new CompileTypeMetadata({name: 'DirA'}), host: {'(a)': 'expr'} }); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual( [[ElementAst, 'div'], [DirectiveAst, dirA], [BoundEventAst, 'a', null, 'expr']]); }); @@ -291,7 +354,7 @@ export function main() { it('should parse directive properties', () => { var dirA = CompileDirectiveMetadata.create( {selector: 'div', type: new CompileTypeMetadata({name: 'DirA'}), inputs: ['aProp']}); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], @@ -302,7 +365,7 @@ export function main() { it('should parse renamed directive properties', () => { var dirA = CompileDirectiveMetadata.create( {selector: 'div', type: new CompileTypeMetadata({name: 'DirA'}), inputs: ['b:a']}); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], @@ -313,7 +376,7 @@ export function main() { it('should parse literal directive properties', () => { var dirA = CompileDirectiveMetadata.create( {selector: 'div', type: new CompileTypeMetadata({name: 'DirA'}), inputs: ['a']}); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [AttrAst, 'a', 'literal'], @@ -325,7 +388,7 @@ export function main() { it('should favor explicit bound properties over literal properties', () => { var dirA = CompileDirectiveMetadata.create( {selector: 'div', type: new CompileTypeMetadata({name: 'DirA'}), inputs: ['a']}); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [AttrAst, 'a', 'literal'], @@ -337,7 +400,7 @@ export function main() { it('should support optional directive properties', () => { var dirA = CompileDirectiveMetadata.create( {selector: 'div', type: new CompileTypeMetadata({name: 'DirA'}), inputs: ['a']}); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([[ElementAst, 'div'], [DirectiveAst, dirA]]); }); @@ -346,29 +409,31 @@ export function main() { describe('variables', () => { it('should parse variables via #... and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [VariableAst, 'a', '']]); }); it('should parse variables via var-... and not report them as attributes', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) + .toEqual([[ElementAst, 'div'], [VariableAst, 'a', '']]); + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [VariableAst, 'a', '']]); }); it('should camel case variables', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [VariableAst, 'someA', '']]); }); it('should assign variables with empty value to the element', () => { - expect(humanizeTemplateAsts(parse('
', []))) + expect(humanizeTplAst(parse('
', []))) .toEqual([[ElementAst, 'div'], [VariableAst, 'a', '']]); }); it('should assign variables to directives via exportAs', () => { var dirA = CompileDirectiveMetadata.create( {selector: '[a]', type: new CompileTypeMetadata({name: 'DirA'}), exportAs: 'dirA'}); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [AttrAst, 'a', ''], @@ -379,12 +444,12 @@ export function main() { it('should report variables with values that dont match a directive as errors', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: -There is no directive with "exportAs" set to "dirA" (
): TestComp@0:5`); +There is no directive with "exportAs" set to "dirA" ("
"): TestComp@0:5`); }); it('should allow variables with values that dont match a directive on embedded template elements', () => { - expect(humanizeTemplateAsts(parse('', []))) + expect(humanizeTplAst(parse('', []))) .toEqual([[EmbeddedTemplateAst], [VariableAst, 'a', 'b']]); }); @@ -396,7 +461,7 @@ There is no directive with "exportAs" set to "dirA" (
): TestComp@ exportAs: 'dirA', template: new CompileTemplateMetadata({ngContentSelectors: []}) }); - expect(humanizeTemplateAsts(parse('
', [dirA]))) + expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [AttrAst, 'a', ''], @@ -410,19 +475,23 @@ There is no directive with "exportAs" set to "dirA" (
): TestComp@ describe('explicit templates', () => { it('should create embedded templates for