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