2016-07-21 13:56:58 -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-08-02 15:53:34 -07:00
import { ParseError , ParseSourceSpan } from '../parse_util' ;
2016-07-21 13:56:58 -07:00
import * as html from './ast' ;
import { DEFAULT_INTERPOLATION_CONFIG , InterpolationConfig } from './interpolation_config' ;
2016-08-02 15:53:34 -07:00
import * as lex from './lexer' ;
2017-04-13 10:37:45 -07:00
import { TagDefinition , getNsPrefix , isNgContainer , mergeNsAndName } from './tags' ;
2016-07-21 13:56:58 -07:00
export class TreeError extends ParseError {
2017-03-24 09:59:58 -07:00
static create ( elementName : string | null , span : ParseSourceSpan , msg : string ) : TreeError {
2016-07-21 13:56:58 -07:00
return new TreeError ( elementName , span , msg );
}
2017-03-24 09:59:58 -07:00
constructor ( public elementName : string | null , span : ParseSourceSpan , msg : string ) {
super ( span , msg );
}
2016-07-21 13:56:58 -07:00
}
export class ParseTreeResult {
constructor ( public rootNodes : html.Node [], public errors : ParseError []) {}
}
export class Parser {
2016-08-11 21:00:35 -07:00
constructor ( public getTagDefinition : ( tagName : string ) => TagDefinition ) {}
2016-07-21 13:56:58 -07:00
parse (
source : string , url : string , parseExpansionForms : boolean = false ,
interpolationConfig : InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG ) : ParseTreeResult {
const tokensAndErrors =
2016-08-11 21:00:35 -07:00
lex . tokenize ( source , url , this . getTagDefinition , parseExpansionForms , interpolationConfig );
2016-07-21 13:56:58 -07:00
2016-08-11 21:00:35 -07:00
const treeAndErrors = new _TreeBuilder ( tokensAndErrors . tokens , this . getTagDefinition ). build ();
2016-07-21 13:56:58 -07:00
return new ParseTreeResult (
treeAndErrors . rootNodes ,
(< ParseError [] > tokensAndErrors . errors ). concat ( treeAndErrors . errors ));
}
}
class _TreeBuilder {
private _index : number = - 1 ;
2018-06-18 16:38:33 -07:00
// TODO(issue/24571): remove '!'.
private _peek !: lex . Token ;
2016-07-21 13:56:58 -07:00
private _rootNodes : html.Node [] = [];
private _errors : TreeError [] = [];
private _elementStack : html.Element [] = [];
constructor (
private tokens : lex.Token [], private getTagDefinition : ( tagName : string ) => TagDefinition ) {
this . _advance ();
}
build () : ParseTreeResult {
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 ();
}
}
return new ParseTreeResult ( this . _rootNodes , this . _errors );
}
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 ;
}
2017-03-24 09:59:58 -07:00
private _advanceIf ( type : lex . TokenType ) : lex . Token | null {
2016-07-21 13:56:58 -07:00
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 );
2017-03-02 09:37:01 -08:00
const value = text != null ? text . parts [ 0 ]. trim () : null ;
2016-07-21 13:56:58 -07:00
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 ) {
2016-11-12 14:08:58 +01:00
const expCase = this . _parseExpansionCase ();
2016-09-30 09:26:53 -07:00
if ( ! expCase ) return ; // error
2016-07-21 13:56:58 -07:00
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 ();
}
2017-03-24 09:59:58 -07:00
private _parseExpansionCase () : html . ExpansionCase | null {
2016-07-21 13:56:58 -07:00
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 );
2016-09-30 09:26:53 -07:00
if ( ! exp ) return null ;
2016-07-21 13:56:58 -07:00
const end = this . _advance ();
exp . push ( new lex . Token ( lex . TokenType . EOF , [], end . sourceSpan ));
// parse everything in between { and }
const parsedExp = new _TreeBuilder ( exp , this . getTagDefinition ). build ();
if ( parsedExp . errors . length > 0 ) {
this . _errors = this . _errors . concat (< TreeError [] > 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 html . ExpansionCase (
value . parts [ 0 ], parsedExp . rootNodes , sourceSpan , value . sourceSpan , expSourceSpan );
}
2017-03-24 09:59:58 -07:00
private _collectExpansionExpTokens ( start : lex.Token ) : lex . Token [] | null {
2016-07-21 13:56:58 -07:00
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 ();
2017-03-02 09:37:01 -08:00
if ( parent != null && parent . children . length == 0 &&
2016-07-21 13:56:58 -07:00
this . getTagDefinition ( parent . name ). ignoreFirstLf ) {
text = text . substring ( 1 );
}
}
if ( text . length > 0 ) {
this . _addToParent ( new html . Text ( text , token . sourceSpan ));
}
}
private _closeVoidElement () : void {
2017-07-26 14:53:59 -07:00
const el = this . _getParentElement ();
if ( el && this . getTagDefinition ( el . name ). isVoid ) {
this . _elementStack . pop ();
2016-07-21 13:56:58 -07:00
}
}
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 );
2017-03-24 09:59:58 -07:00
const el = new html . Element ( fullName , attrs , [], span , span , undefined );
2016-07-21 13:56:58 -07:00
this . _pushElement ( el );
if ( selfClosing ) {
this . _popElement ( fullName );
el . endSourceSpan = span ;
}
}
private _pushElement ( el : html.Element ) {
2017-07-26 14:53:59 -07:00
const parentEl = this . _getParentElement ();
if ( parentEl && this . getTagDefinition ( parentEl . name ). isClosedByChild ( el . name )) {
this . _elementStack . pop ();
2016-07-21 13:56:58 -07:00
}
const tagDef = this . getTagDefinition ( el . name );
const { parent , container } = this . _getParentElementSkippingContainers ();
2017-01-04 13:59:43 -08:00
if ( parent && tagDef . requireExtraParent ( parent . name )) {
2016-07-21 13:56:58 -07:00
const newParent = new html . Element (
tagDef . parentToAdd , [], [], el . sourceSpan , el . startSourceSpan , el . endSourceSpan );
this . _insertBeforeContainer ( parent , container , newParent );
}
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 ()) {
2017-03-24 09:59:58 -07:00
this . _getParentElement () ! . endSourceSpan = endTagToken . sourceSpan ;
2016-07-21 13:56:58 -07:00
}
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 )) {
2017-03-11 00:05:17 +03:00
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 ));
2016-07-21 13:56:58 -07:00
}
}
private _popElement ( fullName : string ) : boolean {
for ( let stackIndex = this . _elementStack . length - 1 ; stackIndex >= 0 ; stackIndex -- ) {
const el = this . _elementStack [ stackIndex ];
if ( el . name == fullName ) {
2016-10-21 15:14:44 -07:00
this . _elementStack . splice ( stackIndex , this . _elementStack . length - stackIndex );
2016-07-21 13:56:58 -07:00
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 = '' ;
2017-03-24 09:59:58 -07:00
let valueSpan : ParseSourceSpan = undefined ! ;
2016-07-21 13:56:58 -07:00
if ( this . _peek . type === lex . TokenType . ATTR_VALUE ) {
const valueToken = this . _advance ();
value = valueToken . parts [ 0 ];
end = valueToken . sourceSpan . end ;
2016-10-07 13:53:29 -07:00
valueSpan = valueToken . sourceSpan ;
2016-07-21 13:56:58 -07:00
}
2016-10-07 13:53:29 -07:00
return new html . Attribute (
fullName , value , new ParseSourceSpan ( attrName . sourceSpan . start , end ), valueSpan );
2016-07-21 13:56:58 -07:00
}
2017-03-24 09:59:58 -07:00
private _getParentElement () : html . Element | null {
2016-10-21 15:14:44 -07:00
return this . _elementStack . length > 0 ? this . _elementStack [ this . _elementStack . length - 1 ] : null ;
2016-07-21 13:56:58 -07:00
}
/**
* Returns the parent in the DOM and the container.
*
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
*/
2017-04-13 10:37:45 -07:00
private _getParentElementSkippingContainers () :
2017-07-26 14:53:59 -07:00
{ parent : html.Element | null , container : html.Element | null } {
2017-04-13 10:37:45 -07:00
let container : html.Element | null = null ;
2016-07-21 13:56:58 -07:00
for ( let i = this . _elementStack . length - 1 ; i >= 0 ; i -- ) {
2017-04-13 10:37:45 -07:00
if ( ! isNgContainer ( this . _elementStack [ i ]. name )) {
2016-07-21 13:56:58 -07:00
return { parent : this._elementStack [ i ], container };
}
container = this . _elementStack [ i ];
}
2017-07-26 14:53:59 -07:00
return { parent : null , container };
2016-07-21 13:56:58 -07:00
}
private _addToParent ( node : html.Node ) {
const parent = this . _getParentElement ();
2017-03-02 09:37:01 -08:00
if ( parent != null ) {
2016-07-21 13:56:58 -07:00
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 (
2017-04-13 10:37:45 -07:00
parent : html.Element , container : html.Element | null , node : html.Element ) {
2016-07-21 13:56:58 -07:00
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 );
}
}
2017-03-24 09:59:58 -07:00
private _getElementFullName ( prefix : string , localName : string , parentElement : html.Element | null ) :
2016-07-21 13:56:58 -07:00
string {
2017-03-02 09:37:01 -08:00
if ( prefix == null ) {
2017-03-24 09:59:58 -07:00
prefix = this . getTagDefinition ( localName ). implicitNamespacePrefix ! ;
2017-03-02 09:37:01 -08:00
if ( prefix == null && parentElement != null ) {
2016-07-21 13:56:58 -07:00
prefix = getNsPrefix ( parentElement . name );
}
}
return mergeNsAndName ( prefix , localName );
}
}
function lastOnStack ( stack : any [], element : any ) : boolean {
return stack . length > 0 && stack [ stack . length - 1 ] === element ;
}