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.TypeOf((*json.Marshaler)(nil)).Elem()
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 return fmt.Sprintf(" /* %s */null ", strings.ReplaceAll(err.Error(), "*/", "* /"))
181 }
182
183
184
185
186
187
188 if len(b) == 0 {
189
190
191 return " null "
192 }
193 first, _ := utf8.DecodeRune(b)
194 last, _ := utf8.DecodeLastRune(b)
195 var buf strings.Builder
196
197
198 pad := isJSIdentPart(first) || isJSIdentPart(last)
199 if pad {
200 buf.WriteByte(' ')
201 }
202 written := 0
203
204
205 for i := 0; i < len(b); {
206 rune, n := utf8.DecodeRune(b[i:])
207 repl := ""
208 if rune == 0x2028 {
209 repl = `\u2028`
210 } else if rune == 0x2029 {
211 repl = `\u2029`
212 }
213 if repl != "" {
214 buf.Write(b[written:i])
215 buf.WriteString(repl)
216 written = i + n
217 }
218 i += n
219 }
220 if buf.Len() != 0 {
221 buf.Write(b[written:])
222 if pad {
223 buf.WriteByte(' ')
224 }
225 return buf.String()
226 }
227 return string(b)
228 }
229
230
231
232
233 func jsStrEscaper(args ...any) string {
234 s, t := stringify(args...)
235 if t == contentTypeJSStr {
236 return replace(s, jsStrNormReplacementTable)
237 }
238 return replace(s, jsStrReplacementTable)
239 }
240
241
242
243
244
245 func jsRegexpEscaper(args ...any) string {
246 s, _ := stringify(args...)
247 s = replace(s, jsRegexpReplacementTable)
248 if s == "" {
249
250 return "(?:)"
251 }
252 return s
253 }
254
255
256
257
258
259
260 func replace(s string, replacementTable []string) string {
261 var b strings.Builder
262 r, w, written := rune(0), 0, 0
263 for i := 0; i < len(s); i += w {
264
265 r, w = utf8.DecodeRuneInString(s[i:])
266 var repl string
267 switch {
268 case int(r) < len(lowUnicodeReplacementTable):
269 repl = lowUnicodeReplacementTable[r]
270 case int(r) < len(replacementTable) && replacementTable[r] != "":
271 repl = replacementTable[r]
272 case r == '\u2028':
273 repl = `\u2028`
274 case r == '\u2029':
275 repl = `\u2029`
276 default:
277 continue
278 }
279 if written == 0 {
280 b.Grow(len(s))
281 }
282 b.WriteString(s[written:i])
283 b.WriteString(repl)
284 written = i + w
285 }
286 if written == 0 {
287 return s
288 }
289 b.WriteString(s[written:])
290 return b.String()
291 }
292
293 var lowUnicodeReplacementTable = []string{
294 0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
295 '\a': `\u0007`,
296 '\b': `\u0008`,
297 '\t': `\t`,
298 '\n': `\n`,
299 '\v': `\u000b`,
300 '\f': `\f`,
301 '\r': `\r`,
302 0xe: `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`,
303 0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`,
304 0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`,
305 }
306
307 var jsStrReplacementTable = []string{
308 0: `\u0000`,
309 '\t': `\t`,
310 '\n': `\n`,
311 '\v': `\u000b`,
312 '\f': `\f`,
313 '\r': `\r`,
314
315
316 '"': `\u0022`,
317 '`': `\u0060`,
318 '&': `\u0026`,
319 '\'': `\u0027`,
320 '+': `\u002b`,
321 '/': `\/`,
322 '<': `\u003c`,
323 '>': `\u003e`,
324 '\\': `\\`,
325 }
326
327
328
329 var jsStrNormReplacementTable = []string{
330 0: `\u0000`,
331 '\t': `\t`,
332 '\n': `\n`,
333 '\v': `\u000b`,
334 '\f': `\f`,
335 '\r': `\r`,
336
337
338 '"': `\u0022`,
339 '&': `\u0026`,
340 '\'': `\u0027`,
341 '`': `\u0060`,
342 '+': `\u002b`,
343 '/': `\/`,
344 '<': `\u003c`,
345 '>': `\u003e`,
346 }
347 var jsRegexpReplacementTable = []string{
348 0: `\u0000`,
349 '\t': `\t`,
350 '\n': `\n`,
351 '\v': `\u000b`,
352 '\f': `\f`,
353 '\r': `\r`,
354
355
356 '"': `\u0022`,
357 '$': `\$`,
358 '&': `\u0026`,
359 '\'': `\u0027`,
360 '(': `\(`,
361 ')': `\)`,
362 '*': `\*`,
363 '+': `\u002b`,
364 '-': `\-`,
365 '.': `\.`,
366 '/': `\/`,
367 '<': `\u003c`,
368 '>': `\u003e`,
369 '?': `\?`,
370 '[': `\[`,
371 '\\': `\\`,
372 ']': `\]`,
373 '^': `\^`,
374 '{': `\{`,
375 '|': `\|`,
376 '}': `\}`,
377 }
378
379
380
381
382
383 func isJSIdentPart(r rune) bool {
384 switch {
385 case r == '$':
386 return true
387 case '0' <= r && r <= '9':
388 return true
389 case 'A' <= r && r <= 'Z':
390 return true
391 case r == '_':
392 return true
393 case 'a' <= r && r <= 'z':
394 return true
395 }
396 return false
397 }
398
399
400
401
402 func isJSType(mimeType string) bool {
403
404
405
406
407
408
409 mimeType, _, _ = strings.Cut(mimeType, ";")
410 mimeType = strings.ToLower(mimeType)
411 mimeType = strings.TrimSpace(mimeType)
412 switch mimeType {
413 case
414 "application/ecmascript",
415 "application/javascript",
416 "application/json",
417 "application/ld+json",
418 "application/x-ecmascript",
419 "application/x-javascript",
420 "module",
421 "text/ecmascript",
422 "text/javascript",
423 "text/javascript1.0",
424 "text/javascript1.1",
425 "text/javascript1.2",
426 "text/javascript1.3",
427 "text/javascript1.4",
428 "text/javascript1.5",
429 "text/jscript",
430 "text/livescript",
431 "text/x-ecmascript",
432 "text/x-javascript":
433 return true
434 default:
435 return false
436 }
437 }
438
View as plain text