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.
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
16 var transitionFunc
= [...]func(context
, []byte) (context
, int){
19 stateAttrName
: tAttrName
,
20 stateAfterName
: tAfterName
,
21 stateBeforeValue
: tBeforeValue
,
22 stateHTMLCmt
: tHTMLCmt
,
23 stateRCDATA
: tSpecialTagEnd
,
27 stateJSDqStr
: tJSDelimited
,
28 stateJSSqStr
: tJSDelimited
,
29 stateJSRegexp
: tJSDelimited
,
30 stateJSBlockCmt
: tBlockCmt
,
31 stateJSLineCmt
: tLineCmt
,
33 stateCSSDqStr
: tCSSStr
,
34 stateCSSSqStr
: tCSSStr
,
35 stateCSSDqURL
: tCSSStr
,
36 stateCSSSqURL
: tCSSStr
,
38 stateCSSBlockCmt
: tBlockCmt
,
39 stateCSSLineCmt
: tLineCmt
,
43 var commentStart
= []byte("<!--")
44 var commentEnd
= []byte("-->")
46 // tText is the context transition function for the text state.
47 func tText(c context
, s
[]byte) (context
, int) {
50 i
:= k
+ bytes
.IndexByte(s
[k
:], '<')
51 if i
< k || i
+1 == len(s
) {
53 } else if i
+4 <= len(s
) && bytes
.Equal(commentStart
, s
[i
:i
+4]) {
54 return context
{state
: stateHTMLCmt
}, i
+ 4
64 j
, e
:= eatTagName(s
, i
)
69 // We've found an HTML tag.
70 return context
{state
: stateTag
, element
: e
}, j
76 var elementContentType
= [...]state
{
77 elementNone
: stateText
,
78 elementScript
: stateJS
,
79 elementStyle
: stateCSS
,
80 elementTextarea
: stateRCDATA
,
81 elementTitle
: stateRCDATA
,
84 // tTag is the context transition function for the tag state.
85 func tTag(c context
, s
[]byte) (context
, int) {
86 // Find the attribute name.
87 i
:= eatWhiteSpace(s
, 0)
93 state
: elementContentType
[c
.element
],
97 j
, err
:= eatAttrName(s
, i
)
99 return context
{state
: stateError
, err
: err
}, len(s
)
101 state
, attr
:= stateTag
, attrNone
105 err
: errorf(ErrBadHTML
, nil, 0, "expected space, attr name, or end of tag, but got %q", s
[i
:]),
109 attrName
:= strings
.ToLower(string(s
[i
:j
]))
110 if c
.element
== elementScript
&& attrName
== "type" {
111 attr
= attrScriptType
113 switch attrType(attrName
) {
124 state
= stateAttrName
126 state
= stateAfterName
128 return context
{state
: state
, element
: c
.element
, attr
: attr
}, j
131 // tAttrName is the context transition function for stateAttrName.
132 func tAttrName(c context
, s
[]byte) (context
, int) {
133 i
, err
:= eatAttrName(s
, 0)
135 return context
{state
: stateError
, err
: err
}, len(s
)
136 } else if i
!= len(s
) {
137 c
.state
= stateAfterName
142 // tAfterName is the context transition function for stateAfterName.
143 func tAfterName(c context
, s
[]byte) (context
, int) {
144 // Look for the start of the value.
145 i
:= eatWhiteSpace(s
, 0)
148 } else if s
[i
] != '=' {
149 // Occurs due to tag ending '>', and valueless attribute.
153 c
.state
= stateBeforeValue
158 var attrStartStates
= [...]state
{
161 attrScriptType
: stateAttr
,
166 // tBeforeValue is the context transition function for stateBeforeValue.
167 func tBeforeValue(c context
, s
[]byte) (context
, int) {
168 i
:= eatWhiteSpace(s
, 0)
172 // Find the attribute delimiter.
173 delim
:= delimSpaceOrTagEnd
176 delim
, i
= delimSingleQuote
, i
+1
178 delim
, i
= delimDoubleQuote
, i
+1
180 c
.state
, c
.delim
= attrStartStates
[c
.attr
], delim
184 // tHTMLCmt is the context transition function for stateHTMLCmt.
185 func tHTMLCmt(c context
, s
[]byte) (context
, int) {
186 if i
:= bytes
.Index(s
, commentEnd
); i
!= -1 {
187 return context
{}, i
+ 3
192 // specialTagEndMarkers maps element types to the character sequence that
193 // case-insensitively signals the end of the special tag body.
194 var specialTagEndMarkers
= [...][]byte{
195 elementScript
: []byte("script"),
196 elementStyle
: []byte("style"),
197 elementTextarea
: []byte("textarea"),
198 elementTitle
: []byte("title"),
202 specialTagEndPrefix
= []byte("</")
203 tagEndSeparators
= []byte("> \t\n\f/")
206 // tSpecialTagEnd is the context transition function for raw text and RCDATA
208 func tSpecialTagEnd(c context
, s
[]byte) (context
, int) {
209 if c
.element
!= elementNone
{
210 if i
:= indexTagEnd(s
, specialTagEndMarkers
[c
.element
]); i
!= -1 {
217 // indexTagEnd finds the index of a special tag end in a case insensitive way, or returns -1
218 func indexTagEnd(s
[]byte, tag
[]byte) int {
220 plen
:= len(specialTagEndPrefix
)
222 // Try to find the tag end prefix first
223 i
:= bytes
.Index(s
, specialTagEndPrefix
)
228 // Try to match the actual tag if there is still space for it
229 if len(tag
) <= len(s
) && bytes
.EqualFold(tag
, s
[:len(tag
)]) {
231 // Check the tag is followed by a proper separator
232 if len(s
) > 0 && bytes
.IndexByte(tagEndSeparators
, s
[0]) != -1 {
242 // tAttr is the context transition function for the attribute state.
243 func tAttr(c context
, s
[]byte) (context
, int) {
247 // tURL is the context transition function for the URL state.
248 func tURL(c context
, s
[]byte) (context
, int) {
249 if bytes
.ContainsAny(s
, "#?") {
250 c
.urlPart
= urlPartQueryOrFrag
251 } else if len(s
) != eatWhiteSpace(s
, 0) && c
.urlPart
== urlPartNone
{
252 // HTML5 uses "Valid URL potentially surrounded by spaces" for
253 // attrs: http://www.w3.org/TR/html5/index.html#attributes-1
254 c
.urlPart
= urlPartPreQuery
259 // tJS is the context transition function for the JS state.
260 func tJS(c context
, s
[]byte) (context
, int) {
261 i
:= bytes
.IndexAny(s
, `"'/`)
263 // Entire input is non string, comment, regexp tokens.
264 c
.jsCtx
= nextJSCtx(s
, c
.jsCtx
)
267 c
.jsCtx
= nextJSCtx(s
[:i
], c
.jsCtx
)
270 c
.state
, c
.jsCtx
= stateJSDqStr
, jsCtxRegexp
272 c
.state
, c
.jsCtx
= stateJSSqStr
, jsCtxRegexp
275 case i
+1 < len(s
) && s
[i
+1] == '/':
276 c
.state
, i
= stateJSLineCmt
, i
+1
277 case i
+1 < len(s
) && s
[i
+1] == '*':
278 c
.state
, i
= stateJSBlockCmt
, i
+1
279 case c
.jsCtx
== jsCtxRegexp
:
280 c
.state
= stateJSRegexp
281 case c
.jsCtx
== jsCtxDivOp
:
282 c
.jsCtx
= jsCtxRegexp
286 err
: errorf(ErrSlashAmbig
, nil, 0, "'/' could start a division or regexp: %.32q", s
[i
:]),
295 // tJSDelimited is the context transition function for the JS string and regexp
297 func tJSDelimited(c context
, s
[]byte) (context
, int) {
306 k
, inCharset
:= 0, false
308 i
:= k
+ bytes
.IndexAny(s
[k
:], specials
)
318 err
: errorf(ErrPartialEscape
, nil, 0, "unfinished escape sequence in JS string: %q", s
),
328 c
.state
, c
.jsCtx
= stateJS
, jsCtxDivOp
336 // This can be fixed by making context richer if interpolation
337 // into charsets is desired.
340 err
: errorf(ErrPartialCharset
, nil, 0, "unfinished JS regexp charset: %q", s
),
347 var blockCommentEnd
= []byte("*/")
349 // tBlockCmt is the context transition function for /*comment*/ states.
350 func tBlockCmt(c context
, s
[]byte) (context
, int) {
351 i
:= bytes
.Index(s
, blockCommentEnd
)
356 case stateJSBlockCmt
:
358 case stateCSSBlockCmt
:
361 panic(c
.state
.String())
366 // tLineCmt is the context transition function for //comment states.
367 func tLineCmt(c context
, s
[]byte) (context
, int) {
368 var lineTerminators
string
372 lineTerminators
, endState
= "\n\r\u2028\u2029", stateJS
373 case stateCSSLineCmt
:
374 lineTerminators
, endState
= "\n\f\r", stateCSS
375 // Line comments are not part of any published CSS standard but
376 // are supported by the 4 major browsers.
377 // This defines line comments as
378 // LINECOMMENT ::= "//" [^\n\f\d]*
379 // since http://www.w3.org/TR/css3-syntax/#SUBTOK-nl defines
381 // nl ::= #xA | #xD #xA | #xD | #xC
383 panic(c
.state
.String())
386 i
:= bytes
.IndexAny(s
, lineTerminators
)
391 // Per section 7.4 of EcmaScript 5 : http://es5.github.com/#x7.4
392 // "However, the LineTerminator at the end of the line is not
393 // considered to be part of the single-line comment; it is
394 // recognized separately by the lexical grammar and becomes part
395 // of the stream of input elements for the syntactic grammar."
399 // tCSS is the context transition function for the CSS state.
400 func tCSS(c context
, s
[]byte) (context
, int) {
401 // CSS quoted strings are almost never used except for:
402 // (1) URLs as in background: "/foo.png"
403 // (2) Multiword font-names as in font-family: "Times New Roman"
404 // (3) List separators in content values as in inline-lists:
406 // ul.inlineList { list-style: none; padding:0 }
407 // ul.inlineList > li { display: inline }
408 // ul.inlineList > li:before { content: ", " }
409 // ul.inlineList > li:first-child:before { content: "" }
411 // <ul class=inlineList><li>One<li>Two<li>Three</ul>
412 // (4) Attribute value selectors as in a[href="http://example.com/"]
414 // We conservatively treat all strings as URLs, but make some
415 // allowances to avoid confusion.
417 // In (1), our conservative assumption is justified.
418 // In (2), valid font names do not contain ':', '?', or '#', so our
419 // conservative assumption is fine since we will never transition past
421 // In (3), our protocol heuristic should not be tripped, and there
422 // should not be non-space content after a '?' or '#', so as long as
423 // we only %-encode RFC 3986 reserved characters we are ok.
424 // In (4), we should URL escape for URL attributes, and for others we
425 // have the attribute name available if our conservative assumption
426 // proves problematic for real code.
430 i
:= k
+ bytes
.IndexAny(s
[k
:], `("'/`)
436 // Look for url to the left.
437 p
:= bytes
.TrimRight(s
[:i
], "\t\n\f\r ")
438 if endsWithCSSKeyword(p
, "url") {
439 j
:= len(s
) - len(bytes
.TrimLeft(s
[i
+1:], "\t\n\f\r "))
441 case j
!= len(s
) && s
[j
] == '"':
442 c
.state
, j
= stateCSSDqURL
, j
+1
443 case j
!= len(s
) && s
[j
] == '\'':
444 c
.state
, j
= stateCSSSqURL
, j
+1
446 c
.state
= stateCSSURL
454 c
.state
= stateCSSLineCmt
457 c
.state
= stateCSSBlockCmt
462 c
.state
= stateCSSDqStr
465 c
.state
= stateCSSSqStr
472 // tCSSStr is the context transition function for the CSS string and URL states.
473 func tCSSStr(c context
, s
[]byte) (context
, int) {
476 case stateCSSDqStr
, stateCSSDqURL
:
478 case stateCSSSqStr
, stateCSSSqURL
:
481 // Unquoted URLs end with a newline or close parenthesis.
482 // The below includes the wc (whitespace character) and nl.
483 endAndEsc
= "\\\t\n\f\r )"
485 panic(c
.state
.String())
490 i
:= k
+ bytes
.IndexAny(s
[k
:], endAndEsc
)
492 c
, nread
:= tURL(c
, decodeCSS(s
[k
:]))
500 err
: errorf(ErrPartialEscape
, nil, 0, "unfinished escape sequence in CSS string: %q", s
),
507 c
, _
= tURL(c
, decodeCSS(s
[:i
+1]))
512 // tError is the context transition function for the error state.
513 func tError(c context
, s
[]byte) (context
, int) {
517 // eatAttrName returns the largest j such that s[i:j] is an attribute name.
518 // It returns an error if s[i:] does not look like it begins with an
519 // attribute name, such as encountering a quote mark without a preceding
521 func eatAttrName(s
[]byte, i
int) (int, *Error
) {
522 for j
:= i
; j
< len(s
); j
++ {
524 case ' ', '\t', '\n', '\f', '\r', '=', '>':
527 // These result in a parse warning in HTML5 and are
528 // indicative of serious problems if seen in an attr
529 // name in a template.
530 return -1, errorf(ErrBadHTML
, nil, 0, "%q in attribute name: %.32q", s
[j
:j
+1], s
)
538 var elementNameMap
= map[string]element
{
539 "script": elementScript
,
540 "style": elementStyle
,
541 "textarea": elementTextarea
,
542 "title": elementTitle
,
545 // asciiAlpha reports whether c is an ASCII letter.
546 func asciiAlpha(c
byte) bool {
547 return 'A' <= c
&& c
<= 'Z' ||
'a' <= c
&& c
<= 'z'
550 // asciiAlphaNum reports whether c is an ASCII letter or digit.
551 func asciiAlphaNum(c
byte) bool {
552 return asciiAlpha(c
) ||
'0' <= c
&& c
<= '9'
555 // eatTagName returns the largest j such that s[i:j] is a tag name and the tag type.
556 func eatTagName(s
[]byte, i
int) (int, element
) {
557 if i
== len(s
) ||
!asciiAlpha(s
[i
]) {
558 return i
, elementNone
563 if asciiAlphaNum(x
) {
567 // Allow "x-y" or "x:y" but not "x-", "-y", or "x--y".
568 if (x
== ':' || x
== '-') && j
+1 < len(s
) && asciiAlphaNum(s
[j
+1]) {
574 return j
, elementNameMap
[strings
.ToLower(string(s
[i
:j
]))]
577 // eatWhiteSpace returns the largest j such that s[i:j] is white space.
578 func eatWhiteSpace(s
[]byte, i
int) int {
579 for j
:= i
; j
< len(s
); j
++ {
581 case ' ', '\t', '\n', '\f', '\r':