refactor(compiler): re-arrange files

This commit is contained in:
Victor Berchet
2016-07-21 11:41:25 -07:00
parent cc5cfe87c3
commit 1b77604ee2
58 changed files with 176 additions and 170 deletions
@@ -0,0 +1,117 @@
/**
* @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 {ParseError, ParseSourceSpan} from '../parse_util';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from './html_ast';
// http://cldr.unicode.org/index/cldr-spec/plural-rules
const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
/**
* Expands special forms into elements.
*
* For example,
*
* ```
* { messages.length, plural,
* =0 {zero}
* =1 {one}
* other {more than one}
* }
* ```
*
* will be expanded into
*
* ```
* <ng-container [ngPlural]="messages.length">
* <template ngPluralCase="=0">zero</ng-container>
* <template ngPluralCase="=1">one</ng-container>
* <template ngPluralCase="other">more than one</ng-container>
* </ng-container>
* ```
*/
export function expandNodes(nodes: HtmlAst[]): ExpansionResult {
const expander = new _Expander();
return new ExpansionResult(htmlVisitAll(expander, nodes), expander.isExpanded, expander.errors);
}
export class ExpansionResult {
constructor(public nodes: HtmlAst[], public expanded: boolean, public errors: ParseError[]) {}
}
export class ExpansionError extends ParseError {
constructor(span: ParseSourceSpan, errorMsg: string) { super(span, errorMsg); }
}
/**
* Expand expansion forms (plural, select) to directives
*
* @internal
*/
class _Expander implements HtmlAstVisitor {
isExpanded: boolean = false;
errors: ParseError[] = [];
visitElement(ast: HtmlElementAst, context: any): any {
return new HtmlElementAst(
ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan, ast.startSourceSpan,
ast.endSourceSpan);
}
visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
visitText(ast: HtmlTextAst, context: any): any { return ast; }
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
this.isExpanded = true;
return ast.type == 'plural' ? _expandPluralForm(ast, this.errors) :
_expandDefaultForm(ast, this.errors);
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
throw new Error('Should not be reached');
}
}
function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
const children = ast.cases.map(c => {
if (PLURAL_CASES.indexOf(c.value) == -1 && !c.value.match(/^=\d+$/)) {
errors.push(new ExpansionError(
c.valueSourceSpan,
`Plural cases should be "=<number>" or one of ${PLURAL_CASES.join(", ")}`));
}
const expansionResult = expandNodes(c.expression);
errors.push(...expansionResult.errors);
return new HtmlElementAst(
`template`, [new HtmlAttrAst('ngPluralCase', `${c.value}`, c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
const switchAttr = new HtmlAttrAst('[ngPlural]', ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst(
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
}
function _expandDefaultForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
let children = ast.cases.map(c => {
const expansionResult = expandNodes(c.expression);
errors.push(...expansionResult.errors);
return new HtmlElementAst(
`template`, [new HtmlAttrAst('ngSwitchCase', `${c.value}`, c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
const switchAttr = new HtmlAttrAst('[ngSwitch]', ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst(
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
}
@@ -0,0 +1,78 @@
/**
* @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 {isPresent} from '../facade/lang';
import {ParseSourceSpan} from '../parse_util';
export interface HtmlAst {
sourceSpan: ParseSourceSpan;
visit(visitor: HtmlAstVisitor, context: any): any;
}
export class HtmlTextAst implements HtmlAst {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class HtmlExpansionAst implements HtmlAst {
constructor(
public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any {
return visitor.visitExpansion(this, context);
}
}
export class HtmlExpansionCaseAst implements HtmlAst {
constructor(
public value: string, public expression: HtmlAst[], public sourceSpan: ParseSourceSpan,
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any {
return visitor.visitExpansionCase(this, context);
}
}
export class HtmlAttrAst implements HtmlAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
export class HtmlElementAst implements HtmlAst {
constructor(
public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); }
}
export class HtmlCommentAst implements HtmlAst {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitComment(this, context); }
}
export interface HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any;
visitAttr(ast: HtmlAttrAst, context: any): any;
visitText(ast: HtmlTextAst, context: any): any;
visitComment(ast: HtmlCommentAst, context: any): any;
visitExpansion(ast: HtmlExpansionAst, context: any): any;
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
}
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {
var result: any[] = [];
asts.forEach(ast => {
var astResult = ast.visit(visitor, context);
if (isPresent(astResult)) {
result.push(astResult);
}
});
return result;
}
@@ -0,0 +1,707 @@
/**
* @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 * as chars from '../chars';
import {isBlank, isPresent} from '../facade/lang';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
import {HtmlTagContentType, NAMED_ENTITIES, getHtmlTagDefinition} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
export enum HtmlTokenType {
TAG_OPEN_START,
TAG_OPEN_END,
TAG_OPEN_END_VOID,
TAG_CLOSE,
TEXT,
ESCAPABLE_RAW_TEXT,
RAW_TEXT,
COMMENT_START,
COMMENT_END,
CDATA_START,
CDATA_END,
ATTR_NAME,
ATTR_VALUE,
DOC_TYPE,
EXPANSION_FORM_START,
EXPANSION_CASE_VALUE,
EXPANSION_CASE_EXP_START,
EXPANSION_CASE_EXP_END,
EXPANSION_FORM_END,
EOF
}
export class HtmlToken {
constructor(
public type: HtmlTokenType, public parts: string[], public sourceSpan: ParseSourceSpan) {}
}
export class HtmlTokenError extends ParseError {
constructor(errorMsg: string, public tokenType: HtmlTokenType, span: ParseSourceSpan) {
super(span, errorMsg);
}
}
export class HtmlTokenizeResult {
constructor(public tokens: HtmlToken[], public errors: HtmlTokenError[]) {}
}
export function tokenizeHtml(
sourceContent: string, sourceUrl: string, tokenizeExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): HtmlTokenizeResult {
return new _HtmlTokenizer(
new ParseSourceFile(sourceContent, sourceUrl), tokenizeExpansionForms,
interpolationConfig)
.tokenize();
}
const _CR_OR_CRLF_REGEXP = /\r\n?/g;
function _unexpectedCharacterErrorMsg(charCode: number): string {
const char = charCode === chars.$EOF ? 'EOF' : String.fromCharCode(charCode);
return `Unexpected character "${char}"`;
}
function _unknownEntityErrorMsg(entitySrc: string): string {
return `Unknown entity "${entitySrc}" - use the "&#<decimal>;" or "&#x<hex>;" syntax`;
}
class _ControlFlowError {
constructor(public error: HtmlTokenError) {}
}
// See http://www.w3.org/TR/html51/syntax.html#writing
class _HtmlTokenizer {
private _input: string;
private _length: number;
// Note: this is always lowercase!
private _peek: number = -1;
private _nextPeek: number = -1;
private _index: number = -1;
private _line: number = 0;
private _column: number = -1;
private _currentTokenStart: ParseLocation;
private _currentTokenType: HtmlTokenType;
private _expansionCaseStack: HtmlTokenType[] = [];
private _inInterpolation: boolean = false;
tokens: HtmlToken[] = [];
errors: HtmlTokenError[] = [];
/**
* @param _file The html source
* @param _tokenizeIcu Whether to tokenize ICU messages (considered as text nodes when false)
* @param _interpolationConfig
*/
constructor(
private _file: ParseSourceFile, private _tokenizeIcu: boolean,
private _interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
this._input = _file.content;
this._length = _file.content.length;
this._advance();
}
private _processCarriageReturns(content: string): string {
// http://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
// In order to keep the original position in the source, we can not
// pre-process it.
// Instead CRs are processed right before instantiating the tokens.
return content.replace(_CR_OR_CRLF_REGEXP, '\n');
}
tokenize(): HtmlTokenizeResult {
while (this._peek !== chars.$EOF) {
const start = this._getLocation();
try {
if (this._attemptCharCode(chars.$LT)) {
if (this._attemptCharCode(chars.$BANG)) {
if (this._attemptCharCode(chars.$LBRACKET)) {
this._consumeCdata(start);
} else if (this._attemptCharCode(chars.$MINUS)) {
this._consumeComment(start);
} else {
this._consumeDocType(start);
}
} else if (this._attemptCharCode(chars.$SLASH)) {
this._consumeTagClose(start);
} else {
this._consumeTagOpen(start);
}
} else if (!this._tokenizeIcu || !this._tokenizeExpansionForm()) {
this._consumeText();
}
} catch (e) {
if (e instanceof _ControlFlowError) {
this.errors.push(e.error);
} else {
throw e;
}
}
}
this._beginToken(HtmlTokenType.EOF);
this._endToken([]);
return new HtmlTokenizeResult(mergeTextTokens(this.tokens), this.errors);
}
/**
* @returns {boolean} whether an ICU token has been created
* @internal
*/
private _tokenizeExpansionForm(): boolean {
if (isExpansionFormStart(this._input, this._index, this._interpolationConfig)) {
this._consumeExpansionFormStart();
return true;
}
if (isExpansionCaseStart(this._peek) && this._isInExpansionForm()) {
this._consumeExpansionCaseStart();
return true;
}
if (this._peek === chars.$RBRACE) {
if (this._isInExpansionCase()) {
this._consumeExpansionCaseEnd();
return true;
}
if (this._isInExpansionForm()) {
this._consumeExpansionFormEnd();
return true;
}
}
return false;
}
private _getLocation(): ParseLocation {
return new ParseLocation(this._file, this._index, this._line, this._column);
}
private _getSpan(
start: ParseLocation = this._getLocation(),
end: ParseLocation = this._getLocation()): ParseSourceSpan {
return new ParseSourceSpan(start, end);
}
private _beginToken(type: HtmlTokenType, start: ParseLocation = this._getLocation()) {
this._currentTokenStart = start;
this._currentTokenType = type;
}
private _endToken(parts: string[], end: ParseLocation = this._getLocation()): HtmlToken {
const token = new HtmlToken(
this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
this.tokens.push(token);
this._currentTokenStart = null;
this._currentTokenType = null;
return token;
}
private _createError(msg: string, span: ParseSourceSpan): _ControlFlowError {
if (this._isInExpansionForm()) {
msg += ' (Do you have an unescaped "{" in your template?).';
}
const error = new HtmlTokenError(msg, this._currentTokenType, span);
this._currentTokenStart = null;
this._currentTokenType = null;
return new _ControlFlowError(error);
}
private _advance() {
if (this._index >= this._length) {
throw this._createError(_unexpectedCharacterErrorMsg(chars.$EOF), this._getSpan());
}
if (this._peek === chars.$LF) {
this._line++;
this._column = 0;
} else if (this._peek !== chars.$LF && this._peek !== chars.$CR) {
this._column++;
}
this._index++;
this._peek = this._index >= this._length ? chars.$EOF : this._input.charCodeAt(this._index);
this._nextPeek =
this._index + 1 >= this._length ? chars.$EOF : this._input.charCodeAt(this._index + 1);
}
private _attemptCharCode(charCode: number): boolean {
if (this._peek === charCode) {
this._advance();
return true;
}
return false;
}
private _attemptCharCodeCaseInsensitive(charCode: number): boolean {
if (compareCharCodeCaseInsensitive(this._peek, charCode)) {
this._advance();
return true;
}
return false;
}
private _requireCharCode(charCode: number) {
const location = this._getLocation();
if (!this._attemptCharCode(charCode)) {
throw this._createError(
_unexpectedCharacterErrorMsg(this._peek), this._getSpan(location, location));
}
}
private _attemptStr(chars: string): boolean {
const len = chars.length;
if (this._index + len > this._length) {
return false;
}
const initialPosition = this._savePosition();
for (let i = 0; i < len; i++) {
if (!this._attemptCharCode(chars.charCodeAt(i))) {
// If attempting to parse the string fails, we want to reset the parser
// to where it was before the attempt
this._restorePosition(initialPosition);
return false;
}
}
return true;
}
private _attemptStrCaseInsensitive(chars: string): boolean {
for (let i = 0; i < chars.length; i++) {
if (!this._attemptCharCodeCaseInsensitive(chars.charCodeAt(i))) {
return false;
}
}
return true;
}
private _requireStr(chars: string) {
const location = this._getLocation();
if (!this._attemptStr(chars)) {
throw this._createError(_unexpectedCharacterErrorMsg(this._peek), this._getSpan(location));
}
}
private _attemptCharCodeUntilFn(predicate: (code: number) => boolean) {
while (!predicate(this._peek)) {
this._advance();
}
}
private _requireCharCodeUntilFn(predicate: (code: number) => boolean, len: number) {
const start = this._getLocation();
this._attemptCharCodeUntilFn(predicate);
if (this._index - start.offset < len) {
throw this._createError(
_unexpectedCharacterErrorMsg(this._peek), this._getSpan(start, start));
}
}
private _attemptUntilChar(char: number) {
while (this._peek !== char) {
this._advance();
}
}
private _readChar(decodeEntities: boolean): string {
if (decodeEntities && this._peek === chars.$AMPERSAND) {
return this._decodeEntity();
} else {
const index = this._index;
this._advance();
return this._input[index];
}
}
private _decodeEntity(): string {
const start = this._getLocation();
this._advance();
if (this._attemptCharCode(chars.$HASH)) {
let isHex = this._attemptCharCode(chars.$x) || this._attemptCharCode(chars.$X);
let numberStart = this._getLocation().offset;
this._attemptCharCodeUntilFn(isDigitEntityEnd);
if (this._peek != chars.$SEMICOLON) {
throw this._createError(_unexpectedCharacterErrorMsg(this._peek), this._getSpan());
}
this._advance();
let strNum = this._input.substring(numberStart, this._index - 1);
try {
let charCode = parseInt(strNum, isHex ? 16 : 10);
return String.fromCharCode(charCode);
} catch (e) {
let entity = this._input.substring(start.offset + 1, this._index - 1);
throw this._createError(_unknownEntityErrorMsg(entity), this._getSpan(start));
}
} else {
let startPosition = this._savePosition();
this._attemptCharCodeUntilFn(isNamedEntityEnd);
if (this._peek != chars.$SEMICOLON) {
this._restorePosition(startPosition);
return '&';
}
this._advance();
let name = this._input.substring(start.offset + 1, this._index - 1);
let char = (NAMED_ENTITIES as any)[name];
if (isBlank(char)) {
throw this._createError(_unknownEntityErrorMsg(name), this._getSpan(start));
}
return char;
}
}
private _consumeRawText(
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): HtmlToken {
let tagCloseStart: ParseLocation;
const textStart = this._getLocation();
this._beginToken(
decodeEntities ? HtmlTokenType.ESCAPABLE_RAW_TEXT : HtmlTokenType.RAW_TEXT, textStart);
const parts: string[] = [];
while (true) {
tagCloseStart = this._getLocation();
if (this._attemptCharCode(firstCharOfEnd) && attemptEndRest()) {
break;
}
if (this._index > tagCloseStart.offset) {
// add the characters consumed by the previous if statement to the output
parts.push(this._input.substring(tagCloseStart.offset, this._index));
}
while (this._peek !== firstCharOfEnd) {
parts.push(this._readChar(decodeEntities));
}
}
return this._endToken([this._processCarriageReturns(parts.join(''))], tagCloseStart);
}
private _consumeComment(start: ParseLocation) {
this._beginToken(HtmlTokenType.COMMENT_START, start);
this._requireCharCode(chars.$MINUS);
this._endToken([]);
const textToken = this._consumeRawText(false, chars.$MINUS, () => this._attemptStr('->'));
this._beginToken(HtmlTokenType.COMMENT_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeCdata(start: ParseLocation) {
this._beginToken(HtmlTokenType.CDATA_START, start);
this._requireStr('CDATA[');
this._endToken([]);
const textToken = this._consumeRawText(false, chars.$RBRACKET, () => this._attemptStr(']>'));
this._beginToken(HtmlTokenType.CDATA_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeDocType(start: ParseLocation) {
this._beginToken(HtmlTokenType.DOC_TYPE, start);
this._attemptUntilChar(chars.$GT);
this._advance();
this._endToken([this._input.substring(start.offset + 2, this._index - 1)]);
}
private _consumePrefixAndName(): string[] {
const nameOrPrefixStart = this._index;
let prefix: string = null;
while (this._peek !== chars.$COLON && !isPrefixEnd(this._peek)) {
this._advance();
}
let nameStart: number;
if (this._peek === chars.$COLON) {
this._advance();
prefix = this._input.substring(nameOrPrefixStart, this._index - 1);
nameStart = this._index;
} else {
nameStart = nameOrPrefixStart;
}
this._requireCharCodeUntilFn(isNameEnd, this._index === nameStart ? 1 : 0);
const name = this._input.substring(nameStart, this._index);
return [prefix, name];
}
private _consumeTagOpen(start: ParseLocation) {
let savedPos = this._savePosition();
let lowercaseTagName: string;
try {
if (!chars.isAsciiLetter(this._peek)) {
throw this._createError(_unexpectedCharacterErrorMsg(this._peek), this._getSpan());
}
const nameStart = this._index;
this._consumeTagOpenStart(start);
lowercaseTagName = this._input.substring(nameStart, this._index).toLowerCase();
this._attemptCharCodeUntilFn(isNotWhitespace);
while (this._peek !== chars.$SLASH && this._peek !== chars.$GT) {
this._consumeAttributeName();
this._attemptCharCodeUntilFn(isNotWhitespace);
if (this._attemptCharCode(chars.$EQ)) {
this._attemptCharCodeUntilFn(isNotWhitespace);
this._consumeAttributeValue();
}
this._attemptCharCodeUntilFn(isNotWhitespace);
}
this._consumeTagOpenEnd();
} catch (e) {
if (e instanceof _ControlFlowError) {
// When the start tag is invalid, assume we want a "<"
this._restorePosition(savedPos);
// Back to back text tokens are merged at the end
this._beginToken(HtmlTokenType.TEXT, start);
this._endToken(['<']);
return;
}
throw e;
}
const contentTokenType = getHtmlTagDefinition(lowercaseTagName).contentType;
if (contentTokenType === HtmlTagContentType.RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, false);
} else if (contentTokenType === HtmlTagContentType.ESCAPABLE_RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, true);
}
}
private _consumeRawTextWithTagClose(lowercaseTagName: string, decodeEntities: boolean) {
const textToken = this._consumeRawText(decodeEntities, chars.$LT, () => {
if (!this._attemptCharCode(chars.$SLASH)) return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
if (!this._attemptStrCaseInsensitive(lowercaseTagName)) return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
return this._attemptCharCode(chars.$GT);
});
this._beginToken(HtmlTokenType.TAG_CLOSE, textToken.sourceSpan.end);
this._endToken([null, lowercaseTagName]);
}
private _consumeTagOpenStart(start: ParseLocation) {
this._beginToken(HtmlTokenType.TAG_OPEN_START, start);
const parts = this._consumePrefixAndName();
this._endToken(parts);
}
private _consumeAttributeName() {
this._beginToken(HtmlTokenType.ATTR_NAME);
const prefixAndName = this._consumePrefixAndName();
this._endToken(prefixAndName);
}
private _consumeAttributeValue() {
this._beginToken(HtmlTokenType.ATTR_VALUE);
var value: string;
if (this._peek === chars.$SQ || this._peek === chars.$DQ) {
var quoteChar = this._peek;
this._advance();
var parts: string[] = [];
while (this._peek !== quoteChar) {
parts.push(this._readChar(true));
}
value = parts.join('');
this._advance();
} else {
var valueStart = this._index;
this._requireCharCodeUntilFn(isNameEnd, 1);
value = this._input.substring(valueStart, this._index);
}
this._endToken([this._processCarriageReturns(value)]);
}
private _consumeTagOpenEnd() {
const tokenType = this._attemptCharCode(chars.$SLASH) ? HtmlTokenType.TAG_OPEN_END_VOID :
HtmlTokenType.TAG_OPEN_END;
this._beginToken(tokenType);
this._requireCharCode(chars.$GT);
this._endToken([]);
}
private _consumeTagClose(start: ParseLocation) {
this._beginToken(HtmlTokenType.TAG_CLOSE, start);
this._attemptCharCodeUntilFn(isNotWhitespace);
let prefixAndName = this._consumePrefixAndName();
this._attemptCharCodeUntilFn(isNotWhitespace);
this._requireCharCode(chars.$GT);
this._endToken(prefixAndName);
}
private _consumeExpansionFormStart() {
this._beginToken(HtmlTokenType.EXPANSION_FORM_START, this._getLocation());
this._requireCharCode(chars.$LBRACE);
this._endToken([]);
this._expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
const condition = this._readUntil(chars.$COMMA);
this._endToken([condition], this._getLocation());
this._requireCharCode(chars.$COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
let type = this._readUntil(chars.$COMMA);
this._endToken([type], this._getLocation());
this._requireCharCode(chars.$COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
}
private _consumeExpansionCaseStart() {
this._beginToken(HtmlTokenType.EXPANSION_CASE_VALUE, this._getLocation());
const value = this._readUntil(chars.$LBRACE).trim();
this._endToken([value], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_START, this._getLocation());
this._requireCharCode(chars.$LBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
}
private _consumeExpansionCaseEnd() {
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_END, this._getLocation());
this._requireCharCode(chars.$RBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.pop();
}
private _consumeExpansionFormEnd() {
this._beginToken(HtmlTokenType.EXPANSION_FORM_END, this._getLocation());
this._requireCharCode(chars.$RBRACE);
this._endToken([]);
this._expansionCaseStack.pop();
}
private _consumeText() {
const start = this._getLocation();
this._beginToken(HtmlTokenType.TEXT, start);
const parts: string[] = [];
do {
if (this._attemptStr(this._interpolationConfig.start)) {
parts.push(this._interpolationConfig.start);
this._inInterpolation = true;
} else if (this._attemptStr(this._interpolationConfig.end) && this._inInterpolation) {
parts.push(this._interpolationConfig.end);
this._inInterpolation = false;
} else {
parts.push(this._readChar(true));
}
} while (!this._isTextEnd());
this._endToken([this._processCarriageReturns(parts.join(''))]);
}
private _isTextEnd(): boolean {
if (this._peek === chars.$LT || this._peek === chars.$EOF) {
return true;
}
if (this._tokenizeIcu && !this._inInterpolation) {
if (isExpansionFormStart(this._input, this._index, this._interpolationConfig)) {
// start of an expansion form
return true;
}
if (this._peek === chars.$RBRACE && this._isInExpansionCase()) {
// end of and expansion case
return true;
}
}
return false;
}
private _savePosition(): [number, number, number, number, number] {
return [this._peek, this._index, this._column, this._line, this.tokens.length];
}
private _readUntil(char: number): string {
let start = this._index;
this._attemptUntilChar(char);
return this._input.substring(start, this._index);
}
private _restorePosition(position: [number, number, number, number, number]): void {
this._peek = position[0];
this._index = position[1];
this._column = position[2];
this._line = position[3];
let nbTokens = position[4];
if (nbTokens < this.tokens.length) {
// remove any extra tokens
this.tokens = this.tokens.slice(0, nbTokens);
}
}
private _isInExpansionCase(): boolean {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
HtmlTokenType.EXPANSION_CASE_EXP_START;
}
private _isInExpansionForm(): boolean {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
HtmlTokenType.EXPANSION_FORM_START;
}
}
function isNotWhitespace(code: number): boolean {
return !chars.isWhitespace(code) || code === chars.$EOF;
}
function isNameEnd(code: number): boolean {
return chars.isWhitespace(code) || code === chars.$GT || code === chars.$SLASH ||
code === chars.$SQ || code === chars.$DQ || code === chars.$EQ;
}
function isPrefixEnd(code: number): boolean {
return (code < chars.$a || chars.$z < code) && (code < chars.$A || chars.$Z < code) &&
(code < chars.$0 || code > chars.$9);
}
function isDigitEntityEnd(code: number): boolean {
return code == chars.$SEMICOLON || code == chars.$EOF || !chars.isAsciiHexDigit(code);
}
function isNamedEntityEnd(code: number): boolean {
return code == chars.$SEMICOLON || code == chars.$EOF || !chars.isAsciiLetter(code);
}
function isExpansionFormStart(
input: string, offset: number, interpolationConfig: InterpolationConfig): boolean {
return input.charCodeAt(offset) == chars.$LBRACE &&
input.indexOf(interpolationConfig.start, offset) != offset;
}
function isExpansionCaseStart(peek: number): boolean {
return peek === chars.$EQ || chars.isAsciiLetter(peek);
}
function compareCharCodeCaseInsensitive(code1: number, code2: number): boolean {
return toUpperCaseCharCode(code1) == toUpperCaseCharCode(code2);
}
function toUpperCaseCharCode(code: number): number {
return code >= chars.$a && code <= chars.$z ? code - chars.$a + chars.$A : code;
}
function mergeTextTokens(srcTokens: HtmlToken[]): HtmlToken[] {
let dstTokens: HtmlToken[] = [];
let lastDstToken: HtmlToken;
for (let i = 0; i < srcTokens.length; i++) {
let token = srcTokens[i];
if (isPresent(lastDstToken) && lastDstToken.type == HtmlTokenType.TEXT &&
token.type == HtmlTokenType.TEXT) {
lastDstToken.parts[0] += token.parts[0];
lastDstToken.sourceSpan.end = token.sourceSpan.end;
} else {
lastDstToken = token;
dstTokens.push(lastDstToken);
}
}
return dstTokens;
}
@@ -0,0 +1,408 @@
/**
* @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 {Injectable} from '../../../core/index';
import {isPresent, isBlank,} from '../facade/lang';
import {ListWrapper} from '../facade/collection';
import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst} from './html_ast';
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
export class HtmlTreeError extends ParseError {
static create(elementName: string, span: ParseSourceSpan, msg: string): HtmlTreeError {
return new HtmlTreeError(elementName, span, msg);
}
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export class HtmlParseTreeResult {
constructor(public rootNodes: HtmlAst[], public errors: ParseError[]) {}
}
@Injectable()
export class HtmlParser {
parse(
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
HtmlParseTreeResult {
const tokensAndErrors =
tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms, interpolationConfig);
const treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
return new HtmlParseTreeResult(
treeAndErrors.rootNodes,
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
}
}
class TreeBuilder {
private index: number = -1;
private peek: HtmlToken;
private rootNodes: HtmlAst[] = [];
private errors: HtmlTreeError[] = [];
private elementStack: HtmlElementAst[] = [];
constructor(private tokens: HtmlToken[]) { this._advance(); }
build(): HtmlParseTreeResult {
while (this.peek.type !== HtmlTokenType.EOF) {
if (this.peek.type === HtmlTokenType.TAG_OPEN_START) {
this._consumeStartTag(this._advance());
} else if (this.peek.type === HtmlTokenType.TAG_CLOSE) {
this._consumeEndTag(this._advance());
} else if (this.peek.type === HtmlTokenType.CDATA_START) {
this._closeVoidElement();
this._consumeCdata(this._advance());
} else if (this.peek.type === HtmlTokenType.COMMENT_START) {
this._closeVoidElement();
this._consumeComment(this._advance());
} else if (
this.peek.type === HtmlTokenType.TEXT || this.peek.type === HtmlTokenType.RAW_TEXT ||
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
this._closeVoidElement();
this._consumeText(this._advance());
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
this._consumeExpansion(this._advance());
} else {
// Skip all other tokens...
this._advance();
}
}
return new HtmlParseTreeResult(this.rootNodes, this.errors);
}
private _advance(): HtmlToken {
const prev = this.peek;
if (this.index < this.tokens.length - 1) {
// Note: there is always an EOF token at the end
this.index++;
}
this.peek = this.tokens[this.index];
return prev;
}
private _advanceIf(type: HtmlTokenType): HtmlToken {
if (this.peek.type === type) {
return this._advance();
}
return null;
}
private _consumeCdata(startToken: HtmlToken) {
this._consumeText(this._advance());
this._advanceIf(HtmlTokenType.CDATA_END);
}
private _consumeComment(token: HtmlToken) {
const text = this._advanceIf(HtmlTokenType.RAW_TEXT);
this._advanceIf(HtmlTokenType.COMMENT_END);
const value = isPresent(text) ? text.parts[0].trim() : null;
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
}
private _consumeExpansion(token: HtmlToken) {
const switchValue = this._advance();
const type = this._advance();
const cases: HtmlExpansionCaseAst[] = [];
// read =
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
let expCase = this._parseExpansionCase();
if (isBlank(expCase)) return; // error
cases.push(expCase);
}
// read the final }
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
this.errors.push(
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
return;
}
this._advance();
const mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
this._addToParent(new HtmlExpansionAst(
switchValue.parts[0], type.parts[0], cases, mainSourceSpan, switchValue.sourceSpan));
}
private _parseExpansionCase(): HtmlExpansionCaseAst {
const value = this._advance();
// read {
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
this.errors.push(
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
return null;
}
// read until }
const start = this._advance();
const exp = this._collectExpansionExpTokens(start);
if (isBlank(exp)) return null;
const end = this._advance();
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
const parsedExp = new TreeBuilder(exp).build();
if (parsedExp.errors.length > 0) {
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
return null;
}
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
return new HtmlExpansionCaseAst(
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
}
private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] {
const exp: HtmlToken[] = [];
const expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START];
while (true) {
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START ||
this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_START) {
expansionFormStack.push(this.peek.type);
}
if (this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_END) {
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_CASE_EXP_START)) {
expansionFormStack.pop();
if (expansionFormStack.length == 0) return exp;
} else {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) {
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) {
expansionFormStack.pop();
} else {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EOF) {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
private _consumeText(token: HtmlToken) {
let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') {
const parent = this._getParentElement();
if (isPresent(parent) && parent.children.length == 0 &&
getHtmlTagDefinition(parent.name).ignoreFirstLf) {
text = text.substring(1);
}
}
if (text.length > 0) {
this._addToParent(new HtmlTextAst(text, token.sourceSpan));
}
}
private _closeVoidElement(): void {
if (this.elementStack.length > 0) {
const el = ListWrapper.last(this.elementStack);
if (getHtmlTagDefinition(el.name).isVoid) {
this.elementStack.pop();
}
}
}
private _consumeStartTag(startTagToken: HtmlToken) {
const prefix = startTagToken.parts[0];
const name = startTagToken.parts[1];
const attrs: HtmlAttrAst[] = [];
while (this.peek.type === HtmlTokenType.ATTR_NAME) {
attrs.push(this._consumeAttr(this._advance()));
}
const fullName = getElementFullName(prefix, name, this._getParentElement());
let selfClosing = false;
// Note: There could have been a tokenizer error
// so that we don't get a token for the end tag...
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
this._advance();
selfClosing = true;
if (getNsPrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create(
fullName, startTagToken.sourceSpan,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
}
} else if (this.peek.type === HtmlTokenType.TAG_OPEN_END) {
this._advance();
selfClosing = false;
}
const end = this.peek.sourceSpan.start;
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
const el = new HtmlElementAst(fullName, attrs, [], span, span, null);
this._pushElement(el);
if (selfClosing) {
this._popElement(fullName);
el.endSourceSpan = span;
}
}
private _pushElement(el: HtmlElementAst) {
if (this.elementStack.length > 0) {
const parentEl = ListWrapper.last(this.elementStack);
if (getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) {
this.elementStack.pop();
}
}
const tagDef = getHtmlTagDefinition(el.name);
const {parent, container} = this._getParentElementSkippingContainers();
if (isPresent(parent) && tagDef.requireExtraParent(parent.name)) {
const newParent = new HtmlElementAst(
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
this._insertBeforeContainer(parent, container, newParent);
}
this._addToParent(el);
this.elementStack.push(el);
}
private _consumeEndTag(endTagToken: HtmlToken) {
const fullName =
getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
if (this._getParentElement()) {
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
}
if (getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create(
fullName, endTagToken.sourceSpan,
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
} else if (!this._popElement(fullName)) {
this.errors.push(HtmlTreeError.create(
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
}
}
private _popElement(fullName: string): boolean {
for (let stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) {
const el = this.elementStack[stackIndex];
if (el.name == fullName) {
ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex);
return true;
}
if (!getHtmlTagDefinition(el.name).closedByParent) {
return false;
}
}
return false;
}
private _consumeAttr(attrName: HtmlToken): HtmlAttrAst {
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
let end = attrName.sourceSpan.end;
let value = '';
if (this.peek.type === HtmlTokenType.ATTR_VALUE) {
const valueToken = this._advance();
value = valueToken.parts[0];
end = valueToken.sourceSpan.end;
}
return new HtmlAttrAst(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end));
}
private _getParentElement(): HtmlElementAst {
return this.elementStack.length > 0 ? ListWrapper.last(this.elementStack) : null;
}
/**
* Returns the parent in the DOM and the container.
*
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
*/
private _getParentElementSkippingContainers():
{parent: HtmlElementAst, container: HtmlElementAst} {
let container: HtmlElementAst = null;
for (let i = this.elementStack.length - 1; i >= 0; i--) {
if (this.elementStack[i].name !== 'ng-container') {
return {parent: this.elementStack[i], container};
}
container = this.elementStack[i];
}
return {parent: ListWrapper.last(this.elementStack), container};
}
private _addToParent(node: HtmlAst) {
const parent = this._getParentElement();
if (isPresent(parent)) {
parent.children.push(node);
} else {
this.rootNodes.push(node);
}
}
/**
* Insert a node between the parent and the container.
* When no container is given, the node is appended as a child of the parent.
* Also updates the element stack accordingly.
*
* @internal
*/
private _insertBeforeContainer(
parent: HtmlElementAst, container: HtmlElementAst, node: HtmlElementAst) {
if (!container) {
this._addToParent(node);
this.elementStack.push(node);
} else {
if (parent) {
// replace the container with the new node in the children
const index = parent.children.indexOf(container);
parent.children[index] = node;
} else {
this.rootNodes.push(node);
}
node.children.push(container);
this.elementStack.splice(this.elementStack.indexOf(container), 0, node);
}
}
}
function getElementFullName(
prefix: string, localName: string, parentElement: HtmlElementAst): string {
if (isBlank(prefix)) {
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
if (isBlank(prefix) && isPresent(parentElement)) {
prefix = getNsPrefix(parentElement.name);
}
}
return mergeNsAndName(prefix, localName);
}
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}
@@ -0,0 +1,408 @@
/**
* @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 {normalizeBool, RegExpWrapper,} from '../facade/lang';
// 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 `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not exist.
export const NAMED_ENTITIES = /*@ts2dart_const*/ {
'Aacute': '\u00C1',
'aacute': '\u00E1',
'Acirc': '\u00C2',
'acirc': '\u00E2',
'acute': '\u00B4',
'AElig': '\u00C6',
'aelig': '\u00E6',
'Agrave': '\u00C0',
'agrave': '\u00E0',
'alefsym': '\u2135',
'Alpha': '\u0391',
'alpha': '\u03B1',
'amp': '&',
'and': '\u2227',
'ang': '\u2220',
'apos': '\u0027',
'Aring': '\u00C5',
'aring': '\u00E5',
'asymp': '\u2248',
'Atilde': '\u00C3',
'atilde': '\u00E3',
'Auml': '\u00C4',
'auml': '\u00E4',
'bdquo': '\u201E',
'Beta': '\u0392',
'beta': '\u03B2',
'brvbar': '\u00A6',
'bull': '\u2022',
'cap': '\u2229',
'Ccedil': '\u00C7',
'ccedil': '\u00E7',
'cedil': '\u00B8',
'cent': '\u00A2',
'Chi': '\u03A7',
'chi': '\u03C7',
'circ': '\u02C6',
'clubs': '\u2663',
'cong': '\u2245',
'copy': '\u00A9',
'crarr': '\u21B5',
'cup': '\u222A',
'curren': '\u00A4',
'dagger': '\u2020',
'Dagger': '\u2021',
'darr': '\u2193',
'dArr': '\u21D3',
'deg': '\u00B0',
'Delta': '\u0394',
'delta': '\u03B4',
'diams': '\u2666',
'divide': '\u00F7',
'Eacute': '\u00C9',
'eacute': '\u00E9',
'Ecirc': '\u00CA',
'ecirc': '\u00EA',
'Egrave': '\u00C8',
'egrave': '\u00E8',
'empty': '\u2205',
'emsp': '\u2003',
'ensp': '\u2002',
'Epsilon': '\u0395',
'epsilon': '\u03B5',
'equiv': '\u2261',
'Eta': '\u0397',
'eta': '\u03B7',
'ETH': '\u00D0',
'eth': '\u00F0',
'Euml': '\u00CB',
'euml': '\u00EB',
'euro': '\u20AC',
'exist': '\u2203',
'fnof': '\u0192',
'forall': '\u2200',
'frac12': '\u00BD',
'frac14': '\u00BC',
'frac34': '\u00BE',
'frasl': '\u2044',
'Gamma': '\u0393',
'gamma': '\u03B3',
'ge': '\u2265',
'gt': '>',
'harr': '\u2194',
'hArr': '\u21D4',
'hearts': '\u2665',
'hellip': '\u2026',
'Iacute': '\u00CD',
'iacute': '\u00ED',
'Icirc': '\u00CE',
'icirc': '\u00EE',
'iexcl': '\u00A1',
'Igrave': '\u00CC',
'igrave': '\u00EC',
'image': '\u2111',
'infin': '\u221E',
'int': '\u222B',
'Iota': '\u0399',
'iota': '\u03B9',
'iquest': '\u00BF',
'isin': '\u2208',
'Iuml': '\u00CF',
'iuml': '\u00EF',
'Kappa': '\u039A',
'kappa': '\u03BA',
'Lambda': '\u039B',
'lambda': '\u03BB',
'lang': '\u27E8',
'laquo': '\u00AB',
'larr': '\u2190',
'lArr': '\u21D0',
'lceil': '\u2308',
'ldquo': '\u201C',
'le': '\u2264',
'lfloor': '\u230A',
'lowast': '\u2217',
'loz': '\u25CA',
'lrm': '\u200E',
'lsaquo': '\u2039',
'lsquo': '\u2018',
'lt': '<',
'macr': '\u00AF',
'mdash': '\u2014',
'micro': '\u00B5',
'middot': '\u00B7',
'minus': '\u2212',
'Mu': '\u039C',
'mu': '\u03BC',
'nabla': '\u2207',
'nbsp': '\u00A0',
'ndash': '\u2013',
'ne': '\u2260',
'ni': '\u220B',
'not': '\u00AC',
'notin': '\u2209',
'nsub': '\u2284',
'Ntilde': '\u00D1',
'ntilde': '\u00F1',
'Nu': '\u039D',
'nu': '\u03BD',
'Oacute': '\u00D3',
'oacute': '\u00F3',
'Ocirc': '\u00D4',
'ocirc': '\u00F4',
'OElig': '\u0152',
'oelig': '\u0153',
'Ograve': '\u00D2',
'ograve': '\u00F2',
'oline': '\u203E',
'Omega': '\u03A9',
'omega': '\u03C9',
'Omicron': '\u039F',
'omicron': '\u03BF',
'oplus': '\u2295',
'or': '\u2228',
'ordf': '\u00AA',
'ordm': '\u00BA',
'Oslash': '\u00D8',
'oslash': '\u00F8',
'Otilde': '\u00D5',
'otilde': '\u00F5',
'otimes': '\u2297',
'Ouml': '\u00D6',
'ouml': '\u00F6',
'para': '\u00B6',
'permil': '\u2030',
'perp': '\u22A5',
'Phi': '\u03A6',
'phi': '\u03C6',
'Pi': '\u03A0',
'pi': '\u03C0',
'piv': '\u03D6',
'plusmn': '\u00B1',
'pound': '\u00A3',
'prime': '\u2032',
'Prime': '\u2033',
'prod': '\u220F',
'prop': '\u221D',
'Psi': '\u03A8',
'psi': '\u03C8',
'quot': '\u0022',
'radic': '\u221A',
'rang': '\u27E9',
'raquo': '\u00BB',
'rarr': '\u2192',
'rArr': '\u21D2',
'rceil': '\u2309',
'rdquo': '\u201D',
'real': '\u211C',
'reg': '\u00AE',
'rfloor': '\u230B',
'Rho': '\u03A1',
'rho': '\u03C1',
'rlm': '\u200F',
'rsaquo': '\u203A',
'rsquo': '\u2019',
'sbquo': '\u201A',
'Scaron': '\u0160',
'scaron': '\u0161',
'sdot': '\u22C5',
'sect': '\u00A7',
'shy': '\u00AD',
'Sigma': '\u03A3',
'sigma': '\u03C3',
'sigmaf': '\u03C2',
'sim': '\u223C',
'spades': '\u2660',
'sub': '\u2282',
'sube': '\u2286',
'sum': '\u2211',
'sup': '\u2283',
'sup1': '\u00B9',
'sup2': '\u00B2',
'sup3': '\u00B3',
'supe': '\u2287',
'szlig': '\u00DF',
'Tau': '\u03A4',
'tau': '\u03C4',
'there4': '\u2234',
'Theta': '\u0398',
'theta': '\u03B8',
'thetasym': '\u03D1',
'thinsp': '\u2009',
'THORN': '\u00DE',
'thorn': '\u00FE',
'tilde': '\u02DC',
'times': '\u00D7',
'trade': '\u2122',
'Uacute': '\u00DA',
'uacute': '\u00FA',
'uarr': '\u2191',
'uArr': '\u21D1',
'Ucirc': '\u00DB',
'ucirc': '\u00FB',
'Ugrave': '\u00D9',
'ugrave': '\u00F9',
'uml': '\u00A8',
'upsih': '\u03D2',
'Upsilon': '\u03A5',
'upsilon': '\u03C5',
'Uuml': '\u00DC',
'uuml': '\u00FC',
'weierp': '\u2118',
'Xi': '\u039E',
'xi': '\u03BE',
'Yacute': '\u00DD',
'yacute': '\u00FD',
'yen': '\u00A5',
'yuml': '\u00FF',
'Yuml': '\u0178',
'Zeta': '\u0396',
'zeta': '\u03B6',
'zwj': '\u200D',
'zwnj': '\u200C',
};
export enum HtmlTagContentType {
RAW_TEXT,
ESCAPABLE_RAW_TEXT,
PARSABLE_DATA
}
export class HtmlTagDefinition {
private closedByChildren: {[key: string]: boolean} = {};
public closedByParent: boolean = false;
public requiredParents: {[key: string]: boolean};
public parentToAdd: string;
public implicitNamespacePrefix: string;
public contentType: HtmlTagContentType;
public isVoid: boolean;
public ignoreFirstLf: boolean;
constructor(
{closedByChildren, requiredParents, implicitNamespacePrefix, contentType, closedByParent,
isVoid, ignoreFirstLf}: {
closedByChildren?: string[],
closedByParent?: boolean,
requiredParents?: string[],
implicitNamespacePrefix?: string,
contentType?: HtmlTagContentType,
isVoid?: boolean,
ignoreFirstLf?: boolean
} = {}) {
if (closedByChildren && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
}
this.isVoid = normalizeBool(isVoid);
this.closedByParent = normalizeBool(closedByParent) || this.isVoid;
if (requiredParents && requiredParents.length > 0) {
this.requiredParents = {};
// The first parent is the list is automatically when none of the listed parents are present
this.parentToAdd = requiredParents[0];
requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
}
this.implicitNamespacePrefix = implicitNamespacePrefix;
this.contentType = contentType || HtmlTagContentType.PARSABLE_DATA;
this.ignoreFirstLf = normalizeBool(ignoreFirstLf);
}
requireExtraParent(currentParent: string): boolean {
if (!this.requiredParents) {
return false;
}
if (!currentParent) {
return true;
}
const lcParent = currentParent.toLowerCase();
return this.requiredParents[lcParent] != true && lcParent != 'template';
}
isClosedByChild(name: string): boolean {
return this.isVoid || normalizeBool(this.closedByChildren[name.toLowerCase()]);
}
}
// 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} = {
'base': new HtmlTagDefinition({isVoid: true}),
'meta': new HtmlTagDefinition({isVoid: true}),
'area': new HtmlTagDefinition({isVoid: true}),
'embed': new HtmlTagDefinition({isVoid: true}),
'link': new HtmlTagDefinition({isVoid: true}),
'img': new HtmlTagDefinition({isVoid: true}),
'input': new HtmlTagDefinition({isVoid: true}),
'param': new HtmlTagDefinition({isVoid: true}),
'hr': new HtmlTagDefinition({isVoid: true}),
'br': new HtmlTagDefinition({isVoid: true}),
'source': new HtmlTagDefinition({isVoid: true}),
'track': new HtmlTagDefinition({isVoid: true}),
'wbr': new HtmlTagDefinition({isVoid: true}),
'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'
],
closedByParent: true
}),
'thead': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot']}),
'tbody': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot'], closedByParent: true}),
'tfoot': new HtmlTagDefinition({closedByChildren: ['tbody'], closedByParent: true}),
'tr': new HtmlTagDefinition({
closedByChildren: ['tr'],
requiredParents: ['tbody', 'tfoot', 'thead'],
closedByParent: true
}),
'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rt': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rtc': new HtmlTagDefinition({closedByChildren: ['rb', 'rtc', 'rp'], closedByParent: true}),
'rp': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'optgroup': new HtmlTagDefinition({closedByChildren: ['optgroup'], closedByParent: true}),
'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
'pre': new HtmlTagDefinition({ignoreFirstLf: true}),
'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
'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, ignoreFirstLf: true}),
};
const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION;
}
const _NS_PREFIX_RE = /^:([^:]+):(.+)/g;
export function splitNsName(elementName: string): [string, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const match = RegExpWrapper.firstMatch(_NS_PREFIX_RE, elementName);
return [match[1], match[2]];
}
export function getNsPrefix(elementName: string): string {
return splitNsName(elementName)[0];
}
export function mergeNsAndName(prefix: string, localName: string): string {
return prefix ? `:${prefix}:${localName}` : localName;
}
@@ -0,0 +1,25 @@
/**
* @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 {assertInterpolationSymbols} from '../assertions';
export class InterpolationConfig {
static fromArray(markers: [string, string]): InterpolationConfig {
if (!markers) {
return DEFAULT_INTERPOLATION_CONFIG;
}
assertInterpolationSymbols('interpolation', markers);
return new InterpolationConfig(markers[0], markers[1]);
}
constructor(public start: string, public end: string){};
}
export const DEFAULT_INTERPOLATION_CONFIG: InterpolationConfig =
new InterpolationConfig('{{', '}}');