/** * @license * Copyright Google LLC 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 * as html from './ast'; import * as lex from './lexer'; import {getNsPrefix, isNgContainer, mergeNsAndName, TagDefinition} from './tags'; export class TreeError extends ParseError { static create(elementName: string|null, span: ParseSourceSpan, msg: string): TreeError { return new TreeError(elementName, span, msg); } constructor(public elementName: string|null, span: ParseSourceSpan, msg: string) { super(span, msg); } } export class ParseTreeResult { constructor(public rootNodes: html.Node[], public errors: ParseError[]) {} } export class Parser { constructor(public getTagDefinition: (tagName: string) => TagDefinition) {} parse(source: string, url: string, options?: lex.TokenizeOptions): ParseTreeResult { const tokenizeResult = lex.tokenize(source, url, this.getTagDefinition, options); const parser = new _TreeBuilder(tokenizeResult.tokens, this.getTagDefinition); parser.build(); return new ParseTreeResult( parser.rootNodes, (tokenizeResult.errors as ParseError[]).concat(parser.errors), ); } } class _TreeBuilder { private _index: number = -1; // `_peek` will be initialized by the call to `advance()` in the constructor. private _peek!: lex.Token; private _elementStack: html.Element[] = []; rootNodes: html.Node[] = []; errors: TreeError[] = []; constructor( private tokens: lex.Token[], private getTagDefinition: (tagName: string) => TagDefinition) { this._advance(); } build(): void { while (this._peek.type !== lex.TokenType.EOF) { if (this._peek.type === lex.TokenType.TAG_OPEN_START) { this._consumeStartTag(this._advance()); } else if (this._peek.type === lex.TokenType.TAG_CLOSE) { this._consumeEndTag(this._advance()); } else if (this._peek.type === lex.TokenType.CDATA_START) { this._closeVoidElement(); this._consumeCdata(this._advance()); } else if (this._peek.type === lex.TokenType.COMMENT_START) { this._closeVoidElement(); this._consumeComment(this._advance()); } else if ( this._peek.type === lex.TokenType.TEXT || this._peek.type === lex.TokenType.RAW_TEXT || this._peek.type === lex.TokenType.ESCAPABLE_RAW_TEXT) { this._closeVoidElement(); this._consumeText(this._advance()); } else if (this._peek.type === lex.TokenType.EXPANSION_FORM_START) { this._consumeExpansion(this._advance()); } else { // Skip all other tokens... this._advance(); } } } private _advance(): lex.Token { 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: lex.TokenType): lex.Token|null { if (this._peek.type === type) { return this._advance(); } return null; } private _consumeCdata(_startToken: lex.Token) { this._consumeText(this._advance()); this._advanceIf(lex.TokenType.CDATA_END); } private _consumeComment(token: lex.Token) { const text = this._advanceIf(lex.TokenType.RAW_TEXT); this._advanceIf(lex.TokenType.COMMENT_END); const value = text != null ? text.parts[0].trim() : null; this._addToParent(new html.Comment(value, token.sourceSpan)); } private _consumeExpansion(token: lex.Token) { const switchValue = this._advance(); const type = this._advance(); const cases: html.ExpansionCase[] = []; // read = while (this._peek.type === lex.TokenType.EXPANSION_CASE_VALUE) { const expCase = this._parseExpansionCase(); if (!expCase) return; // error cases.push(expCase); } // read the final } if (this._peek.type !== lex.TokenType.EXPANSION_FORM_END) { this.errors.push( TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '}'.`)); return; } const sourceSpan = new ParseSourceSpan(token.sourceSpan.start, this._peek.sourceSpan.end); this._addToParent(new html.Expansion( switchValue.parts[0], type.parts[0], cases, sourceSpan, switchValue.sourceSpan)); this._advance(); } private _parseExpansionCase(): html.ExpansionCase|null { const value = this._advance(); // read { if (this._peek.type !== lex.TokenType.EXPANSION_CASE_EXP_START) { this.errors.push( TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '{'.`)); return null; } // read until } const start = this._advance(); const exp = this._collectExpansionExpTokens(start); if (!exp) return null; const end = this._advance(); exp.push(new lex.Token(lex.TokenType.EOF, [], end.sourceSpan)); // parse everything in between { and } const expansionCaseParser = new _TreeBuilder(exp, this.getTagDefinition); expansionCaseParser.build(); if (expansionCaseParser.errors.length > 0) { this.errors = this.errors.concat(expansionCaseParser.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 html.ExpansionCase( value.parts[0], expansionCaseParser.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan); } private _collectExpansionExpTokens(start: lex.Token): lex.Token[]|null { const exp: lex.Token[] = []; const expansionFormStack = [lex.TokenType.EXPANSION_CASE_EXP_START]; while (true) { if (this._peek.type === lex.TokenType.EXPANSION_FORM_START || this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_START) { expansionFormStack.push(this._peek.type); } if (this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_END) { if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_CASE_EXP_START)) { expansionFormStack.pop(); if (expansionFormStack.length == 0) return exp; } else { this.errors.push( TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); return null; } } if (this._peek.type === lex.TokenType.EXPANSION_FORM_END) { if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_FORM_START)) { expansionFormStack.pop(); } else { this.errors.push( TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); return null; } } if (this._peek.type === lex.TokenType.EOF) { this.errors.push( TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); return null; } exp.push(this._advance()); } } private _consumeText(token: lex.Token) { let text = token.parts[0]; if (text.length > 0 && text[0] == '\n') { const parent = this._getParentElement(); if (parent != null && parent.children.length == 0 && this.getTagDefinition(parent.name).ignoreFirstLf) { text = text.substring(1); } } if (text.length > 0) { this._addToParent(new html.Text(text, token.sourceSpan)); } } private _closeVoidElement(): void { const el = this._getParentElement(); if (el && this.getTagDefinition(el.name).isVoid) { this._elementStack.pop(); } } private _consumeStartTag(startTagToken: lex.Token) { const prefix = startTagToken.parts[0]; const name = startTagToken.parts[1]; const attrs: html.Attribute[] = []; while (this._peek.type === lex.TokenType.ATTR_NAME) { attrs.push(this._consumeAttr(this._advance())); } const fullName = this._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 === lex.TokenType.TAG_OPEN_END_VOID) { this._advance(); selfClosing = true; const tagDef = this.getTagDefinition(fullName); if (!(tagDef.canSelfClose || getNsPrefix(fullName) !== null || tagDef.isVoid)) { this.errors.push(TreeError.create( fullName, startTagToken.sourceSpan, `Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`)); } } else if (this._peek.type === lex.TokenType.TAG_OPEN_END) { this._advance(); selfClosing = false; } const end = this._peek.sourceSpan.start; const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end); const el = new html.Element(fullName, attrs, [], span, span, undefined); this._pushElement(el); if (selfClosing) { this._popElement(fullName); el.endSourceSpan = span; } } private _pushElement(el: html.Element) { const parentEl = this._getParentElement(); if (parentEl && this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) { this._elementStack.pop(); } this._addToParent(el); this._elementStack.push(el); } private _consumeEndTag(endTagToken: lex.Token) { const fullName = this._getElementFullName( endTagToken.parts[0], endTagToken.parts[1], this._getParentElement()); if (this._getParentElement()) { this._getParentElement()!.endSourceSpan = endTagToken.sourceSpan; } if (this.getTagDefinition(fullName).isVoid) { this.errors.push(TreeError.create( fullName, endTagToken.sourceSpan, `Void elements do not have end tags "${endTagToken.parts[1]}"`)); } else if (!this._popElement(fullName)) { const errMsg = `Unexpected closing tag "${ fullName}". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags`; this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, errMsg)); } } private _popElement(fullName: string): boolean { for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) { const el = this._elementStack[stackIndex]; if (el.name == fullName) { this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex); return true; } if (!this.getTagDefinition(el.name).closedByParent) { return false; } } return false; } private _consumeAttr(attrName: lex.Token): html.Attribute { const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]); let end = attrName.sourceSpan.end; let value = ''; let valueSpan: ParseSourceSpan = undefined!; if (this._peek.type === lex.TokenType.ATTR_QUOTE) { this._advance(); } if (this._peek.type === lex.TokenType.ATTR_VALUE) { const valueToken = this._advance(); value = valueToken.parts[0]; end = valueToken.sourceSpan.end; valueSpan = valueToken.sourceSpan; } if (this._peek.type === lex.TokenType.ATTR_QUOTE) { const quoteToken = this._advance(); end = quoteToken.sourceSpan.end; } return new html.Attribute( fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end), valueSpan); } private _getParentElement(): html.Element|null { return this._elementStack.length > 0 ? this._elementStack[this._elementStack.length - 1] : null; } private _addToParent(node: html.Node) { const parent = this._getParentElement(); if (parent != null) { parent.children.push(node); } else { this.rootNodes.push(node); } } private _getElementFullName(prefix: string, localName: string, parentElement: html.Element|null): string { if (prefix === '') { prefix = this.getTagDefinition(localName).implicitNamespacePrefix || ''; if (prefix === '' && parentElement != null) { prefix = getNsPrefix(parentElement.name); } } return mergeNsAndName(prefix, localName); } } function lastOnStack(stack: any[], element: any): boolean { return stack.length > 0 && stack[stack.length - 1] === element; }