@@ -25,73 +25,145 @@ class HtmlScan {
25
25
tagNames
26
26
} ) {
27
27
this . sourceName = sourceName ;
28
- this . originalContents = contents ;
29
- this . contents = normalizeCarriageReturns ( contents ) . replace ( tagCommentRegex , '' ) ;
28
+ this . contents = contents ;
30
29
this . tagNames = tagNames ;
31
30
31
+ this . rest = contents ;
32
+ this . index = 0 ;
33
+
32
34
this . tags = [ ] ;
33
35
34
- let result ;
36
+ tagNameRegex = this . tagNames . join ( "|" ) ;
37
+ const openTagRegex = new RegExp ( `^((<(${ tagNameRegex } )\\b)|(<!--)|(<!DOCTYPE|{{!)|$)` , "i" ) ;
35
38
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 ) ;
40
42
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
+ }
46
48
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 ] ;
51
53
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)
66
59
}
67
- }
68
60
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 '<!-- -->'." ) ;
81
78
}
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 ;
88
98
}
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 ;
90
106
}
91
- } ;
92
107
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 ) ;
95
167
}
96
168
97
169
getTags ( ) {
0 commit comments