1
2
3
4
5 package template
6
7 import (
8 "bytes"
9 "encoding/json"
10 "fmt"
11 "reflect"
12 "regexp"
13 "strings"
14 "unicode/utf8"
15 )
16
17
18
19
20 const jsWhitespace = "\f\n\r\t\v\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
21
22
23
24
25
26
27
28
29
30
31
32
33
34 func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
35
36 s = bytes.TrimRight(s, jsWhitespace)
37 if len(s) == 0 {
38 return preceding
39 }
40
41
42 switch c, n := s[len(s)-1], len(s); c {
43 case '+', '-':
44
45
46 start := n - 1
47
48 for start > 0 && s[start-1] == c {
49 start--
50 }
51 if (n-start)&1 == 1 {
52
53
54 return jsCtxRegexp
55 }
56 return jsCtxDivOp
57 case '.':
58
59 if n != 1 && '0' <= s[n-2] && s[n-2] <= '9' {
60 return jsCtxDivOp
61 }
62 return jsCtxRegexp
63
64
65 case ',', '<', '>', '=', '*', '%', '&', '|', '^', '?':
66 return jsCtxRegexp
67
68
69 case '!', '~':
70 return jsCtxRegexp
71
72
73 case '(', '[':
74 return jsCtxRegexp
75
76
77 case ':', ';', '{':
78 return jsCtxRegexp
79
80
81
82
83
84
85
86
87
88
89
90 case '}':
91 return jsCtxRegexp
92 default:
93
94
95 j := n
96 for j > 0 && isJSIdentPart(rune(s[j-1])) {
97 j--
98 }
99
100
101 if regexpPrecederKeywords[string(s[j:])] &&
102 !bytes.HasSuffix(bytes.TrimRight(s[:j], jsWhitespace), []byte(".")) {
103 return jsCtxRegexp
104 }
105 }
106
107
108
109 return jsCtxDivOp
110 }
111
112
113
114
115 var regexpPrecederKeywords = map[string]bool{
116 "break": true,
117 "case": true,
118 "continue": true,
119 "delete": true,
120 "do": true,
121 "else": true,
122 "finally": true,
123 "in": true,
124 "instanceof": true,
125 "return": true,
126 "throw": true,
127 "try": true,
128 "typeof": true,
129 "void": true,
130 "yield": true,
131 }
132
133 var jsonMarshalType = reflect.TypeFor[json.Marshaler]()
134
135
136
137 func indirectToJSONMarshaler(a any) any {
138
139
140
141
142 if a == nil {
143 return nil
144 }
145
146 v := reflect.ValueOf(a)
147 for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Pointer && !v.IsNil() {
148 v = v.Elem()
149 }
150 return v.Interface()
151 }
152
153 var scriptTagRe = regexp.MustCompile("(?i)<(/?)script")
154
155
156
157 func jsValEscaper(args ...any) string {
158 var a any
159 if len(args) == 1 {
160 a = indirectToJSONMarshaler(args[0])
161 switch t := a.(type) {
162 case JS:
163 return string(t)
164 case JSStr:
165
166 return `"` + string(t) + `"`
167 case json.Marshaler:
168
169 case fmt.Stringer:
170 a = t.String()
171 }
172 } else {
173 for i, arg := range args {
174 args[i] = indirectToJSONMarshaler(arg)
175 }
176 a = fmt.Sprint(args...)
177 }
178
179
180 b, err := json.Marshal(a)
181 if err != nil {
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202 errStr := err.Error()
203 errStr = string(scriptTagRe.ReplaceAll([]byte(errStr), []byte(`\x3C${1}script`)))
204 errStr = strings.ReplaceAll(errStr, "*/", "* /")
205 errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
206 return fmt.Sprintf(" /* %s */null ", errStr)
207 }
208
209
210
211
212
213
214 if len(b) == 0 {
215
216
217 return " null "
218 }
219 first, _ := utf8.DecodeRune(b)
220 last, _ := utf8.DecodeLastRune(b)
221 var buf strings.Builder
222
223
224 pad := isJSIdentPart(first) || isJSIdentPart(last)
225 if pad {
226 buf.WriteByte(' ')
227 }
228 written := 0
229
230
231 for i := 0; i < len(b); {
232 rune, n := utf8.DecodeRune(b[i:])
233 repl := ""
234 if rune == 0x2028 {
235 repl = `\u2028`
236 } else if rune == 0x2029 {
237 repl = `\u2029`
238 }
239 if repl != "" {
240 buf.Write(b[written:i])
241 buf.WriteString(repl)
242 written = i + n
243 }
244 i += n
245 }
246 if buf.Len() != 0 {
247 buf.Write(b[written:])
248 if pad {
249 buf.WriteByte(' ')
250 }
251 return buf.String()
252 }
253 return string(b)
254 }
255
256
257
258
259 func jsStrEscaper(args ...any) string {
260 s, t := stringify(args...)
261 if t == contentTypeJSStr {
262 return replace(s, jsStrNormReplacementTable)
263 }
264 return replace(s, jsStrReplacementTable)
265 }
266
267 func jsTmplLitEscaper(args ...any) string {
268 s, _ := stringify(args...)
269 return replace(s, jsBqStrReplacementTable)
270 }
271
272
273
274
275
276 func jsRegexpEscaper(args ...any) string {
277 s, _ := stringify(args...)
278 s = replace(s, jsRegexpReplacementTable)
279 if s == "" {
280
281 return "(?:)"
282 }
283 return s
284 }
285
286
287
288
289
290
291 func replace(s string, replacementTable []string) string {
292 var b strings.Builder
293 r, w, written := rune(0), 0, 0
294 for i := 0; i < len(s); i += w {
295
296 r, w = utf8.DecodeRuneInString(s[i:])
297 var repl string
298 switch {
299 case int(r) < len(lowUnicodeReplacementTable):
300 repl = lowUnicodeReplacementTable[r]
301 case int(r) < len(replacementTable) && replacementTable[r] != "":
302 repl = replacementTable[r]
303 case r == '\u2028':
304 repl = `\u2028`
305 case r == '\u2029':
306 repl = `\u2029`
307 default:
308 continue
309 }
310 if written == 0 {
311 b.Grow(len(s))
312 }
313 b.WriteString(s[written:i])
314 b.WriteString(repl)
315 written = i + w
316 }
317 if written == 0 {
318 return s
319 }
320 b.WriteString(s[written:])
321 return b.String()
322 }
323
324 var lowUnicodeReplacementTable = []string{
325 0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
326 '\a': `\u0007`,
327 '\b': `\u0008`,
328 '\t': `\t`,
329 '\n': `\n`,
330 '\v': `\u000b`,
331 '\f': `\f`,
332 '\r': `\r`,
333 0xe: `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`,
334 0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`,
335 0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`,
336 }
337
338 var jsStrReplacementTable = []string{
339 0: `\u0000`,
340 '\t': `\t`,
341 '\n': `\n`,
342 '\v': `\u000b`,
343 '\f': `\f`,
344 '\r': `\r`,
345
346
347 '"': `\u0022`,
348 '`': `\u0060`,
349 '&': `\u0026`,
350 '\'': `\u0027`,
351 '+': `\u002b`,
352 '/': `\/`,
353 '<': `\u003c`,
354 '>': `\u003e`,
355 '\\': `\\`,
356 }
357
358
359
360 var jsBqStrReplacementTable = []string{
361 0: `\u0000`,
362 '\t': `\t`,
363 '\n': `\n`,
364 '\v': `\u000b`,
365 '\f': `\f`,
366 '\r': `\r`,
367
368
369 '"': `\u0022`,
370 '`': `\u0060`,
371 '&': `\u0026`,
372 '\'': `\u0027`,
373 '+': `\u002b`,
374 '/': `\/`,
375 '<': `\u003c`,
376 '>': `\u003e`,
377 '\\': `\\`,
378 '$': `\u0024`,
379 '{': `\u007b`,
380 '}': `\u007d`,
381 }
382
383
384
385 var jsStrNormReplacementTable = []string{
386 0: `\u0000`,
387 '\t': `\t`,
388 '\n': `\n`,
389 '\v': `\u000b`,
390 '\f': `\f`,
391 '\r': `\r`,
392
393
394 '"': `\u0022`,
395 '&': `\u0026`,
396 '\'': `\u0027`,
397 '`': `\u0060`,
398 '+': `\u002b`,
399 '/': `\/`,
400 '<': `\u003c`,
401 '>': `\u003e`,
402 }
403 var jsRegexpReplacementTable = []string{
404 0: `\u0000`,
405 '\t': `\t`,
406 '\n': `\n`,
407 '\v': `\u000b`,
408 '\f': `\f`,
409 '\r': `\r`,
410
411
412 '"': `\u0022`,
413 '$': `\$`,
414 '&': `\u0026`,
415 '\'': `\u0027`,
416 '(': `\(`,
417 ')': `\)`,
418 '*': `\*`,
419 '+': `\u002b`,
420 '-': `\-`,
421 '.': `\.`,
422 '/': `\/`,
423 '<': `\u003c`,
424 '>': `\u003e`,
425 '?': `\?`,
426 '[': `\[`,
427 '\\': `\\`,
428 ']': `\]`,
429 '^': `\^`,
430 '{': `\{`,
431 '|': `\|`,
432 '}': `\}`,
433 }
434
435
436
437
438
439 func isJSIdentPart(r rune) bool {
440 switch {
441 case r == '$':
442 return true
443 case '0' <= r && r <= '9':
444 return true
445 case 'A' <= r && r <= 'Z':
446 return true
447 case r == '_':
448 return true
449 case 'a' <= r && r <= 'z':
450 return true
451 }
452 return false
453 }
454
455
456
457
458 func isJSType(mimeType string) bool {
459
460
461
462
463
464
465 mimeType, _, _ = strings.Cut(mimeType, ";")
466 mimeType = strings.ToLower(mimeType)
467 mimeType = strings.TrimSpace(mimeType)
468 switch mimeType {
469 case
470 "",
471 "application/ecmascript",
472 "application/javascript",
473 "application/json",
474 "application/ld+json",
475 "application/x-ecmascript",
476 "application/x-javascript",
477 "module",
478 "text/ecmascript",
479 "text/javascript",
480 "text/javascript1.0",
481 "text/javascript1.1",
482 "text/javascript1.2",
483 "text/javascript1.3",
484 "text/javascript1.4",
485 "text/javascript1.5",
486 "text/jscript",
487 "text/livescript",
488 "text/x-ecmascript",
489 "text/x-javascript":
490 return true
491 default:
492 return false
493 }
494 }
495
View as plain text