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