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 }
335 case '{':
336
337
338 if len(c.jsBraceDepth) == 0 {
339 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
340 return c, i + 1
341 }
342 c.jsBraceDepth[len(c.jsBraceDepth)-1]++
343 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
344 case '}':
345 if len(c.jsBraceDepth) == 0 {
346 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
347 return c, i + 1
348 }
349
350
351
352
353 c.jsBraceDepth[len(c.jsBraceDepth)-1]--
354 if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
355 c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx)
356 return c, i + 1
357 }
358 c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
359 c.state = stateJSTmplLit
360 default:
361 panic("unreachable")
362 }
363 return c, i + 1
364 }
365
366 func tJSTmpl(c context, s []byte) (context, int) {
367 var k int
368 for {
369 i := k + bytes.IndexAny(s[k:], "`\\$")
370 if i < k {
371 break
372 }
373 switch s[i] {
374 case '\\':
375 i++
376 if i == len(s) {
377 return context{
378 state: stateError,
379 err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
380 }, len(s)
381 }
382 case '$':
383 if len(s) >= i+2 && s[i+1] == '{' {
384 c.jsBraceDepth = append(c.jsBraceDepth, 0)
385 c.state = stateJS
386 return c, i + 2
387 }
388 case '`':
389
390 c.state = stateJS
391 return c, i + 1
392 }
393 k = i + 1
394 }
395
396 return c, len(s)
397 }
398
399
400
401 func tJSDelimited(c context, s []byte) (context, int) {
402 specials := `\"`
403 switch c.state {
404 case stateJSSqStr:
405 specials = `\'`
406 case stateJSRegexp:
407 specials = `\/[]`
408 }
409
410 k, inCharset := 0, false
411 for {
412 i := k + bytes.IndexAny(s[k:], specials)
413 if i < k {
414 break
415 }
416 switch s[i] {
417 case '\\':
418 i++
419 if i == len(s) {
420 return context{
421 state: stateError,
422 err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
423 }, len(s)
424 }
425 case '[':
426 inCharset = true
427 case ']':
428 inCharset = false
429 case '/':
430
431
432
433 if i > 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
434 i++
435 } else if !inCharset {
436 c.state, c.jsCtx = stateJS, jsCtxDivOp
437 return c, i + 1
438 }
439 default:
440
441 if !inCharset {
442 c.state, c.jsCtx = stateJS, jsCtxDivOp
443 return c, i + 1
444 }
445 }
446 k = i + 1
447 }
448
449 if inCharset {
450
451
452 return context{
453 state: stateError,
454 err: errorf(ErrPartialCharset, nil, 0, "unfinished JS regexp charset: %q", s),
455 }, len(s)
456 }
457
458 return c, len(s)
459 }
460
461 var blockCommentEnd = []byte("*/")
462
463
464 func tBlockCmt(c context, s []byte) (context, int) {
465 i := bytes.Index(s, blockCommentEnd)
466 if i == -1 {
467 return c, len(s)
468 }
469 switch c.state {
470 case stateJSBlockCmt:
471 c.state = stateJS
472 case stateCSSBlockCmt:
473 c.state = stateCSS
474 default:
475 panic(c.state.String())
476 }
477 return c, i + 2
478 }
479
480
481 func tLineCmt(c context, s []byte) (context, int) {
482 var lineTerminators string
483 var endState state
484 switch c.state {
485 case stateJSLineCmt, stateJSHTMLOpenCmt, stateJSHTMLCloseCmt:
486 lineTerminators, endState = "\n\r\u2028\u2029", stateJS
487 case stateCSSLineCmt:
488 lineTerminators, endState = "\n\f\r", stateCSS
489
490
491
492
493
494
495
496 default:
497 panic(c.state.String())
498 }
499
500 i := bytes.IndexAny(s, lineTerminators)
501 if i == -1 {
502 return c, len(s)
503 }
504 c.state = endState
505
506
507
508
509
510 return c, i
511 }
512
513
514 func tCSS(c context, s []byte) (context, int) {
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542 k := 0
543 for {
544 i := k + bytes.IndexAny(s[k:], `("'/`)
545 if i < k {
546 return c, len(s)
547 }
548 switch s[i] {
549 case '(':
550
551 p := bytes.TrimRight(s[:i], "\t\n\f\r ")
552 if endsWithCSSKeyword(p, "url") {
553 j := len(s) - len(bytes.TrimLeft(s[i+1:], "\t\n\f\r "))
554 switch {
555 case j != len(s) && s[j] == '"':
556 c.state, j = stateCSSDqURL, j+1
557 case j != len(s) && s[j] == '\'':
558 c.state, j = stateCSSSqURL, j+1
559 default:
560 c.state = stateCSSURL
561 }
562 return c, j
563 }
564 case '/':
565 if i+1 < len(s) {
566 switch s[i+1] {
567 case '/':
568 c.state = stateCSSLineCmt
569 return c, i + 2
570 case '*':
571 c.state = stateCSSBlockCmt
572 return c, i + 2
573 }
574 }
575 case '"':
576 c.state = stateCSSDqStr
577 return c, i + 1
578 case '\'':
579 c.state = stateCSSSqStr
580 return c, i + 1
581 }
582 k = i + 1
583 }
584 }
585
586
587 func tCSSStr(c context, s []byte) (context, int) {
588 var endAndEsc string
589 switch c.state {
590 case stateCSSDqStr, stateCSSDqURL:
591 endAndEsc = `\"`
592 case stateCSSSqStr, stateCSSSqURL:
593 endAndEsc = `\'`
594 case stateCSSURL:
595
596
597 endAndEsc = "\\\t\n\f\r )"
598 default:
599 panic(c.state.String())
600 }
601
602 k := 0
603 for {
604 i := k + bytes.IndexAny(s[k:], endAndEsc)
605 if i < k {
606 c, nread := tURL(c, decodeCSS(s[k:]))
607 return c, k + nread
608 }
609 if s[i] == '\\' {
610 i++
611 if i == len(s) {
612 return context{
613 state: stateError,
614 err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in CSS string: %q", s),
615 }, len(s)
616 }
617 } else {
618 c.state = stateCSS
619 return c, i + 1
620 }
621 c, _ = tURL(c, decodeCSS(s[:i+1]))
622 k = i + 1
623 }
624 }
625
626
627 func tError(c context, s []byte) (context, int) {
628 return c, len(s)
629 }
630
631
632 func tMetaContent(c context, s []byte) (context, int) {
633 for i := range len(s) {
634 if i+3 <= len(s)-1 && bytes.EqualFold(s[i:i+3], []byte("url")) {
635 if j := eatWhiteSpace(s, i+3); j < len(s) && s[j] == '=' {
636 c.state = stateMetaContentURL
637 return c, j + 1
638 }
639 }
640 }
641 return c, len(s)
642 }
643
644
645 func tMetaContentURL(c context, s []byte) (context, int) {
646 for i := range len(s) {
647 if s[i] == ';' {
648 c.state = stateMetaContent
649 return c, i + 1
650 }
651 }
652 return c, len(s)
653 }
654
655
656
657
658
659 func eatAttrName(s []byte, i int) (int, *Error) {
660 for j := i; j < len(s); j++ {
661 switch s[j] {
662 case ' ', '\t', '\n', '\f', '\r', '=', '>':
663 return j, nil
664 case '\'', '"', '<':
665
666
667
668 return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s)
669 default:
670
671 }
672 }
673 return len(s), nil
674 }
675
676 var elementNameMap = map[string]element{
677 "script": elementScript,
678 "style": elementStyle,
679 "textarea": elementTextarea,
680 "title": elementTitle,
681 "meta": elementMeta,
682 }
683
684
685 func asciiAlpha(c byte) bool {
686 return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
687 }
688
689
690 func asciiAlphaNum(c byte) bool {
691 return asciiAlpha(c) || '0' <= c && c <= '9'
692 }
693
694
695 func eatTagName(s []byte, i int) (int, element) {
696 if i == len(s) || !asciiAlpha(s[i]) {
697 return i, elementNone
698 }
699 j := i + 1
700 for j < len(s) {
701 x := s[j]
702 if asciiAlphaNum(x) {
703 j++
704 continue
705 }
706
707 if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) {
708 j += 2
709 continue
710 }
711 break
712 }
713 return j, elementNameMap[strings.ToLower(string(s[i:j]))]
714 }
715
716
717 func eatWhiteSpace(s []byte, i int) int {
718 for j := i; j < len(s); j++ {
719 switch s[j] {
720 case ' ', '\t', '\n', '\f', '\r':
721
722 default:
723 return j
724 }
725 }
726 return len(s)
727 }
728
View as plain text