2016-06-23 09:47:54 -07:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2016-04-28 17:50:03 -07:00
|
|
|
import {Injectable} from '@angular/core';
|
2016-06-08 16:38:52 -07:00
|
|
|
import {isPresent, isBlank,} from '../src/facade/lang';
|
2016-04-28 17:50:03 -07:00
|
|
|
import {ListWrapper} from '../src/facade/collection';
|
2016-06-08 16:38:52 -07:00
|
|
|
import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst} from './html_ast';
|
2015-10-07 09:34:21 -07:00
|
|
|
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
|
2016-04-28 17:50:03 -07:00
|
|
|
import {ParseError, ParseSourceSpan} from './parse_util';
|
|
|
|
import {getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags';
|
2016-06-22 17:25:42 -07:00
|
|
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
|
2015-11-16 14:36:39 -08:00
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
export class HtmlTreeError extends ParseError {
|
2016-02-16 16:46:51 -08:00
|
|
|
static create(elementName: string, span: ParseSourceSpan, msg: string): HtmlTreeError {
|
|
|
|
return new HtmlTreeError(elementName, span, msg);
|
2015-08-25 15:36:02 -07:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
|
2016-02-16 16:46:51 -08:00
|
|
|
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
|
2015-08-25 15:36:02 -07:00
|
|
|
}
|
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
export class HtmlParseTreeResult {
|
|
|
|
constructor(public rootNodes: HtmlAst[], public errors: ParseError[]) {}
|
2015-08-25 15:36:02 -07:00
|
|
|
}
|
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
@Injectable()
|
|
|
|
export class HtmlParser {
|
2016-06-22 17:25:42 -07:00
|
|
|
parse(
|
|
|
|
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
|
|
|
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
|
2016-06-08 16:38:52 -07:00
|
|
|
HtmlParseTreeResult {
|
2016-06-30 14:59:23 -07:00
|
|
|
const tokensAndErrors =
|
2016-06-24 14:31:35 -07:00
|
|
|
tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms, interpolationConfig);
|
2016-06-30 14:59:23 -07:00
|
|
|
const treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
|
2016-06-08 16:38:52 -07:00
|
|
|
return new HtmlParseTreeResult(
|
|
|
|
treeAndErrors.rootNodes,
|
|
|
|
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2015-08-25 15:36:02 -07:00
|
|
|
}
|
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
class TreeBuilder {
|
|
|
|
private index: number = -1;
|
|
|
|
private peek: HtmlToken;
|
|
|
|
|
|
|
|
private rootNodes: HtmlAst[] = [];
|
|
|
|
private errors: HtmlTreeError[] = [];
|
|
|
|
|
|
|
|
private elementStack: HtmlElementAst[] = [];
|
2015-10-07 09:34:21 -07:00
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
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) {
|
2015-12-01 13:01:05 -08:00
|
|
|
this._closeVoidElement();
|
2015-10-07 09:34:21 -07:00
|
|
|
this._consumeCdata(this._advance());
|
|
|
|
} else if (this.peek.type === HtmlTokenType.COMMENT_START) {
|
2015-12-01 13:01:05 -08:00
|
|
|
this._closeVoidElement();
|
2015-10-07 09:34:21 -07:00
|
|
|
this._consumeComment(this._advance());
|
2016-06-08 16:38:52 -07:00
|
|
|
} else if (
|
|
|
|
this.peek.type === HtmlTokenType.TEXT || this.peek.type === HtmlTokenType.RAW_TEXT ||
|
|
|
|
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
|
2015-12-01 13:01:05 -08:00
|
|
|
this._closeVoidElement();
|
2015-10-07 09:34:21 -07:00
|
|
|
this._consumeText(this._advance());
|
2016-04-12 11:46:39 -07:00
|
|
|
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
|
|
|
|
this._consumeExpansion(this._advance());
|
2015-10-07 09:34:21 -07:00
|
|
|
} else {
|
|
|
|
// Skip all other tokens...
|
|
|
|
this._advance();
|
|
|
|
}
|
2015-08-25 15:36:02 -07:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
return new HtmlParseTreeResult(this.rootNodes, this.errors);
|
|
|
|
}
|
|
|
|
|
|
|
|
private _advance(): HtmlToken {
|
2016-06-30 14:59:23 -07:00
|
|
|
const prev = this.peek;
|
2015-10-07 09:34:21 -07:00
|
|
|
if (this.index < this.tokens.length - 1) {
|
|
|
|
// Note: there is always an EOF token at the end
|
|
|
|
this.index++;
|
2015-09-11 13:35:46 -07:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
this.peek = this.tokens[this.index];
|
|
|
|
return prev;
|
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
private _advanceIf(type: HtmlTokenType): HtmlToken {
|
|
|
|
if (this.peek.type === type) {
|
|
|
|
return this._advance();
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2015-11-16 14:37:00 -08:00
|
|
|
return null;
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
|
|
|
|
private _consumeCdata(startToken: HtmlToken) {
|
|
|
|
this._consumeText(this._advance());
|
|
|
|
this._advanceIf(HtmlTokenType.CDATA_END);
|
2015-11-16 14:36:39 -08:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
|
2016-03-06 20:21:20 -08:00
|
|
|
private _consumeComment(token: HtmlToken) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const text = this._advanceIf(HtmlTokenType.RAW_TEXT);
|
2015-10-07 09:34:21 -07:00
|
|
|
this._advanceIf(HtmlTokenType.COMMENT_END);
|
2016-06-30 14:59:23 -07:00
|
|
|
const value = isPresent(text) ? text.parts[0].trim() : null;
|
2016-03-14 10:51:23 -07:00
|
|
|
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
|
2016-04-12 11:46:39 -07:00
|
|
|
private _consumeExpansion(token: HtmlToken) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const switchValue = this._advance();
|
2016-04-12 11:46:39 -07:00
|
|
|
|
2016-06-30 14:59:23 -07:00
|
|
|
const type = this._advance();
|
|
|
|
const cases: HtmlExpansionCaseAst[] = [];
|
2016-04-12 11:46:39 -07:00
|
|
|
|
|
|
|
// read =
|
|
|
|
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
|
2016-04-13 16:01:25 -07:00
|
|
|
let expCase = this._parseExpansionCase();
|
|
|
|
if (isBlank(expCase)) return; // error
|
|
|
|
cases.push(expCase);
|
2016-04-12 11:46:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// read the final }
|
|
|
|
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
|
|
|
|
this.errors.push(
|
2016-07-08 16:46:49 -07:00
|
|
|
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
2016-04-12 11:46:39 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._advance();
|
|
|
|
|
2016-06-30 14:59:23 -07:00
|
|
|
const mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
|
2016-06-08 16:38:52 -07:00
|
|
|
this._addToParent(new HtmlExpansionAst(
|
|
|
|
switchValue.parts[0], type.parts[0], cases, mainSourceSpan, switchValue.sourceSpan));
|
2016-04-12 11:46:39 -07:00
|
|
|
}
|
|
|
|
|
2016-04-13 16:01:25 -07:00
|
|
|
private _parseExpansionCase(): HtmlExpansionCaseAst {
|
2016-06-30 14:59:23 -07:00
|
|
|
const value = this._advance();
|
2016-04-13 16:01:25 -07:00
|
|
|
|
|
|
|
// read {
|
|
|
|
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
|
2016-07-15 09:42:33 -07:00
|
|
|
this.errors.push(
|
|
|
|
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
|
2016-04-13 16:01:25 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// read until }
|
2016-06-30 14:59:23 -07:00
|
|
|
const start = this._advance();
|
2016-04-13 16:01:25 -07:00
|
|
|
|
2016-06-30 14:59:23 -07:00
|
|
|
const exp = this._collectExpansionExpTokens(start);
|
2016-04-13 16:01:25 -07:00
|
|
|
if (isBlank(exp)) return null;
|
|
|
|
|
2016-06-30 14:59:23 -07:00
|
|
|
const end = this._advance();
|
2016-04-13 16:01:25 -07:00
|
|
|
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
|
|
|
|
|
|
|
|
// parse everything in between { and }
|
2016-06-30 14:59:23 -07:00
|
|
|
const parsedExp = new TreeBuilder(exp).build();
|
2016-04-13 16:01:25 -07:00
|
|
|
if (parsedExp.errors.length > 0) {
|
|
|
|
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2016-06-30 14:59:23 -07:00
|
|
|
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
|
|
|
|
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
|
2016-06-08 16:38:52 -07:00
|
|
|
return new HtmlExpansionCaseAst(
|
|
|
|
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
|
2016-04-13 16:01:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] {
|
2016-06-30 14:59:23 -07:00
|
|
|
const exp: HtmlToken[] = [];
|
|
|
|
const expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START];
|
2016-04-13 16:01:25 -07:00
|
|
|
|
|
|
|
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(
|
2016-07-08 16:46:49 -07:00
|
|
|
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
2016-04-13 16:01:25 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) {
|
|
|
|
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) {
|
|
|
|
expansionFormStack.pop();
|
|
|
|
} else {
|
|
|
|
this.errors.push(
|
2016-07-08 16:46:49 -07:00
|
|
|
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
2016-04-13 16:01:25 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.peek.type === HtmlTokenType.EOF) {
|
|
|
|
this.errors.push(
|
2016-07-08 16:46:49 -07:00
|
|
|
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
2016-04-13 16:01:25 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
exp.push(this._advance());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
private _consumeText(token: HtmlToken) {
|
2015-12-05 00:15:18 -08:00
|
|
|
let text = token.parts[0];
|
|
|
|
if (text.length > 0 && text[0] == '\n') {
|
2016-06-30 14:59:23 -07:00
|
|
|
const parent = this._getParentElement();
|
2015-12-05 00:15:18 -08:00
|
|
|
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));
|
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
|
2015-12-01 13:01:05 -08:00
|
|
|
private _closeVoidElement(): void {
|
|
|
|
if (this.elementStack.length > 0) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const el = ListWrapper.last(this.elementStack);
|
2015-12-01 13:01:05 -08:00
|
|
|
|
|
|
|
if (getHtmlTagDefinition(el.name).isVoid) {
|
|
|
|
this.elementStack.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
private _consumeStartTag(startTagToken: HtmlToken) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const prefix = startTagToken.parts[0];
|
|
|
|
const name = startTagToken.parts[1];
|
|
|
|
const attrs: HtmlAttrAst[] = [];
|
2015-10-07 09:34:21 -07:00
|
|
|
while (this.peek.type === HtmlTokenType.ATTR_NAME) {
|
|
|
|
attrs.push(this._consumeAttr(this._advance()));
|
|
|
|
}
|
2016-06-30 14:59:23 -07:00
|
|
|
const fullName = getElementFullName(prefix, name, this._getParentElement());
|
|
|
|
let selfClosing = false;
|
2015-10-07 09:34:21 -07:00
|
|
|
// 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();
|
2015-12-02 10:11:01 -08:00
|
|
|
selfClosing = true;
|
2015-12-09 09:32:15 -08:00
|
|
|
if (getNsPrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
|
2015-12-03 16:10:20 -08:00
|
|
|
this.errors.push(HtmlTreeError.create(
|
2016-02-16 16:46:51 -08:00
|
|
|
fullName, startTagToken.sourceSpan,
|
2015-12-03 16:10:20 -08:00
|
|
|
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
|
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
} else if (this.peek.type === HtmlTokenType.TAG_OPEN_END) {
|
|
|
|
this._advance();
|
2015-12-02 10:11:01 -08:00
|
|
|
selfClosing = false;
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2016-06-30 14:59:23 -07:00
|
|
|
const end = this.peek.sourceSpan.start;
|
|
|
|
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
|
|
|
|
const el = new HtmlElementAst(fullName, attrs, [], span, span, null);
|
2015-10-07 09:34:21 -07:00
|
|
|
this._pushElement(el);
|
2015-12-02 10:11:01 -08:00
|
|
|
if (selfClosing) {
|
2015-10-07 09:34:21 -07:00
|
|
|
this._popElement(fullName);
|
2016-03-23 13:43:28 -07:00
|
|
|
el.endSourceSpan = span;
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private _pushElement(el: HtmlElementAst) {
|
2015-11-10 15:56:25 -08:00
|
|
|
if (this.elementStack.length > 0) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const parentEl = ListWrapper.last(this.elementStack);
|
2015-11-10 15:56:25 -08:00
|
|
|
if (getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) {
|
|
|
|
this.elementStack.pop();
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-15 09:37:33 -07:00
|
|
|
const tagDef = getHtmlTagDefinition(el.name);
|
|
|
|
const {parent, container} = this._getParentElementSkippingContainers();
|
|
|
|
|
2016-06-15 09:45:19 -07:00
|
|
|
if (isPresent(parent) && tagDef.requireExtraParent(parent.name)) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const newParent = new HtmlElementAst(
|
2016-06-15 09:37:33 -07:00
|
|
|
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
|
|
|
|
this._insertBeforeContainer(parent, container, newParent);
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2016-06-15 09:37:33 -07:00
|
|
|
|
|
|
|
this._addToParent(el);
|
|
|
|
this.elementStack.push(el);
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private _consumeEndTag(endTagToken: HtmlToken) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const fullName =
|
2015-11-10 15:56:25 -08:00
|
|
|
getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
|
2015-12-02 10:11:01 -08:00
|
|
|
|
2016-06-03 19:49:17 +02:00
|
|
|
if (this._getParentElement()) {
|
|
|
|
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
|
|
|
|
}
|
2016-03-23 13:43:28 -07:00
|
|
|
|
2015-12-03 15:53:44 -08:00
|
|
|
if (getHtmlTagDefinition(fullName).isVoid) {
|
2016-06-08 16:38:52 -07:00
|
|
|
this.errors.push(HtmlTreeError.create(
|
|
|
|
fullName, endTagToken.sourceSpan,
|
|
|
|
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
|
2015-12-03 15:53:44 -08:00
|
|
|
} else if (!this._popElement(fullName)) {
|
2016-06-08 16:38:52 -07:00
|
|
|
this.errors.push(HtmlTreeError.create(
|
|
|
|
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private _popElement(fullName: string): boolean {
|
2015-11-10 15:56:25 -08:00
|
|
|
for (let stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const el = this.elementStack[stackIndex];
|
2015-11-23 16:02:19 -08:00
|
|
|
if (el.name == fullName) {
|
2015-11-10 15:56:25 -08:00
|
|
|
ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex);
|
|
|
|
return true;
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2015-12-02 10:11:01 -08:00
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
if (!getHtmlTagDefinition(el.name).closedByParent) {
|
2015-11-10 15:56:25 -08:00
|
|
|
return false;
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
}
|
2015-11-10 15:56:25 -08:00
|
|
|
return false;
|
2015-11-16 14:36:39 -08:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
|
|
|
|
private _consumeAttr(attrName: HtmlToken): HtmlAttrAst {
|
2016-06-30 14:59:23 -07:00
|
|
|
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
|
|
|
|
let end = attrName.sourceSpan.end;
|
|
|
|
let value = '';
|
2015-10-07 09:34:21 -07:00
|
|
|
if (this.peek.type === HtmlTokenType.ATTR_VALUE) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const valueToken = this._advance();
|
2015-10-07 09:34:21 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-06-15 09:37:33 -07:00
|
|
|
/**
|
|
|
|
* 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') {
|
2016-06-16 13:54:00 -07:00
|
|
|
return {parent: this.elementStack[i], container};
|
2016-06-15 09:37:33 -07:00
|
|
|
}
|
|
|
|
container = this.elementStack[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
return {parent: ListWrapper.last(this.elementStack), container};
|
|
|
|
}
|
|
|
|
|
2015-10-07 09:34:21 -07:00
|
|
|
private _addToParent(node: HtmlAst) {
|
2016-06-30 14:59:23 -07:00
|
|
|
const parent = this._getParentElement();
|
2015-10-07 09:34:21 -07:00
|
|
|
if (isPresent(parent)) {
|
|
|
|
parent.children.push(node);
|
|
|
|
} else {
|
|
|
|
this.rootNodes.push(node);
|
|
|
|
}
|
|
|
|
}
|
2016-06-15 09:37:33 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2016-06-30 14:59:23 -07:00
|
|
|
const index = parent.children.indexOf(container);
|
2016-06-15 09:37:33 -07:00
|
|
|
parent.children[index] = node;
|
|
|
|
} else {
|
|
|
|
this.rootNodes.push(node);
|
|
|
|
}
|
|
|
|
node.children.push(container);
|
|
|
|
this.elementStack.splice(this.elementStack.indexOf(container), 0, node);
|
|
|
|
}
|
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
|
|
|
|
2016-06-08 16:38:52 -07:00
|
|
|
function getElementFullName(
|
|
|
|
prefix: string, localName: string, parentElement: HtmlElementAst): string {
|
2015-10-07 09:34:21 -07:00
|
|
|
if (isBlank(prefix)) {
|
|
|
|
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
|
2015-11-10 15:56:25 -08:00
|
|
|
if (isBlank(prefix) && isPresent(parentElement)) {
|
2015-12-09 09:32:15 -08:00
|
|
|
prefix = getNsPrefix(parentElement.name);
|
2015-11-10 15:56:25 -08:00
|
|
|
}
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2015-11-10 15:56:25 -08:00
|
|
|
|
|
|
|
return mergeNsAndName(prefix, localName);
|
2015-10-07 09:34:21 -07:00
|
|
|
}
|
2016-04-13 16:01:25 -07:00
|
|
|
|
|
|
|
function lastOnStack(stack: any[], element: any): boolean {
|
|
|
|
return stack.length > 0 && stack[stack.length - 1] === element;
|
2016-04-25 21:47:33 -07:00
|
|
|
}
|