@@ -25,73 +25,145 @@ class HtmlScan {
2525 tagNames
2626 } ) {
2727 this . sourceName = sourceName ;
28- this . originalContents = contents ;
29- this . contents = normalizeCarriageReturns ( contents ) . replace ( tagCommentRegex , '' ) ;
28+ this . contents = contents ;
3029 this . tagNames = tagNames ;
3130
31+ this . rest = contents ;
32+ this . index = 0 ;
33+
3234 this . tags = [ ] ;
3335
34- let result ;
36+ tagNameRegex = this . tagNames . join ( "|" ) ;
37+ const openTagRegex = new RegExp ( `^((<(${ tagNameRegex } )\\b)|(<!--)|(<!DOCTYPE|{{!)|$)` , "i" ) ;
3538
36- // Unique tags: Template & Script
37- while ( result = expandedTagRegex . exec ( this . contents ) ) {
38- this . addTagFromResult ( result ) ;
39- }
39+ while ( this . rest ) {
40+ // skip whitespace first (for better line numbers)
41+ this . advance ( this . rest . match ( / ^ \s * / ) [ 0 ] . length ) ;
4042
41- // Multiple styles
42- while ( result = limitedTagRegex . exec ( this . contents ) ) {
43- this . addTagFromResult ( result ) ;
44- }
45- }
43+ const match = openTagRegex . exec ( this . rest ) ;
44+
45+ if ( ! match ) {
46+ this . throwCompileError ( `Expected one of: < ${ this . tagNames . join ( '>, <' ) } >` ) ;
47+ }
4648
47- addTagFromResult ( result ) {
48- let tagName = result [ 1 ] ;
49- let attrs = result [ 2 ] ;
50- let tagContents = result [ 3 ] ;
49+ const matchToken = match [ 1 ] ;
50+ const matchTokenTagName = match [ 3 ] ;
51+ const matchTokenComment = match [ 4 ] ;
52+ const matchTokenUnsupported = match [ 5 ] ;
5153
52- let tagAttribs = { } ;
53- if ( attrs ) {
54- let attr ;
55- while ( attr = attrsRegex . exec ( attrs ) ) {
56- let attrValue ;
57- if ( attr . length === 5 ) {
58- attrValue = attr [ 4 ] ;
59- if ( attrValue === undefined ) {
60- attrValue = true ;
61- }
62- } else {
63- attrValue = true ;
64- }
65- tagAttribs [ attr [ 1 ] ] = attrValue ;
54+ const tagStartIndex = this . index ;
55+ this . advance ( match . index + match [ 0 ] . length ) ;
56+
57+ if ( ! matchToken ) {
58+ break ; // matched $ (end of file)
6659 }
67- }
6860
69- const originalContents = this . originalContents ;
70-
71- const tag = {
72- tagName : tagName ,
73- attribs : tagAttribs ,
74- contents : tagContents ,
75- fileContents : this . contents ,
76- sourceName : this . sourceName ,
77- _tagStartIndex : null ,
78- get tagStartIndex ( ) {
79- if ( this . _tagStartIndex === null ) {
80- this . _tagStartIndex = originalContents . indexOf ( tagContents . substr ( 0 , 10 ) ) ;
61+ if ( matchTokenComment === '<!--' ) {
62+ // top-level HTML comment
63+ const commentEnd = / - - \s * > / . exec ( this . rest ) ;
64+ if ( ! commentEnd )
65+ this . throwCompileError ( "unclosed HTML comment in template file" ) ;
66+ this . advance ( commentEnd . index + commentEnd [ 0 ] . length ) ;
67+ continue ;
68+ }
69+
70+ if ( matchTokenUnsupported ) {
71+ switch ( matchTokenUnsupported . toLowerCase ( ) ) {
72+ case '<!doctype' :
73+ this . throwCompileError (
74+ "Can't set DOCTYPE here. (Meteor sets <!DOCTYPE html> for you)" ) ;
75+ case '{{!' :
76+ this . throwCompileError (
77+ "Can't use '{{! }}' outside a template. Use '<!-- -->'." ) ;
8178 }
82- return this . _tagStartIndex ;
83- } ,
84- _tagEndIndex : null ,
85- get tagEndIndex ( ) {
86- if ( this . _tagEndIndex === null ) {
87- this . _tagEndIndex = originalContents . indexOf ( '</script>' ) - 1 ;
79+
80+ this . throwCompileError ( ) ;
81+ }
82+
83+ // otherwise, a <tag>
84+ const tagName = matchTokenTagName . toLowerCase ( ) ;
85+ const tagAttribs = { } ; // bare name -> value dict
86+ const tagPartRegex = / ^ \s * ( ( ( [ a - z A - Z 0 - 9 : _ - ] + ) \s * ( = \s * ( [ " ' ] ) ( .* ?) \5) ? ) | ( > ) ) / ;
87+
88+ // read attributes
89+ let attr ;
90+ while ( ( attr = tagPartRegex . exec ( this . rest ) ) ) {
91+ const attrToken = attr [ 1 ] ;
92+ const attrKey = attr [ 3 ] ;
93+ let attrValue = attr [ 6 ] ;
94+ this . advance ( attr . index + attr [ 0 ] . length ) ;
95+
96+ if ( attrToken === '>' ) {
97+ break ;
8898 }
89- return this . _tagEndIndex ;
99+
100+ // XXX we don't HTML unescape the attribute value
101+ // (e.g. to allow "abcd"efg") or protect against
102+ // collisions with methods of tagAttribs (e.g. for
103+ // a property named toString)
104+ attrValue = attrValue && attrValue . match ( / ^ \s * ( [ \s \S ] * ?) \s * $ / ) [ 1 ] ; // trim
105+ tagAttribs [ attrKey ] = attrValue ;
90106 }
91- } ;
92107
93- // save the tag
94- this . tags . push ( tag ) ;
108+ if ( ! attr ) { // didn't end on '>'
109+ this . throwCompileError ( `Parse error in tag ${ tagName } ` ) ;
110+ }
111+
112+ // find </tag>
113+ const end = ( new RegExp ( '</' + tagName + '\\s*>' , 'i' ) ) . exec ( this . rest ) ;
114+ if ( ! end ) {
115+ this . throwCompileError ( "unclosed <" + tagName + ">" ) ;
116+ }
117+
118+ const tagContents = this . rest . slice ( 0 , end . index ) ;
119+ const contentsStartIndex = this . index ;
120+
121+ // trim the tag contents.
122+ // this is a courtesy and is also relied on by some unit tests.
123+ var m = tagContents . match ( / ^ ( [ \t \r \n ] * ) ( [ \s \S ] * ?) [ \t \r \n ] * $ / ) ;
124+ const trimmedContentsStartIndex = contentsStartIndex + m [ 1 ] . length ;
125+ const trimmedTagContents = m [ 2 ] ;
126+
127+ const tag = {
128+ tagName : tagName ,
129+ attribs : tagAttribs ,
130+ contents : trimmedTagContents ,
131+ contentsStartIndex : trimmedContentsStartIndex ,
132+ tagStartIndex : tagStartIndex ,
133+ fileContents : this . contents ,
134+ sourceName : this . sourceName
135+ } ;
136+
137+ // save the tag
138+ this . tags . push ( tag ) ;
139+
140+ // advance afterwards, so that line numbers in errors are correct
141+ this . advance ( end . index + end [ 0 ] . length ) ;
142+ }
143+ }
144+
145+ /**
146+ * Advance the parser
147+ * @param {Number } amount The amount of characters to advance
148+ */
149+ advance ( amount ) {
150+ this . rest = this . rest . substring ( amount ) ;
151+ this . index += amount ;
152+ }
153+
154+ throwCompileError ( msg , overrideIndex ) {
155+ const finalIndex = ( typeof overrideIndex === 'number' ? overrideIndex : this . index ) ;
156+
157+ const err = new TemplatingTools . CompileError ( ) ;
158+ err . message = msg || "bad formatting in template file" ;
159+ err . file = this . sourceName ;
160+ err . line = this . contents . substring ( 0 , finalIndex ) . split ( '\n' ) . length ;
161+
162+ throw err ;
163+ }
164+
165+ throwBodyAttrsError ( msg ) {
166+ this . parseError ( msg ) ;
95167 }
96168
97169 getTags ( ) {
0 commit comments