...
Run Format

Source file src/html/template/transition.go

Documentation: html/template

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

View as plain text