feat(i18n): add support for nested expansion forms

Closes #7977
This commit is contained in:
vsavkin
2016-04-13 16:01:25 -07:00
committed by Victor Savkin
parent 22ae2d0976
commit c6244d1470
9 changed files with 246 additions and 94 deletions

View File

@ -125,8 +125,7 @@ class _HtmlTokenizer {
private currentTokenStart: ParseLocation;
private currentTokenType: HtmlTokenType;
private inExpansionCase: boolean = false;
private inExpansionForm: boolean = false;
private expansionCaseStack = [];
tokens: HtmlToken[] = [];
errors: HtmlTokenError[] = [];
@ -169,10 +168,12 @@ class _HtmlTokenizer {
} else if (this.peek === $EQ && this.tokenizeExpansionForms) {
this._consumeExpansionCaseStart();
} else if (this.peek === $RBRACE && this.inExpansionCase && this.tokenizeExpansionForms) {
} else if (this.peek === $RBRACE && this.isInExpansionCase() &&
this.tokenizeExpansionForms) {
this._consumeExpansionCaseEnd();
} else if (this.peek === $RBRACE && !this.inExpansionCase && this.tokenizeExpansionForms) {
} else if (this.peek === $RBRACE && this.isInExpansionForm() &&
this.tokenizeExpansionForms) {
this._consumeExpansionFormEnd();
} else {
@ -551,7 +552,7 @@ class _HtmlTokenizer {
this._requireCharCode($COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this.inExpansionForm = true;
this.expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
}
private _consumeExpansionCaseStart() {
@ -567,7 +568,7 @@ class _HtmlTokenizer {
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this.inExpansionCase = true;
this.expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
}
private _consumeExpansionCaseEnd() {
@ -576,7 +577,7 @@ class _HtmlTokenizer {
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this.inExpansionCase = false;
this.expansionCaseStack.pop();
}
private _consumeExpansionFormEnd() {
@ -584,7 +585,7 @@ class _HtmlTokenizer {
this._requireCharCode($RBRACE);
this._endToken([]);
this.inExpansionForm = false;
this.expansionCaseStack.pop();
}
private _consumeText() {
@ -622,7 +623,9 @@ class _HtmlTokenizer {
if (this.peek === $LT || this.peek === $EOF) return true;
if (this.tokenizeExpansionForms) {
if (isSpecialFormStart(this.peek, this.nextPeek)) return true;
if (this.peek === $RBRACE && !interpolation && this.inExpansionForm) return true;
if (this.peek === $RBRACE && !interpolation &&
(this.isInExpansionCase() || this.isInExpansionForm()))
return true;
}
return false;
}
@ -648,6 +651,18 @@ class _HtmlTokenizer {
this.tokens = ListWrapper.slice(this.tokens, 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 {

View File

@ -124,40 +124,9 @@ class TreeBuilder {
// read =
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
let value = this._advance();
// read {
let exp = [];
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan,
`Invalid expansion form. Missing '{'.,`));
return;
}
// read until }
let start = this._advance();
while (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_END) {
exp.push(this._advance());
if (this.peek.type === HtmlTokenType.EOF) {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
return;
}
}
let end = this._advance();
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
let parsedExp = new TreeBuilder(exp).build();
if (parsedExp.errors.length > 0) {
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
return;
}
let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
cases.push(new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan,
value.sourceSpan, expSourceSpan));
let expCase = this._parseExpansionCase();
if (isBlank(expCase)) return; // error
cases.push(expCase);
}
// read the final }
@ -173,6 +142,80 @@ class TreeBuilder {
mainSourceSpan, switchValue.sourceSpan));
}
private _parseExpansionCase(): HtmlExpansionCaseAst {
let value = this._advance();
// read {
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan,
`Invalid expansion form. Missing '{'.,`));
return null;
}
// read until }
let start = this._advance();
let exp = this._collectExpansionExpTokens(start);
if (isBlank(exp)) return null;
let end = this._advance();
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
let parsedExp = new TreeBuilder(exp).build();
if (parsedExp.errors.length > 0) {
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
return null;
}
let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
let 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[] {
let exp = [];
let 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 expansion form. 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 expansion form. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EOF) {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
private _consumeText(token: HtmlToken) {
let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') {
@ -321,3 +364,7 @@ function getElementFullName(prefix: string, localName: string,
return mergeNsAndName(prefix, localName);
}
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}

View File

@ -254,6 +254,10 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, parent: ElementContext): any {
var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
var expr = this._parseInterpolation(ast.value, ast.sourceSpan);
@ -270,7 +274,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitElement(element: HtmlElementAst, component: ElementContext): any {
visitElement(element: HtmlElementAst, parent: ElementContext): any {
var nodeName = element.name;
var preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
@ -773,7 +777,6 @@ class NonBindableVisitor implements HtmlAstVisitor {
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
}