]> Cypherpunks.ru repositories - gostls13.git/blob - src/html/template/transition.go
html/template: support parsing complex JS template literals
[gostls13.git] / src / html / template / transition.go
1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package template
6
7 import (
8         "bytes"
9         "strings"
10 )
11
12 // transitionFunc is the array of context transition functions for text nodes.
13 // A transition function takes a context and template text input, and returns
14 // the updated context and the number of bytes consumed from the front of the
15 // input.
16 var transitionFunc = [...]func(context, []byte) (context, int){
17         stateText:           tText,
18         stateTag:            tTag,
19         stateAttrName:       tAttrName,
20         stateAfterName:      tAfterName,
21         stateBeforeValue:    tBeforeValue,
22         stateHTMLCmt:        tHTMLCmt,
23         stateRCDATA:         tSpecialTagEnd,
24         stateAttr:           tAttr,
25         stateURL:            tURL,
26         stateSrcset:         tURL,
27         stateJS:             tJS,
28         stateJSDqStr:        tJSDelimited,
29         stateJSSqStr:        tJSDelimited,
30         stateJSRegexp:       tJSDelimited,
31         stateJSTmplLit:      tJSTmpl,
32         stateJSBlockCmt:     tBlockCmt,
33         stateJSLineCmt:      tLineCmt,
34         stateJSHTMLOpenCmt:  tLineCmt,
35         stateJSHTMLCloseCmt: tLineCmt,
36         stateCSS:            tCSS,
37         stateCSSDqStr:       tCSSStr,
38         stateCSSSqStr:       tCSSStr,
39         stateCSSDqURL:       tCSSStr,
40         stateCSSSqURL:       tCSSStr,
41         stateCSSURL:         tCSSStr,
42         stateCSSBlockCmt:    tBlockCmt,
43         stateCSSLineCmt:     tLineCmt,
44         stateError:          tError,
45 }
46
47 var commentStart = []byte("<!--")
48 var commentEnd = []byte("-->")
49
50 // tText is the context transition function for the text state.
51 func tText(c context, s []byte) (context, int) {
52         k := 0
53         for {
54                 i := k + bytes.IndexByte(s[k:], '<')
55                 if i < k || i+1 == len(s) {
56                         return c, len(s)
57                 } else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) {
58                         return context{state: stateHTMLCmt}, i + 4
59                 }
60                 i++
61                 end := false
62                 if s[i] == '/' {
63                         if i+1 == len(s) {
64                                 return c, len(s)
65                         }
66                         end, i = true, i+1
67                 }
68                 j, e := eatTagName(s, i)
69                 if j != i {
70                         if end {
71                                 e = elementNone
72                         }
73                         // We've found an HTML tag.
74                         return context{state: stateTag, element: e}, j
75                 }
76                 k = j
77         }
78 }
79
80 var elementContentType = [...]state{
81         elementNone:     stateText,
82         elementScript:   stateJS,
83         elementStyle:    stateCSS,
84         elementTextarea: stateRCDATA,
85         elementTitle:    stateRCDATA,
86 }
87
88 // tTag is the context transition function for the tag state.
89 func tTag(c context, s []byte) (context, int) {
90         // Find the attribute name.
91         i := eatWhiteSpace(s, 0)
92         if i == len(s) {
93                 return c, len(s)
94         }
95         if s[i] == '>' {
96                 return context{
97                         state:   elementContentType[c.element],
98                         element: c.element,
99                 }, i + 1
100         }
101         j, err := eatAttrName(s, i)
102         if err != nil {
103                 return context{state: stateError, err: err}, len(s)
104         }
105         state, attr := stateTag, attrNone
106         if i == j {
107                 return context{
108                         state: stateError,
109                         err:   errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]),
110                 }, len(s)
111         }
112
113         attrName := strings.ToLower(string(s[i:j]))
114         if c.element == elementScript && attrName == "type" {
115                 attr = attrScriptType
116         } else {
117                 switch attrType(attrName) {
118                 case contentTypeURL:
119                         attr = attrURL
120                 case contentTypeCSS:
121                         attr = attrStyle
122                 case contentTypeJS:
123                         attr = attrScript
124                 case contentTypeSrcset:
125                         attr = attrSrcset
126                 }
127         }
128
129         if j == len(s) {
130                 state = stateAttrName
131         } else {
132                 state = stateAfterName
133         }
134         return context{state: state, element: c.element, attr: attr}, j
135 }
136
137 // tAttrName is the context transition function for stateAttrName.
138 func tAttrName(c context, s []byte) (context, int) {
139         i, err := eatAttrName(s, 0)
140         if err != nil {
141                 return context{state: stateError, err: err}, len(s)
142         } else if i != len(s) {
143                 c.state = stateAfterName
144         }
145         return c, i
146 }
147
148 // tAfterName is the context transition function for stateAfterName.
149 func tAfterName(c context, s []byte) (context, int) {
150         // Look for the start of the value.
151         i := eatWhiteSpace(s, 0)
152         if i == len(s) {
153                 return c, len(s)
154         } else if s[i] != '=' {
155                 // Occurs due to tag ending '>', and valueless attribute.
156                 c.state = stateTag
157                 return c, i
158         }
159         c.state = stateBeforeValue
160         // Consume the "=".
161         return c, i + 1
162 }
163
164 var attrStartStates = [...]state{
165         attrNone:       stateAttr,
166         attrScript:     stateJS,
167         attrScriptType: stateAttr,
168         attrStyle:      stateCSS,
169         attrURL:        stateURL,
170         attrSrcset:     stateSrcset,
171 }
172
173 // tBeforeValue is the context transition function for stateBeforeValue.
174 func tBeforeValue(c context, s []byte) (context, int) {
175         i := eatWhiteSpace(s, 0)
176         if i == len(s) {
177                 return c, len(s)
178         }
179         // Find the attribute delimiter.
180         delim := delimSpaceOrTagEnd
181         switch s[i] {
182         case '\'':
183                 delim, i = delimSingleQuote, i+1
184         case '"':
185                 delim, i = delimDoubleQuote, i+1
186         }
187         c.state, c.delim = attrStartStates[c.attr], delim
188         return c, i
189 }
190
191 // tHTMLCmt is the context transition function for stateHTMLCmt.
192 func tHTMLCmt(c context, s []byte) (context, int) {
193         if i := bytes.Index(s, commentEnd); i != -1 {
194                 return context{}, i + 3
195         }
196         return c, len(s)
197 }
198
199 // specialTagEndMarkers maps element types to the character sequence that
200 // case-insensitively signals the end of the special tag body.
201 var specialTagEndMarkers = [...][]byte{
202         elementScript:   []byte("script"),
203         elementStyle:    []byte("style"),
204         elementTextarea: []byte("textarea"),
205         elementTitle:    []byte("title"),
206 }
207
208 var (
209         specialTagEndPrefix = []byte("</")
210         tagEndSeparators    = []byte("> \t\n\f/")
211 )
212
213 // tSpecialTagEnd is the context transition function for raw text and RCDATA
214 // element states.
215 func tSpecialTagEnd(c context, s []byte) (context, int) {
216         if c.element != elementNone {
217                 // script end tags ("</script") within script literals are ignored, so that
218                 // we can properly escape them.
219                 if c.element == elementScript && (isInScriptLiteral(c.state) || isComment(c.state)) {
220                         return c, len(s)
221                 }
222                 if i := indexTagEnd(s, specialTagEndMarkers[c.element]); i != -1 {
223                         return context{}, i
224                 }
225         }
226         return c, len(s)
227 }
228
229 // indexTagEnd finds the index of a special tag end in a case insensitive way, or returns -1
230 func indexTagEnd(s []byte, tag []byte) int {
231         res := 0
232         plen := len(specialTagEndPrefix)
233         for len(s) > 0 {
234                 // Try to find the tag end prefix first
235                 i := bytes.Index(s, specialTagEndPrefix)
236                 if i == -1 {
237                         return i
238                 }
239                 s = s[i+plen:]
240                 // Try to match the actual tag if there is still space for it
241                 if len(tag) <= len(s) && bytes.EqualFold(tag, s[:len(tag)]) {
242                         s = s[len(tag):]
243                         // Check the tag is followed by a proper separator
244                         if len(s) > 0 && bytes.IndexByte(tagEndSeparators, s[0]) != -1 {
245                                 return res + i
246                         }
247                         res += len(tag)
248                 }
249                 res += i + plen
250         }
251         return -1
252 }
253
254 // tAttr is the context transition function for the attribute state.
255 func tAttr(c context, s []byte) (context, int) {
256         return c, len(s)
257 }
258
259 // tURL is the context transition function for the URL state.
260 func tURL(c context, s []byte) (context, int) {
261         if bytes.ContainsAny(s, "#?") {
262                 c.urlPart = urlPartQueryOrFrag
263         } else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone {
264                 // HTML5 uses "Valid URL potentially surrounded by spaces" for
265                 // attrs: https://www.w3.org/TR/html5/index.html#attributes-1
266                 c.urlPart = urlPartPreQuery
267         }
268         return c, len(s)
269 }
270
271 // tJS is the context transition function for the JS state.
272 func tJS(c context, s []byte) (context, int) {
273         i := bytes.IndexAny(s, "\"`'/{}<-#")
274         if i == -1 {
275                 // Entire input is non string, comment, regexp tokens.
276                 c.jsCtx = nextJSCtx(s, c.jsCtx)
277                 return c, len(s)
278         }
279         c.jsCtx = nextJSCtx(s[:i], c.jsCtx)
280         switch s[i] {
281         case '"':
282                 c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
283         case '\'':
284                 c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
285         case '`':
286                 c.state, c.jsCtx = stateJSTmplLit, jsCtxRegexp
287         case '/':
288                 switch {
289                 case i+1 < len(s) && s[i+1] == '/':
290                         c.state, i = stateJSLineCmt, i+1
291                 case i+1 < len(s) && s[i+1] == '*':
292                         c.state, i = stateJSBlockCmt, i+1
293                 case c.jsCtx == jsCtxRegexp:
294                         c.state = stateJSRegexp
295                 case c.jsCtx == jsCtxDivOp:
296                         c.jsCtx = jsCtxRegexp
297                 default:
298                         return context{
299                                 state: stateError,
300                                 err:   errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]),
301                         }, len(s)
302                 }
303         // ECMAScript supports HTML style comments for legacy reasons, see Appendix
304         // B.1.1 "HTML-like Comments". The handling of these comments is somewhat
305         // confusing. Multi-line comments are not supported, i.e. anything on lines
306         // between the opening and closing tokens is not considered a comment, but
307         // anything following the opening or closing token, on the same line, is
308         // ignored. As such we simply treat any line prefixed with "<!--" or "-->"
309         // as if it were actually prefixed with "//" and move on.
310         case '<':
311                 if i+3 < len(s) && bytes.Equal(commentStart, s[i:i+4]) {
312                         c.state, i = stateJSHTMLOpenCmt, i+3
313                 }
314         case '-':
315                 if i+2 < len(s) && bytes.Equal(commentEnd, s[i:i+3]) {
316                         c.state, i = stateJSHTMLCloseCmt, i+2
317                 }
318         // ECMAScript also supports "hashbang" comment lines, see Section 12.5.
319         case '#':
320                 if i+1 < len(s) && s[i+1] == '!' {
321                         c.state, i = stateJSLineCmt, i+1
322                 }
323         case '{':
324                 c.jsBraceDepth++
325         case '}':
326                 if c.jsTmplExprDepth == 0 {
327                         return c, i + 1
328                 }
329                 for j := 0; j <= i; j++ {
330                         switch s[j] {
331                         case '\\':
332                                 j++
333                         case '{':
334                                 c.jsBraceDepth++
335                         case '}':
336                                 c.jsBraceDepth--
337                         }
338                 }
339                 if c.jsBraceDepth >= 0 {
340                         return c, i + 1
341                 }
342                 c.jsTmplExprDepth--
343                 c.jsBraceDepth = 0
344                 c.state = stateJSTmplLit
345         default:
346                 panic("unreachable")
347         }
348         return c, i + 1
349 }
350
351 func tJSTmpl(c context, s []byte) (context, int) {
352         var k int
353         for {
354                 i := k + bytes.IndexAny(s[k:], "`\\$")
355                 if i < k {
356                         break
357                 }
358                 switch s[i] {
359                 case '\\':
360                         i++
361                         if i == len(s) {
362                                 return context{
363                                         state: stateError,
364                                         err:   errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
365                                 }, len(s)
366                         }
367                 case '$':
368                         if len(s) >= i+2 && s[i+1] == '{' {
369                                 c.jsTmplExprDepth++
370                                 c.state = stateJS
371                                 return c, i + 2
372                         }
373                 case '`':
374                         // end
375                         c.state = stateJS
376                         return c, i + 1
377                 }
378                 k = i + 1
379         }
380
381         return c, len(s)
382 }
383
384 // tJSDelimited is the context transition function for the JS string and regexp
385 // states.
386 func tJSDelimited(c context, s []byte) (context, int) {
387         specials := `\"`
388         switch c.state {
389         case stateJSSqStr:
390                 specials = `\'`
391         case stateJSRegexp:
392                 specials = `\/[]`
393         }
394
395         k, inCharset := 0, false
396         for {
397                 i := k + bytes.IndexAny(s[k:], specials)
398                 if i < k {
399                         break
400                 }
401                 switch s[i] {
402                 case '\\':
403                         i++
404                         if i == len(s) {
405                                 return context{
406                                         state: stateError,
407                                         err:   errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
408                                 }, len(s)
409                         }
410                 case '[':
411                         inCharset = true
412                 case ']':
413                         inCharset = false
414                 case '/':
415                         // If "</script" appears in a regex literal, the '/' should not
416                         // close the regex literal, and it will later be escaped to
417                         // "\x3C/script" in escapeText.
418                         if i > 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte("</script")) == 0 {
419                                 i++
420                         } else if !inCharset {
421                                 c.state, c.jsCtx = stateJS, jsCtxDivOp
422                                 return c, i + 1
423                         }
424                 default:
425                         // end delimiter
426                         if !inCharset {
427                                 c.state, c.jsCtx = stateJS, jsCtxDivOp
428                                 return c, i + 1
429                         }
430                 }
431                 k = i + 1
432         }
433
434         if inCharset {
435                 // This can be fixed by making context richer if interpolation
436                 // into charsets is desired.
437                 return context{
438                         state: stateError,
439                         err:   errorf(ErrPartialCharset, nil, 0, "unfinished JS regexp charset: %q", s),
440                 }, len(s)
441         }
442
443         return c, len(s)
444 }
445
446 var blockCommentEnd = []byte("*/")
447
448 // tBlockCmt is the context transition function for /*comment*/ states.
449 func tBlockCmt(c context, s []byte) (context, int) {
450         i := bytes.Index(s, blockCommentEnd)
451         if i == -1 {
452                 return c, len(s)
453         }
454         switch c.state {
455         case stateJSBlockCmt:
456                 c.state = stateJS
457         case stateCSSBlockCmt:
458                 c.state = stateCSS
459         default:
460                 panic(c.state.String())
461         }
462         return c, i + 2
463 }
464
465 // tLineCmt is the context transition function for //comment states, and the JS HTML-like comment state.
466 func tLineCmt(c context, s []byte) (context, int) {
467         var lineTerminators string
468         var endState state
469         switch c.state {
470         case stateJSLineCmt, stateJSHTMLOpenCmt, stateJSHTMLCloseCmt:
471                 lineTerminators, endState = "\n\r\u2028\u2029", stateJS
472         case stateCSSLineCmt:
473                 lineTerminators, endState = "\n\f\r", stateCSS
474                 // Line comments are not part of any published CSS standard but
475                 // are supported by the 4 major browsers.
476                 // This defines line comments as
477                 //     LINECOMMENT ::= "//" [^\n\f\d]*
478                 // since https://www.w3.org/TR/css3-syntax/#SUBTOK-nl defines
479                 // newlines:
480                 //     nl ::= #xA | #xD #xA | #xD | #xC
481         default:
482                 panic(c.state.String())
483         }
484
485         i := bytes.IndexAny(s, lineTerminators)
486         if i == -1 {
487                 return c, len(s)
488         }
489         c.state = endState
490         // Per section 7.4 of EcmaScript 5 : https://es5.github.io/#x7.4
491         // "However, the LineTerminator at the end of the line is not
492         // considered to be part of the single-line comment; it is
493         // recognized separately by the lexical grammar and becomes part
494         // of the stream of input elements for the syntactic grammar."
495         return c, i
496 }
497
498 // tCSS is the context transition function for the CSS state.
499 func tCSS(c context, s []byte) (context, int) {
500         // CSS quoted strings are almost never used except for:
501         // (1) URLs as in background: "/foo.png"
502         // (2) Multiword font-names as in font-family: "Times New Roman"
503         // (3) List separators in content values as in inline-lists:
504         //    <style>
505         //    ul.inlineList { list-style: none; padding:0 }
506         //    ul.inlineList > li { display: inline }
507         //    ul.inlineList > li:before { content: ", " }
508         //    ul.inlineList > li:first-child:before { content: "" }
509         //    </style>
510         //    <ul class=inlineList><li>One<li>Two<li>Three</ul>
511         // (4) Attribute value selectors as in a[href="http://example.com/"]
512         //
513         // We conservatively treat all strings as URLs, but make some
514         // allowances to avoid confusion.
515         //
516         // In (1), our conservative assumption is justified.
517         // In (2), valid font names do not contain ':', '?', or '#', so our
518         // conservative assumption is fine since we will never transition past
519         // urlPartPreQuery.
520         // In (3), our protocol heuristic should not be tripped, and there
521         // should not be non-space content after a '?' or '#', so as long as
522         // we only %-encode RFC 3986 reserved characters we are ok.
523         // In (4), we should URL escape for URL attributes, and for others we
524         // have the attribute name available if our conservative assumption
525         // proves problematic for real code.
526
527         k := 0
528         for {
529                 i := k + bytes.IndexAny(s[k:], `("'/`)
530                 if i < k {
531                         return c, len(s)
532                 }
533                 switch s[i] {
534                 case '(':
535                         // Look for url to the left.
536                         p := bytes.TrimRight(s[:i], "\t\n\f\r ")
537                         if endsWithCSSKeyword(p, "url") {
538                                 j := len(s) - len(bytes.TrimLeft(s[i+1:], "\t\n\f\r "))
539                                 switch {
540                                 case j != len(s) && s[j] == '"':
541                                         c.state, j = stateCSSDqURL, j+1
542                                 case j != len(s) && s[j] == '\'':
543                                         c.state, j = stateCSSSqURL, j+1
544                                 default:
545                                         c.state = stateCSSURL
546                                 }
547                                 return c, j
548                         }
549                 case '/':
550                         if i+1 < len(s) {
551                                 switch s[i+1] {
552                                 case '/':
553                                         c.state = stateCSSLineCmt
554                                         return c, i + 2
555                                 case '*':
556                                         c.state = stateCSSBlockCmt
557                                         return c, i + 2
558                                 }
559                         }
560                 case '"':
561                         c.state = stateCSSDqStr
562                         return c, i + 1
563                 case '\'':
564                         c.state = stateCSSSqStr
565                         return c, i + 1
566                 }
567                 k = i + 1
568         }
569 }
570
571 // tCSSStr is the context transition function for the CSS string and URL states.
572 func tCSSStr(c context, s []byte) (context, int) {
573         var endAndEsc string
574         switch c.state {
575         case stateCSSDqStr, stateCSSDqURL:
576                 endAndEsc = `\"`
577         case stateCSSSqStr, stateCSSSqURL:
578                 endAndEsc = `\'`
579         case stateCSSURL:
580                 // Unquoted URLs end with a newline or close parenthesis.
581                 // The below includes the wc (whitespace character) and nl.
582                 endAndEsc = "\\\t\n\f\r )"
583         default:
584                 panic(c.state.String())
585         }
586
587         k := 0
588         for {
589                 i := k + bytes.IndexAny(s[k:], endAndEsc)
590                 if i < k {
591                         c, nread := tURL(c, decodeCSS(s[k:]))
592                         return c, k + nread
593                 }
594                 if s[i] == '\\' {
595                         i++
596                         if i == len(s) {
597                                 return context{
598                                         state: stateError,
599                                         err:   errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in CSS string: %q", s),
600                                 }, len(s)
601                         }
602                 } else {
603                         c.state = stateCSS
604                         return c, i + 1
605                 }
606                 c, _ = tURL(c, decodeCSS(s[:i+1]))
607                 k = i + 1
608         }
609 }
610
611 // tError is the context transition function for the error state.
612 func tError(c context, s []byte) (context, int) {
613         return c, len(s)
614 }
615
616 // eatAttrName returns the largest j such that s[i:j] is an attribute name.
617 // It returns an error if s[i:] does not look like it begins with an
618 // attribute name, such as encountering a quote mark without a preceding
619 // equals sign.
620 func eatAttrName(s []byte, i int) (int, *Error) {
621         for j := i; j < len(s); j++ {
622                 switch s[j] {
623                 case ' ', '\t', '\n', '\f', '\r', '=', '>':
624                         return j, nil
625                 case '\'', '"', '<':
626                         // These result in a parse warning in HTML5 and are
627                         // indicative of serious problems if seen in an attr
628                         // name in a template.
629                         return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s)
630                 default:
631                         // No-op.
632                 }
633         }
634         return len(s), nil
635 }
636
637 var elementNameMap = map[string]element{
638         "script":   elementScript,
639         "style":    elementStyle,
640         "textarea": elementTextarea,
641         "title":    elementTitle,
642 }
643
644 // asciiAlpha reports whether c is an ASCII letter.
645 func asciiAlpha(c byte) bool {
646         return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
647 }
648
649 // asciiAlphaNum reports whether c is an ASCII letter or digit.
650 func asciiAlphaNum(c byte) bool {
651         return asciiAlpha(c) || '0' <= c && c <= '9'
652 }
653
654 // eatTagName returns the largest j such that s[i:j] is a tag name and the tag type.
655 func eatTagName(s []byte, i int) (int, element) {
656         if i == len(s) || !asciiAlpha(s[i]) {
657                 return i, elementNone
658         }
659         j := i + 1
660         for j < len(s) {
661                 x := s[j]
662                 if asciiAlphaNum(x) {
663                         j++
664                         continue
665                 }
666                 // Allow "x-y" or "x:y" but not "x-", "-y", or "x--y".
667                 if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) {
668                         j += 2
669                         continue
670                 }
671                 break
672         }
673         return j, elementNameMap[strings.ToLower(string(s[i:j]))]
674 }
675
676 // eatWhiteSpace returns the largest j such that s[i:j] is white space.
677 func eatWhiteSpace(s []byte, i int) int {
678         for j := i; j < len(s); j++ {
679                 switch s[j] {
680                 case ' ', '\t', '\n', '\f', '\r':
681                         // No-op.
682                 default:
683                         return j
684                 }
685         }
686         return len(s)
687 }