// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package template import ( "bytes" "strings" ) // transitionFunc is the array of context transition functions for text nodes. // A transition function takes a context and template text input, and returns // the updated context and the number of bytes consumed from the front of the // input. var transitionFunc = [...]func(context, []byte) (context, int){ stateText: tText, stateTag: tTag, stateAttrName: tAttrName, stateAfterName: tAfterName, stateBeforeValue: tBeforeValue, stateHTMLCmt: tHTMLCmt, stateRCDATA: tSpecialTagEnd, stateAttr: tAttr, stateURL: tURL, stateSrcset: tURL, stateJS: tJS, stateJSDqStr: tJSDelimited, stateJSSqStr: tJSDelimited, stateJSBqStr: tJSDelimited, stateJSRegexp: tJSDelimited, stateJSBlockCmt: tBlockCmt, stateJSLineCmt: tLineCmt, stateJSHTMLOpenCmt: tLineCmt, stateJSHTMLCloseCmt: tLineCmt, stateCSS: tCSS, stateCSSDqStr: tCSSStr, stateCSSSqStr: tCSSStr, stateCSSDqURL: tCSSStr, stateCSSSqURL: tCSSStr, stateCSSURL: tCSSStr, stateCSSBlockCmt: tBlockCmt, stateCSSLineCmt: tLineCmt, stateError: tError, } var commentStart = []byte("") // tText is the context transition function for the text state. func tText(c context, s []byte) (context, int) { k := 0 for { i := k + bytes.IndexByte(s[k:], '<') if i < k || i+1 == len(s) { return c, len(s) } else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) { return context{state: stateHTMLCmt}, i + 4 } i++ end := false if s[i] == '/' { if i+1 == len(s) { return c, len(s) } end, i = true, i+1 } j, e := eatTagName(s, i) if j != i { if end { e = elementNone } // We've found an HTML tag. return context{state: stateTag, element: e}, j } k = j } } var elementContentType = [...]state{ elementNone: stateText, elementScript: stateJS, elementStyle: stateCSS, elementTextarea: stateRCDATA, elementTitle: stateRCDATA, } // tTag is the context transition function for the tag state. func tTag(c context, s []byte) (context, int) { // Find the attribute name. i := eatWhiteSpace(s, 0) if i == len(s) { return c, len(s) } if s[i] == '>' { return context{ state: elementContentType[c.element], element: c.element, }, i + 1 } j, err := eatAttrName(s, i) if err != nil { return context{state: stateError, err: err}, len(s) } state, attr := stateTag, attrNone if i == j { return context{ state: stateError, err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]), }, len(s) } attrName := strings.ToLower(string(s[i:j])) if c.element == elementScript && attrName == "type" { attr = attrScriptType } else { switch attrType(attrName) { case contentTypeURL: attr = attrURL case contentTypeCSS: attr = attrStyle case contentTypeJS: attr = attrScript case contentTypeSrcset: attr = attrSrcset } } if j == len(s) { state = stateAttrName } else { state = stateAfterName } return context{state: state, element: c.element, attr: attr}, j } // tAttrName is the context transition function for stateAttrName. func tAttrName(c context, s []byte) (context, int) { i, err := eatAttrName(s, 0) if err != nil { return context{state: stateError, err: err}, len(s) } else if i != len(s) { c.state = stateAfterName } return c, i } // tAfterName is the context transition function for stateAfterName. func tAfterName(c context, s []byte) (context, int) { // Look for the start of the value. i := eatWhiteSpace(s, 0) if i == len(s) { return c, len(s) } else if s[i] != '=' { // Occurs due to tag ending '>', and valueless attribute. c.state = stateTag return c, i } c.state = stateBeforeValue // Consume the "=". return c, i + 1 } var attrStartStates = [...]state{ attrNone: stateAttr, attrScript: stateJS, attrScriptType: stateAttr, attrStyle: stateCSS, attrURL: stateURL, attrSrcset: stateSrcset, } // tBeforeValue is the context transition function for stateBeforeValue. func tBeforeValue(c context, s []byte) (context, int) { i := eatWhiteSpace(s, 0) if i == len(s) { return c, len(s) } // Find the attribute delimiter. delim := delimSpaceOrTagEnd switch s[i] { case '\'': delim, i = delimSingleQuote, i+1 case '"': delim, i = delimDoubleQuote, i+1 } c.state, c.delim = attrStartStates[c.attr], delim return c, i } // tHTMLCmt is the context transition function for stateHTMLCmt. func tHTMLCmt(c context, s []byte) (context, int) { if i := bytes.Index(s, commentEnd); i != -1 { return context{}, i + 3 } return c, len(s) } // specialTagEndMarkers maps element types to the character sequence that // case-insensitively signals the end of the special tag body. var specialTagEndMarkers = [...][]byte{ elementScript: []byte("script"), elementStyle: []byte("style"), elementTextarea: []byte("textarea"), elementTitle: []byte("title"), } var ( specialTagEndPrefix = []byte("") tagEndSeparators = []byte("> \t\n\f/") ) // tSpecialTagEnd is the context transition function for raw text and RCDATA // element states. func tSpecialTagEnd(c context, s []byte) (context, int) { if c.element != elementNone { // script end tags (" 0 { // Try to find the tag end prefix first i := bytes.Index(s, specialTagEndPrefix) if i == -1 { return i } s = s[i+plen:] // Try to match the actual tag if there is still space for it if len(tag) <= len(s) && bytes.EqualFold(tag, s[:len(tag)]) { s = s[len(tag):] // Check the tag is followed by a proper separator if len(s) > 0 && bytes.IndexByte(tagEndSeparators, s[0]) != -1 { return res + i } res += len(tag) } res += i + plen } return -1 } // tAttr is the context transition function for the attribute state. func tAttr(c context, s []byte) (context, int) { return c, len(s) } // tURL is the context transition function for the URL state. func tURL(c context, s []byte) (context, int) { if bytes.ContainsAny(s, "#?") { c.urlPart = urlPartQueryOrFrag } else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone { // HTML5 uses "Valid URL potentially surrounded by spaces" for // attrs: https://www.w3.org/TR/html5/index.html#attributes-1 c.urlPart = urlPartPreQuery } return c, len(s) } // tJS is the context transition function for the JS state. func tJS(c context, s []byte) (context, int) { i := bytes.IndexAny(s, "\"`'/<-#") if i == -1 { // Entire input is non string, comment, regexp tokens. c.jsCtx = nextJSCtx(s, c.jsCtx) return c, len(s) } c.jsCtx = nextJSCtx(s[:i], c.jsCtx) switch s[i] { case '"': c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp case '\'': c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp case '`': c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp case '/': switch { case i+1 < len(s) && s[i+1] == '/': c.state, i = stateJSLineCmt, i+1 case i+1 < len(s) && s[i+1] == '*': c.state, i = stateJSBlockCmt, i+1 case c.jsCtx == jsCtxRegexp: c.state = stateJSRegexp case c.jsCtx == jsCtxDivOp: c.jsCtx = jsCtxRegexp default: return context{ state: stateError, err: errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]), }, len(s) } // ECMAScript supports HTML style comments for legacy reasons, see Appendix // B.1.1 "HTML-like Comments". The handling of these comments is somewhat // confusing. Multi-line comments are not supported, i.e. anything on lines // between the opening and closing tokens is not considered a comment, but // anything following the opening or closing token, on the same line, is // ignored. As such we simply treat any line prefixed with "" // as if it were actually prefixed with "//" and move on. case '<': if i+3 < len(s) && bytes.Equal(commentStart, s[i:i+4]) { c.state, i = stateJSHTMLOpenCmt, i+3 } case '-': if i+2 < len(s) && bytes.Equal(commentEnd, s[i:i+3]) { c.state, i = stateJSHTMLCloseCmt, i+2 } // ECMAScript also supports "hashbang" comment lines, see Section 12.5. case '#': if i+1 < len(s) && s[i+1] == '!' { c.state, i = stateJSLineCmt, i+1 } default: panic("unreachable") } return c, i + 1 } // tJSDelimited is the context transition function for the JS string and regexp // states. func tJSDelimited(c context, s []byte) (context, int) { specials := `\"` switch c.state { case stateJSSqStr: specials = `\'` case stateJSBqStr: specials = "`\\" case stateJSRegexp: specials = `\/[]` } k, inCharset := 0, false for { i := k + bytes.IndexAny(s[k:], specials) if i < k { break } switch s[i] { case '\\': i++ if i == len(s) { return context{ state: stateError, err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s), }, len(s) } case '[': inCharset = true case ']': inCharset = false case '/': // If " 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte(" // ul.inlineList { list-style: none; padding:0 } // ul.inlineList > li { display: inline } // ul.inlineList > li:before { content: ", " } // ul.inlineList > li:first-child:before { content: "" } // //