1
2
3
4
5 package template
6
7 import (
8 "bytes"
9 "strings"
10 )
11
12
13
14
15
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 stateMetaContent: tMetaContent,
27 stateMetaContentURL: tMetaContentURL,
28 stateSrcset: tURL,
29 stateJS: tJS,
30 stateJSDqStr: tJSDelimited,
31 stateJSSqStr: tJSDelimited,
32 stateJSRegexp: tJSDelimited,
33 stateJSTmplLit: tJSTmpl,
34 stateJSBlockCmt: tBlockCmt,
35 stateJSLineCmt: tLineCmt,
36 stateJSHTMLOpenCmt: tLineCmt,
37 stateJSHTMLCloseCmt: tLineCmt,
38 stateCSS: tCSS,
39 stateCSSDqStr: tCSSStr,
40 stateCSSSqStr: tCSSStr,
41 stateCSSDqURL: tCSSStr,
42 stateCSSSqURL: tCSSStr,
43 stateCSSURL: tCSSStr,
44 stateCSSBlockCmt: tBlockCmt,
45 stateCSSLineCmt: tLineCmt,
46 stateError: tError,
47 }
48
49 var commentStart = []byte("<!--")
50 var commentEnd = []byte("-->")
51
52
53 func tText(c context, s []byte) (context, int) {
54 k := 0
55 for {
56 i := k + bytes.IndexByte(s[k:], '<')
57 if i < k || i+1 == len(s) {
58 return c, len(s)
59 } else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) {
60 return context{state: stateHTMLCmt}, i + 4
61 }
62 i++
63 end := false
64 if s[i] == '/' {
65 if i+1 == len(s) {
66 return c, len(s)
67 }
68 end, i = true, i+1
69 }
70 j, e := eatTagName(s, i)
71 if j != i {
72 if end {
73 e = elementNone
74 }
75
76 return context{state: stateTag, element: e}, j
77 }
78 k = j
79 }
80 }
81
82 var elementContentType = [...]state{
83 elementNone: stateText,
84 elementScript: stateJS,
85 elementStyle: stateCSS,
86 elementTextarea: stateRCDATA,
87 elementTitle: stateRCDATA,
88 elementMeta: stateText,
89 }
90
91
92 func tTag(c context, s []byte) (context, int) {
93
94 i := eatWhiteSpace(s, 0)
95 if i == len(s) {
96 return c, len(s)
97 }
98 if s[i] == '>' {
99
100
101 if c.element == elementMeta {
102 return context{state: stateText, element: elementNone}, i + 1
103 }
104 return context{
105 state: elementContentType[c.element],
106 element: c.element,
107 }, i + 1
108 }
109 j, err := eatAttrName(s, i)
110 if err != nil {
111 return context{state: stateError, err: err}, len(s)
112 }
113 state, attr := stateTag, attrNone
114 if i == j {
115 return context{
116 state: stateError,
117 err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]),
118 }, len(s)
119 }
120
121 attrName := strings.ToLower(string(s[i:j]))
122 if c.element == elementScript && attrName == "type" {
123 attr = attrScriptType
124 } else if c.element == elementMeta && attrName == "content" {
125 attr = attrMetaContent
126 } else {
127 switch attrType(attrName) {
128 case contentTypeURL:
129 attr = attrURL
130 case contentTypeCSS:
131 attr = attrStyle
132 case contentTypeJS:
133 attr = attrScript
134 case contentTypeSrcset:
135 attr = attrSrcset
136 }
137 }
138
139 if j == len(s) {
140 state = stateAttrName
141 } else {
142 state = stateAfterName
143 }
144 return context{state: state, element: c.element, attr: attr}, j
145 }
146
147
148 func tAttrName(c context, s []byte) (context, int) {
149 i, err := eatAttrName(s, 0)
150 if err != nil {
151 return context{state: stateError, err: err}, len(s)
152 } else if i != len(s) {
153 c.state = stateAfterName
154 }
155 return c, i
156 }
157
158
159 func tAfterName(c context, s []byte) (context, int) {
160
161 i := eatWhiteSpace(s, 0)
162 if i == len(s) {
163 return c, len(s)
164 } else if s[i] != '=' {
165
166 c.state = stateTag
167 return c, i
168 }
169 c.state = stateBeforeValue
170
171 return c, i + 1
172 }
173
174 var attrStartStates = [...]state{
175 attrNone: stateAttr,
176 attrScript: stateJS,
177 attrScriptType: stateAttr,
178 attrStyle: stateCSS,
179 attrURL: stateURL,
180 attrSrcset: stateSrcset,
181 attrMetaContent: stateMetaContent,
182 }
183
184
185 func tBeforeValue(c context, s []byte) (context, int) {
186 i := eatWhiteSpace(s, 0)
187 if i == len(s) {
188 return c, len(s)
189 }
190
191 delim := delimSpaceOrTagEnd
192 switch s[i] {
193 case '\'':
194 delim, i = delimSingleQuote, i+1
195 case '"':
196 delim, i = delimDoubleQuote, i+1
197 }
198 c.state, c.delim = attrStartStates[c.attr], delim
199 return c, i
200 }
201
202
203 func tHTMLCmt(c context, s []byte) (context, int) {
204 if i := bytes.Index(s, commentEnd); i != -1 {
205 return context{}, i + 3
206 }
207 return c, len(s)
208 }
209
210
211
212 var specialTagEndMarkers = [...][]byte{
213 elementScript: []byte("script"),
214 elementStyle: []byte("style"),
215 elementTextarea: []byte("textarea"),
216 elementTitle: []byte("title"),
217 elementMeta: []byte(""),
218 }
219
220 var (
221 specialTagEndPrefix = []byte("</")
222 tagEndSeparators = []byte("> \t\n\f/")
223 )
224
225
226
227 func tSpecialTagEnd(c context, s []byte) (context, int) {
228 if c.element != elementNone {
229
230
231 if c.element == elementScript && (isInScriptLiteral(c.state) || isComment(c.state)) {
232 return c, len(s)
233 }
234 if i := indexTagEnd(s, specialTagEndMarkers[c.element]); i != -1 {
235 return context{}, i
236 }
237 }
238 return c, len(s)
239 }
240
241
242 func indexTagEnd(s []byte, tag []byte) int {
243 res := 0
244 plen := len(specialTagEndPrefix)
245 for len(s) > 0 {
246
247 i := bytes.Index(s, specialTagEndPrefix)
248 if i == -1 {
249 return i
250 }
251 s = s[i+plen:]
252
253 if len(tag) <= len(s) && bytes.EqualFold(tag, s[:len(tag)]) {
254 s = s[len(tag):]
255
256 if len(s) > 0 && bytes.IndexByte(tagEndSeparators, s[0]) != -1 {
257 return res + i
258 }
259 res += len(tag)
260 }
261 res += i + plen
262 }
263 return -1
264 }
265
266
267 func tAttr(c context, s []byte) (context, int) {
268 return c, len(s)
269 }
270
271
272 func tURL(c context, s []byte) (context, int) {
273 if bytes.ContainsAny(s, "#?") {
274 c.urlPart = urlPartQueryOrFrag
275 } else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone {
276
277
278 c.urlPart = urlPartPreQuery
279 }
280 return c, len(s)
281 }
282
283
284 func tJS(c context, s []byte) (context, int) {
285 i := bytes.IndexAny(s, "\"`'/{}<-#")
286 if i == -1 {
287
288 c.jsCtx = nextJSCtx(s, c.jsCtx)
289 return c, len(s)
290 }
291 c.jsCtx = nextJSCtx(s[:i], c.jsCtx)
292 switch s[i] {
293 case '"':
294 c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
295 case '\'':
296 c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
297 case '`':
298 c.state, c.jsCtx = stateJSTmplLit, jsCtxRegexp
299 case '/':
300 switch {
301 case i+1 < len(s) && s[i+1] == '/':
302 c.state, i = stateJSLineCmt, i+1
303 case i+1 < len(s) && s[i+1] == '*':
304 c.state, i = stateJSBlockCmt, i+1
305 case c.jsCtx == jsCtxRegexp:
306 c.state = stateJSRegexp
307 case c.jsCtx == jsCtxDivOp:
308 c.jsCtx = jsCtxRegexp
309 default:
310 return context{
311 state: stateError,
312 err: errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]),
313 }, len(s)
314 }
315
316
317
318
319
320
321
322 case '<':
323 if i+3 < len(s) && bytes.Equal(commentStart, s[i:i+4]) {
324 c.state, i = stateJSHTMLOpenCmt, i+3
325 }
326 case '-':
327 if i+2 < len(s) && bytes.Equal(commentEnd, s[i:i+3]) {
328 c.state, i = stateJSHTMLCloseCmt, i+2
329 }
330
331 case '#':
332 if i+1 < len(s) && s[i+1] == '!' {
333 c.state, i = stateJSLineCmt, i+1
334 break
335 }
336
337
338 for i+1 < len(s) && isJSIdentPart(rune(s[i+1])) {
339 i++
340 }
341 c.jsCtx = jsCtxDivOp
342 case '{':
343
344
345 if len(c.jsBraceDepth) == 0 {
346 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
347 return c, i + 1
348 }
349 c.jsBraceDepth[len(c.jsBraceDepth)-1]++
350 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
351 case '}':
352 if len(c.jsBraceDepth) == 0 {
353 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
354 return c, i + 1
355 }
356
357
358
359
360 c.jsBraceDepth[len(c.jsBraceDepth)-1]--
361 if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
362 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
363 return c, i + 1
364 }
365 c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
366 c.state = stateJSTmplLit
367 default:
368 panic("unreachable")
369 }
370 return c, i + 1
371 }
372
373 func tJSTmpl(c context, s []byte) (context, int) {
374 var k int
375 for {
376 i := k + bytes.IndexAny(s[k:], "`\\$")
377 if i < k {
378 break
379 }
380 switch s[i] {
381 case '\\':
382 i++
383 if i == len(s) {
384 return context{
385 state: stateError,
386 err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
387 }, len(s)
388 }
389 case '$':
390 if len(s) >= i+2 && s[i+1] == '{' {
391 c.jsBraceDepth = append(c.jsBraceDepth, 0)
392 c.state, c.jsCtx = stateJS, jsCtxRegexp
393 return c, i + 2
394 }
395 case '`':
396
397 c.state = stateJS
398 return c, i + 1
399 }
400 k = i + 1
401 }
402
403 return c, len(s)
404 }
405
406
407
408 func tJSDelimited(c context, s []byte) (context, int) {
409 specials := `\"`
410 switch c.state {
411 case stateJSSqStr:
412 specials = `\'`
413 case stateJSRegexp:
414 specials = `\/[]`
415 }
416
417 k, inCharset := 0, false
418 for {
419 i := k + bytes.IndexAny(s[k:], specials)
420 if i < k {
421 break
422 }
423 switch s[i] {
424 case '\\':
425 i++
426 if i == len(s) {
427 return context{
428 state: stateError,
429 err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
430 }, len(s)
431 }
432 case '[':
433 inCharset = true
434 case ']':
435 inCharset = false
436 case '/':
437
438
439
440 if i > 0 && i+7 <= len(s) && bytes.EqualFold(s[i-1:i+7], []byte("</script")) {
441 i++
442 } else if !inCharset {
443 c.state, c.jsCtx = stateJS, jsCtxDivOp
444 return c, i + 1
445 }
446 default:
447
448 if !inCharset {
449 c.state, c.jsCtx = stateJS, jsCtxDivOp
450 return c, i + 1
451 }
452 }
453 k = i + 1
454 }
455
456 if inCharset {
457
458
459 return context{
460 state: stateError,
461 err: errorf(ErrPartialCharset, nil, 0, "unfinished JS regexp charset: %q", s),
462 }, len(s)
463 }
464
465 return c, len(s)
466 }
467
468 var blockCommentEnd = []byte("*/")
469
470
471 func tBlockCmt(c context, s []byte) (context, int) {
472 i := bytes.Index(s, blockCommentEnd)
473 if i == -1 {
474 return c, len(s)
475 }
476 switch c.state {
477 case stateJSBlockCmt:
478 c.state = stateJS
479 case stateCSSBlockCmt:
480 c.state = stateCSS
481 default:
482 panic(c.state.String())
483 }
484 return c, i + 2
485 }
486
487
488 func tLineCmt(c context, s []byte) (context, int) {
489 var lineTerminators string
490 var endState state
491 switch c.state {
492 case stateJSLineCmt, stateJSHTMLOpenCmt, stateJSHTMLCloseCmt:
493 lineTerminators, endState = "\n\r\u2028\u2029", stateJS
494 case stateCSSLineCmt:
495 lineTerminators, endState = "\n\f\r", stateCSS
496
497
498
499
500
501
502
503 default:
504 panic(c.state.String())
505 }
506
507 i := bytes.IndexAny(s, lineTerminators)
508 if i == -1 {
509 return c, len(s)
510 }
511 c.state = endState
512
513
514
515
516
517 return c, i
518 }
519
520
521 func tCSS(c context, s []byte) (context, int) {
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549 k := 0
550 for {
551 i := k + bytes.IndexAny(s[k:], `("'/`)
552 if i < k {
553 return c, len(s)
554 }
555 switch s[i] {
556 case '(':
557
558 p := bytes.TrimRight(s[:i], "\t\n\f\r ")
559 if endsWithCSSKeyword(p, "url") {
560 j := len(s) - len(bytes.TrimLeft(s[i+1:], "\t\n\f\r "))
561 switch {
562 case j != len(s) && s[j] == '"':
563 c.state, j = stateCSSDqURL, j+1
564 case j != len(s) && s[j] == '\'':
565 c.state, j = stateCSSSqURL, j+1
566 default:
567 c.state = stateCSSURL
568 }
569 return c, j
570 }
571 case '/':
572 if i+1 < len(s) {
573 switch s[i+1] {
574 case '/':
575 c.state = stateCSSLineCmt
576 return c, i + 2
577 case '*':
578 c.state = stateCSSBlockCmt
579 return c, i + 2
580 }
581 }
582 case '"':
583 c.state = stateCSSDqStr
584 return c, i + 1
585 case '\'':
586 c.state = stateCSSSqStr
587 return c, i + 1
588 }
589 k = i + 1
590 }
591 }
592
593
594 func tCSSStr(c context, s []byte) (context, int) {
595 var endAndEsc string
596 switch c.state {
597 case stateCSSDqStr, stateCSSDqURL:
598 endAndEsc = `\"`
599 case stateCSSSqStr, stateCSSSqURL:
600 endAndEsc = `\'`
601 case stateCSSURL:
602
603
604 endAndEsc = "\\\t\n\f\r )"
605 default:
606 panic(c.state.String())
607 }
608
609 k := 0
610 for {
611 i := k + bytes.IndexAny(s[k:], endAndEsc)
612 if i < k {
613 c, nread := tURL(c, decodeCSS(s[k:]))
614 return c, k + nread
615 }
616 if s[i] == '\\' {
617 i++
618 if i == len(s) {
619 return context{
620 state: stateError,
621 err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in CSS string: %q", s),
622 }, len(s)
623 }
624 } else {
625 c.state = stateCSS
626 return c, i + 1
627 }
628 c, _ = tURL(c, decodeCSS(s[:i+1]))
629 k = i + 1
630 }
631 }
632
633
634 func tError(c context, s []byte) (context, int) {
635 return c, len(s)
636 }
637
638
639 func tMetaContent(c context, s []byte) (context, int) {
640 for i := range len(s) {
641 if i+3 <= len(s)-1 && bytes.EqualFold(s[i:i+3], []byte("url")) {
642 if j := eatWhiteSpace(s, i+3); j < len(s) && s[j] == '=' {
643 c.state = stateMetaContentURL
644 return c, j + 1
645 }
646 }
647 }
648 return c, len(s)
649 }
650
651
652 func tMetaContentURL(c context, s []byte) (context, int) {
653 for i := range len(s) {
654 if s[i] == ';' {
655 c.state = stateMetaContent
656 return c, i + 1
657 }
658 }
659 return c, len(s)
660 }
661
662
663
664
665
666 func eatAttrName(s []byte, i int) (int, *Error) {
667 for j := i; j < len(s); j++ {
668 switch s[j] {
669 case ' ', '\t', '\n', '\f', '\r', '=', '>':
670 return j, nil
671 case '\'', '"', '<':
672
673
674
675 return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s)
676 default:
677
678 }
679 }
680 return len(s), nil
681 }
682
683 var elementNameMap = map[string]element{
684 "script": elementScript,
685 "style": elementStyle,
686 "textarea": elementTextarea,
687 "title": elementTitle,
688 "meta": elementMeta,
689 }
690
691
692 func asciiAlpha(c byte) bool {
693 return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
694 }
695
696
697 func asciiAlphaNum(c byte) bool {
698 return asciiAlpha(c) || '0' <= c && c <= '9'
699 }
700
701
702 func eatTagName(s []byte, i int) (int, element) {
703 if i == len(s) || !asciiAlpha(s[i]) {
704 return i, elementNone
705 }
706 j := i + 1
707 for j < len(s) {
708 x := s[j]
709 if asciiAlphaNum(x) {
710 j++
711 continue
712 }
713
714 if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) {
715 j += 2
716 continue
717 }
718 break
719 }
720 return j, elementNameMap[strings.ToLower(string(s[i:j]))]
721 }
722
723
724 func eatWhiteSpace(s []byte, i int) int {
725 for j := i; j < len(s); j++ {
726 switch s[j] {
727 case ' ', '\t', '\n', '\f', '\r':
728
729 default:
730 return j
731 }
732 }
733 return len(s)
734 }
735
View as plain text