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 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
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
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
86 func tTag(c context, s []byte) (context, int) {
87
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
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
146 func tAfterName(c context, s []byte) (context, int) {
147
148 i := eatWhiteSpace(s, 0)
149 if i == len(s) {
150 return c, len(s)
151 } else if s[i] != '=' {
152
153 c.state = stateTag
154 return c, i
155 }
156 c.state = stateBeforeValue
157
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
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
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
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
197
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
211
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
222 func indexTagEnd(s []byte, tag []byte) int {
223 res := 0
224 plen := len(specialTagEndPrefix)
225 for len(s) > 0 {
226
227 i := bytes.Index(s, specialTagEndPrefix)
228 if i == -1 {
229 return i
230 }
231 s = s[i+plen:]
232
233 if len(tag) <= len(s) && bytes.EqualFold(tag, s[:len(tag)]) {
234 s = s[len(tag):]
235
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
247 func tAttr(c context, s []byte) (context, int) {
248 return c, len(s)
249 }
250
251
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
257
258 c.urlPart = urlPartPreQuery
259 }
260 return c, len(s)
261 }
262
263
264 func tJS(c context, s []byte) (context, int) {
265 i := bytes.IndexAny(s, `"'/`)
266 if i == -1 {
267
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
300
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
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
341
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
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
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
380
381
382
383
384
385
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
396
397
398
399
400 return c, i
401 }
402
403
404 func tCSS(c context, s []byte) (context, int) {
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
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
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
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
486
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
517 func tError(c context, s []byte) (context, int) {
518 return c, len(s)
519 }
520
521
522
523
524
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
532
533
534 return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s)
535 default:
536
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
550 func asciiAlpha(c byte) bool {
551 return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
552 }
553
554
555 func asciiAlphaNum(c byte) bool {
556 return asciiAlpha(c) || '0' <= c && c <= '9'
557 }
558
559
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
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
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
587 default:
588 return j
589 }
590 }
591 return len(s)
592 }
593
View as plain text