1
2
3
4
5 package template
6
7 import (
8 "bytes"
9 "fmt"
10 "html"
11 "io"
12 "text/template"
13 "text/template/parse"
14 )
15
16
17
18
19
20
21 func escapeTemplate(tmpl *Template, node parse.Node, name string) error {
22 c, _ := tmpl.esc.escapeTree(context{}, node, name, 0)
23 var err error
24 if c.err != nil {
25 err, c.err.Name = c.err, name
26 } else if c.state != stateText {
27 err = &Error{ErrEndContext, nil, name, 0, fmt.Sprintf("ends in a non-text context: %v", c)}
28 }
29 if err != nil {
30
31 if t := tmpl.set[name]; t != nil {
32 t.escapeErr = err
33 t.text.Tree = nil
34 t.Tree = nil
35 }
36 return err
37 }
38 tmpl.esc.commit()
39 if t := tmpl.set[name]; t != nil {
40 t.escapeErr = escapeOK
41 t.Tree = t.text.Tree
42 }
43 return nil
44 }
45
46
47
48 func evalArgs(args ...any) string {
49
50 if len(args) == 1 {
51 if s, ok := args[0].(string); ok {
52 return s
53 }
54 }
55 for i, arg := range args {
56 args[i] = indirectToStringerOrError(arg)
57 }
58 return fmt.Sprint(args...)
59 }
60
61
62 var funcMap = template.FuncMap{
63 "_html_template_attrescaper": attrEscaper,
64 "_html_template_commentescaper": commentEscaper,
65 "_html_template_cssescaper": cssEscaper,
66 "_html_template_cssvaluefilter": cssValueFilter,
67 "_html_template_htmlnamefilter": htmlNameFilter,
68 "_html_template_htmlescaper": htmlEscaper,
69 "_html_template_jsregexpescaper": jsRegexpEscaper,
70 "_html_template_jsstrescaper": jsStrEscaper,
71 "_html_template_jsvalescaper": jsValEscaper,
72 "_html_template_nospaceescaper": htmlNospaceEscaper,
73 "_html_template_rcdataescaper": rcdataEscaper,
74 "_html_template_srcsetescaper": srcsetFilterAndEscaper,
75 "_html_template_urlescaper": urlEscaper,
76 "_html_template_urlfilter": urlFilter,
77 "_html_template_urlnormalizer": urlNormalizer,
78 "_eval_args_": evalArgs,
79 }
80
81
82
83 type escaper struct {
84
85 ns *nameSpace
86
87
88 output map[string]context
89
90
91 derived map[string]*template.Template
92
93 called map[string]bool
94
95
96
97 actionNodeEdits map[*parse.ActionNode][]string
98 templateNodeEdits map[*parse.TemplateNode]string
99 textNodeEdits map[*parse.TextNode][]byte
100
101 rangeContext *rangeContext
102 }
103
104
105 type rangeContext struct {
106 outer *rangeContext
107 breaks []context
108 continues []context
109 }
110
111
112 func makeEscaper(n *nameSpace) escaper {
113 return escaper{
114 n,
115 map[string]context{},
116 map[string]*template.Template{},
117 map[string]bool{},
118 map[*parse.ActionNode][]string{},
119 map[*parse.TemplateNode]string{},
120 map[*parse.TextNode][]byte{},
121 nil,
122 }
123 }
124
125
126
127
128
129
130 const filterFailsafe = "ZgotmplZ"
131
132
133 func (e *escaper) escape(c context, n parse.Node) context {
134 switch n := n.(type) {
135 case *parse.ActionNode:
136 return e.escapeAction(c, n)
137 case *parse.BreakNode:
138 c.n = n
139 e.rangeContext.breaks = append(e.rangeContext.breaks, c)
140 return context{state: stateDead}
141 case *parse.CommentNode:
142 return c
143 case *parse.ContinueNode:
144 c.n = n
145 e.rangeContext.continues = append(e.rangeContext.breaks, c)
146 return context{state: stateDead}
147 case *parse.IfNode:
148 return e.escapeBranch(c, &n.BranchNode, "if")
149 case *parse.ListNode:
150 return e.escapeList(c, n)
151 case *parse.RangeNode:
152 return e.escapeBranch(c, &n.BranchNode, "range")
153 case *parse.TemplateNode:
154 return e.escapeTemplate(c, n)
155 case *parse.TextNode:
156 return e.escapeText(c, n)
157 case *parse.WithNode:
158 return e.escapeBranch(c, &n.BranchNode, "with")
159 }
160 panic("escaping " + n.String() + " is unimplemented")
161 }
162
163
164 func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
165 if len(n.Pipe.Decl) != 0 {
166
167 return c
168 }
169 c = nudge(c)
170
171 for pos, idNode := range n.Pipe.Cmds {
172 node, ok := idNode.Args[0].(*parse.IdentifierNode)
173 if !ok {
174
175
176
177
178
179
180
181 continue
182 }
183 ident := node.Ident
184 if _, ok := predefinedEscapers[ident]; ok {
185 if pos < len(n.Pipe.Cmds)-1 ||
186 c.state == stateAttr && c.delim == delimSpaceOrTagEnd && ident == "html" {
187 return context{
188 state: stateError,
189 err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident),
190 }
191 }
192 }
193 }
194 s := make([]string, 0, 3)
195 switch c.state {
196 case stateError:
197 return c
198 case stateURL, stateCSSDqStr, stateCSSSqStr, stateCSSDqURL, stateCSSSqURL, stateCSSURL:
199 switch c.urlPart {
200 case urlPartNone:
201 s = append(s, "_html_template_urlfilter")
202 fallthrough
203 case urlPartPreQuery:
204 switch c.state {
205 case stateCSSDqStr, stateCSSSqStr:
206 s = append(s, "_html_template_cssescaper")
207 default:
208 s = append(s, "_html_template_urlnormalizer")
209 }
210 case urlPartQueryOrFrag:
211 s = append(s, "_html_template_urlescaper")
212 case urlPartUnknown:
213 return context{
214 state: stateError,
215 err: errorf(ErrAmbigContext, n, n.Line, "%s appears in an ambiguous context within a URL", n),
216 }
217 default:
218 panic(c.urlPart.String())
219 }
220 case stateJS:
221 s = append(s, "_html_template_jsvalescaper")
222
223 c.jsCtx = jsCtxDivOp
224 case stateJSDqStr, stateJSSqStr:
225 s = append(s, "_html_template_jsstrescaper")
226 case stateJSRegexp:
227 s = append(s, "_html_template_jsregexpescaper")
228 case stateCSS:
229 s = append(s, "_html_template_cssvaluefilter")
230 case stateText:
231 s = append(s, "_html_template_htmlescaper")
232 case stateRCDATA:
233 s = append(s, "_html_template_rcdataescaper")
234 case stateAttr:
235
236 case stateAttrName, stateTag:
237 c.state = stateAttrName
238 s = append(s, "_html_template_htmlnamefilter")
239 case stateSrcset:
240 s = append(s, "_html_template_srcsetescaper")
241 default:
242 if isComment(c.state) {
243 s = append(s, "_html_template_commentescaper")
244 } else {
245 panic("unexpected state " + c.state.String())
246 }
247 }
248 switch c.delim {
249 case delimNone:
250
251 case delimSpaceOrTagEnd:
252 s = append(s, "_html_template_nospaceescaper")
253 default:
254 s = append(s, "_html_template_attrescaper")
255 }
256 e.editActionNode(n, s)
257 return c
258 }
259
260
261
262
263 func ensurePipelineContains(p *parse.PipeNode, s []string) {
264 if len(s) == 0 {
265
266 return
267 }
268
269
270
271 pipelineLen := len(p.Cmds)
272 if pipelineLen > 0 {
273 lastCmd := p.Cmds[pipelineLen-1]
274 if idNode, ok := lastCmd.Args[0].(*parse.IdentifierNode); ok {
275 if esc := idNode.Ident; predefinedEscapers[esc] {
276
277 if len(p.Cmds) == 1 && len(lastCmd.Args) > 1 {
278
279
280
281
282
283 lastCmd.Args[0] = parse.NewIdentifier("_eval_args_").SetTree(nil).SetPos(lastCmd.Args[0].Position())
284 p.Cmds = appendCmd(p.Cmds, newIdentCmd(esc, p.Position()))
285 pipelineLen++
286 }
287
288
289 dup := false
290 for i, escaper := range s {
291 if escFnsEq(esc, escaper) {
292 s[i] = idNode.Ident
293 dup = true
294 }
295 }
296 if dup {
297
298
299 pipelineLen--
300 }
301 }
302 }
303 }
304
305 newCmds := make([]*parse.CommandNode, pipelineLen, pipelineLen+len(s))
306 insertedIdents := make(map[string]bool)
307 for i := 0; i < pipelineLen; i++ {
308 cmd := p.Cmds[i]
309 newCmds[i] = cmd
310 if idNode, ok := cmd.Args[0].(*parse.IdentifierNode); ok {
311 insertedIdents[normalizeEscFn(idNode.Ident)] = true
312 }
313 }
314 for _, name := range s {
315 if !insertedIdents[normalizeEscFn(name)] {
316
317
318
319
320 newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position()))
321 }
322 }
323 p.Cmds = newCmds
324 }
325
326
327
328 var predefinedEscapers = map[string]bool{
329 "html": true,
330 "urlquery": true,
331 }
332
333
334
335 var equivEscapers = map[string]string{
336
337
338 "_html_template_attrescaper": "html",
339 "_html_template_htmlescaper": "html",
340 "_html_template_rcdataescaper": "html",
341
342
343
344 "_html_template_urlescaper": "urlquery",
345
346
347
348
349
350
351 "_html_template_urlnormalizer": "urlquery",
352 }
353
354
355 func escFnsEq(a, b string) bool {
356 return normalizeEscFn(a) == normalizeEscFn(b)
357 }
358
359
360
361 func normalizeEscFn(e string) string {
362 if norm := equivEscapers[e]; norm != "" {
363 return norm
364 }
365 return e
366 }
367
368
369
370 var redundantFuncs = map[string]map[string]bool{
371 "_html_template_commentescaper": {
372 "_html_template_attrescaper": true,
373 "_html_template_nospaceescaper": true,
374 "_html_template_htmlescaper": true,
375 },
376 "_html_template_cssescaper": {
377 "_html_template_attrescaper": true,
378 },
379 "_html_template_jsregexpescaper": {
380 "_html_template_attrescaper": true,
381 },
382 "_html_template_jsstrescaper": {
383 "_html_template_attrescaper": true,
384 },
385 "_html_template_urlescaper": {
386 "_html_template_urlnormalizer": true,
387 },
388 }
389
390
391
392 func appendCmd(cmds []*parse.CommandNode, cmd *parse.CommandNode) []*parse.CommandNode {
393 if n := len(cmds); n != 0 {
394 last, okLast := cmds[n-1].Args[0].(*parse.IdentifierNode)
395 next, okNext := cmd.Args[0].(*parse.IdentifierNode)
396 if okLast && okNext && redundantFuncs[last.Ident][next.Ident] {
397 return cmds
398 }
399 }
400 return append(cmds, cmd)
401 }
402
403
404 func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode {
405 return &parse.CommandNode{
406 NodeType: parse.NodeCommand,
407 Args: []parse.Node{parse.NewIdentifier(identifier).SetTree(nil).SetPos(pos)},
408 }
409 }
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429 func nudge(c context) context {
430 switch c.state {
431 case stateTag:
432
433 c.state = stateAttrName
434 case stateBeforeValue:
435
436 c.state, c.delim, c.attr = attrStartStates[c.attr], delimSpaceOrTagEnd, attrNone
437 case stateAfterName:
438
439 c.state, c.attr = stateAttrName, attrNone
440 }
441 return c
442 }
443
444
445
446
447 func join(a, b context, node parse.Node, nodeName string) context {
448 if a.state == stateError {
449 return a
450 }
451 if b.state == stateError {
452 return b
453 }
454 if a.state == stateDead {
455 return b
456 }
457 if b.state == stateDead {
458 return a
459 }
460 if a.eq(b) {
461 return a
462 }
463
464 c := a
465 c.urlPart = b.urlPart
466 if c.eq(b) {
467
468 c.urlPart = urlPartUnknown
469 return c
470 }
471
472 c = a
473 c.jsCtx = b.jsCtx
474 if c.eq(b) {
475
476 c.jsCtx = jsCtxUnknown
477 return c
478 }
479
480
481
482
483
484
485 if c, d := nudge(a), nudge(b); !(c.eq(a) && d.eq(b)) {
486 if e := join(c, d, node, nodeName); e.state != stateError {
487 return e
488 }
489 }
490
491 return context{
492 state: stateError,
493 err: errorf(ErrBranchEnd, node, 0, "{{%s}} branches end in different contexts: %v, %v", nodeName, a, b),
494 }
495 }
496
497
498 func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context {
499 if nodeName == "range" {
500 e.rangeContext = &rangeContext{outer: e.rangeContext}
501 }
502 c0 := e.escapeList(c, n.List)
503 if nodeName == "range" {
504 if c0.state != stateError {
505 c0 = joinRange(c0, e.rangeContext)
506 }
507 e.rangeContext = e.rangeContext.outer
508 if c0.state == stateError {
509 return c0
510 }
511
512
513
514
515 e.rangeContext = &rangeContext{outer: e.rangeContext}
516 c1, _ := e.escapeListConditionally(c0, n.List, nil)
517 c0 = join(c0, c1, n, nodeName)
518 if c0.state == stateError {
519 e.rangeContext = e.rangeContext.outer
520
521
522
523 c0.err.Line = n.Line
524 c0.err.Description = "on range loop re-entry: " + c0.err.Description
525 return c0
526 }
527 c0 = joinRange(c0, e.rangeContext)
528 e.rangeContext = e.rangeContext.outer
529 if c0.state == stateError {
530 return c0
531 }
532 }
533 c1 := e.escapeList(c, n.ElseList)
534 return join(c0, c1, n, nodeName)
535 }
536
537 func joinRange(c0 context, rc *rangeContext) context {
538
539
540
541 for _, c := range rc.breaks {
542 c0 = join(c0, c, c.n, "range")
543 if c0.state == stateError {
544 c0.err.Line = c.n.(*parse.BreakNode).Line
545 c0.err.Description = "at range loop break: " + c0.err.Description
546 return c0
547 }
548 }
549 for _, c := range rc.continues {
550 c0 = join(c0, c, c.n, "range")
551 if c0.state == stateError {
552 c0.err.Line = c.n.(*parse.ContinueNode).Line
553 c0.err.Description = "at range loop continue: " + c0.err.Description
554 return c0
555 }
556 }
557 return c0
558 }
559
560
561 func (e *escaper) escapeList(c context, n *parse.ListNode) context {
562 if n == nil {
563 return c
564 }
565 for _, m := range n.Nodes {
566 c = e.escape(c, m)
567 if c.state == stateDead {
568 break
569 }
570 }
571 return c
572 }
573
574
575
576
577
578 func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) {
579 e1 := makeEscaper(e.ns)
580 e1.rangeContext = e.rangeContext
581
582 for k, v := range e.output {
583 e1.output[k] = v
584 }
585 c = e1.escapeList(c, n)
586 ok := filter != nil && filter(&e1, c)
587 if ok {
588
589 for k, v := range e1.output {
590 e.output[k] = v
591 }
592 for k, v := range e1.derived {
593 e.derived[k] = v
594 }
595 for k, v := range e1.called {
596 e.called[k] = v
597 }
598 for k, v := range e1.actionNodeEdits {
599 e.editActionNode(k, v)
600 }
601 for k, v := range e1.templateNodeEdits {
602 e.editTemplateNode(k, v)
603 }
604 for k, v := range e1.textNodeEdits {
605 e.editTextNode(k, v)
606 }
607 }
608 return c, ok
609 }
610
611
612 func (e *escaper) escapeTemplate(c context, n *parse.TemplateNode) context {
613 c, name := e.escapeTree(c, n, n.Name, n.Line)
614 if name != n.Name {
615 e.editTemplateNode(n, name)
616 }
617 return c
618 }
619
620
621
622 func (e *escaper) escapeTree(c context, node parse.Node, name string, line int) (context, string) {
623
624
625 dname := c.mangle(name)
626 e.called[dname] = true
627 if out, ok := e.output[dname]; ok {
628
629 return out, dname
630 }
631 t := e.template(name)
632 if t == nil {
633
634
635 if e.ns.set[name] != nil {
636 return context{
637 state: stateError,
638 err: errorf(ErrNoSuchTemplate, node, line, "%q is an incomplete or empty template", name),
639 }, dname
640 }
641 return context{
642 state: stateError,
643 err: errorf(ErrNoSuchTemplate, node, line, "no such template %q", name),
644 }, dname
645 }
646 if dname != name {
647
648
649 dt := e.template(dname)
650 if dt == nil {
651 dt = template.New(dname)
652 dt.Tree = &parse.Tree{Name: dname, Root: t.Root.CopyList()}
653 e.derived[dname] = dt
654 }
655 t = dt
656 }
657 return e.computeOutCtx(c, t), dname
658 }
659
660
661
662 func (e *escaper) computeOutCtx(c context, t *template.Template) context {
663
664 c1, ok := e.escapeTemplateBody(c, t)
665 if !ok {
666
667 if c2, ok2 := e.escapeTemplateBody(c1, t); ok2 {
668 c1, ok = c2, true
669 }
670
671 }
672 if !ok && c1.state != stateError {
673 return context{
674 state: stateError,
675 err: errorf(ErrOutputContext, t.Tree.Root, 0, "cannot compute output context for template %s", t.Name()),
676 }
677 }
678 return c1
679 }
680
681
682
683
684 func (e *escaper) escapeTemplateBody(c context, t *template.Template) (context, bool) {
685 filter := func(e1 *escaper, c1 context) bool {
686 if c1.state == stateError {
687
688 return false
689 }
690 if !e1.called[t.Name()] {
691
692
693 return true
694 }
695
696 return c.eq(c1)
697 }
698
699
700
701
702 e.output[t.Name()] = c
703 return e.escapeListConditionally(c, t.Tree.Root, filter)
704 }
705
706
707 var delimEnds = [...]string{
708 delimDoubleQuote: `"`,
709 delimSingleQuote: "'",
710
711
712
713
714
715
716
717 delimSpaceOrTagEnd: " \t\n\f\r>",
718 }
719
720 var doctypeBytes = []byte("<!DOCTYPE")
721
722
723 func (e *escaper) escapeText(c context, n *parse.TextNode) context {
724 s, written, i, b := n.Text, 0, 0, new(bytes.Buffer)
725 for i != len(s) {
726 c1, nread := contextAfterText(c, s[i:])
727 i1 := i + nread
728 if c.state == stateText || c.state == stateRCDATA {
729 end := i1
730 if c1.state != c.state {
731 for j := end - 1; j >= i; j-- {
732 if s[j] == '<' {
733 end = j
734 break
735 }
736 }
737 }
738 for j := i; j < end; j++ {
739 if s[j] == '<' && !bytes.HasPrefix(bytes.ToUpper(s[j:]), doctypeBytes) {
740 b.Write(s[written:j])
741 b.WriteString("<")
742 written = j + 1
743 }
744 }
745 } else if isComment(c.state) && c.delim == delimNone {
746 switch c.state {
747 case stateJSBlockCmt:
748
749
750
751
752
753
754
755 if bytes.ContainsAny(s[written:i1], "\n\r\u2028\u2029") {
756 b.WriteByte('\n')
757 } else {
758 b.WriteByte(' ')
759 }
760 case stateCSSBlockCmt:
761 b.WriteByte(' ')
762 }
763 written = i1
764 }
765 if c.state != c1.state && isComment(c1.state) && c1.delim == delimNone {
766
767 cs := i1 - 2
768 if c1.state == stateHTMLCmt {
769
770 cs -= 2
771 }
772 b.Write(s[written:cs])
773 written = i1
774 }
775 if i == i1 && c.state == c1.state {
776 panic(fmt.Sprintf("infinite loop from %v to %v on %q..%q", c, c1, s[:i], s[i:]))
777 }
778 c, i = c1, i1
779 }
780
781 if written != 0 && c.state != stateError {
782 if !isComment(c.state) || c.delim != delimNone {
783 b.Write(n.Text[written:])
784 }
785 e.editTextNode(n, b.Bytes())
786 }
787 return c
788 }
789
790
791
792 func contextAfterText(c context, s []byte) (context, int) {
793 if c.delim == delimNone {
794 c1, i := tSpecialTagEnd(c, s)
795 if i == 0 {
796
797
798 return c1, 0
799 }
800
801 return transitionFunc[c.state](c, s[:i])
802 }
803
804
805
806 i := bytes.IndexAny(s, delimEnds[c.delim])
807 if i == -1 {
808 i = len(s)
809 }
810 if c.delim == delimSpaceOrTagEnd {
811
812
813
814
815
816
817
818 if j := bytes.IndexAny(s[:i], "\"'<=`"); j >= 0 {
819 return context{
820 state: stateError,
821 err: errorf(ErrBadHTML, nil, 0, "%q in unquoted attr: %q", s[j:j+1], s[:i]),
822 }, len(s)
823 }
824 }
825 if i == len(s) {
826
827
828
829
830 for u := []byte(html.UnescapeString(string(s))); len(u) != 0; {
831 c1, i1 := transitionFunc[c.state](c, u)
832 c, u = c1, u[i1:]
833 }
834 return c, len(s)
835 }
836
837 element := c.element
838
839
840 if c.state == stateAttr && c.element == elementScript && c.attr == attrScriptType && !isJSType(string(s[:i])) {
841 element = elementNone
842 }
843
844 if c.delim != delimSpaceOrTagEnd {
845
846 i++
847 }
848
849
850 return context{state: stateTag, element: element}, i
851 }
852
853
854 func (e *escaper) editActionNode(n *parse.ActionNode, cmds []string) {
855 if _, ok := e.actionNodeEdits[n]; ok {
856 panic(fmt.Sprintf("node %s shared between templates", n))
857 }
858 e.actionNodeEdits[n] = cmds
859 }
860
861
862 func (e *escaper) editTemplateNode(n *parse.TemplateNode, callee string) {
863 if _, ok := e.templateNodeEdits[n]; ok {
864 panic(fmt.Sprintf("node %s shared between templates", n))
865 }
866 e.templateNodeEdits[n] = callee
867 }
868
869
870 func (e *escaper) editTextNode(n *parse.TextNode, text []byte) {
871 if _, ok := e.textNodeEdits[n]; ok {
872 panic(fmt.Sprintf("node %s shared between templates", n))
873 }
874 e.textNodeEdits[n] = text
875 }
876
877
878
879 func (e *escaper) commit() {
880 for name := range e.output {
881 e.template(name).Funcs(funcMap)
882 }
883
884
885 tmpl := e.arbitraryTemplate()
886 for _, t := range e.derived {
887 if _, err := tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil {
888 panic("error adding derived template")
889 }
890 }
891 for n, s := range e.actionNodeEdits {
892 ensurePipelineContains(n.Pipe, s)
893 }
894 for n, name := range e.templateNodeEdits {
895 n.Name = name
896 }
897 for n, s := range e.textNodeEdits {
898 n.Text = s
899 }
900
901
902 e.called = make(map[string]bool)
903 e.actionNodeEdits = make(map[*parse.ActionNode][]string)
904 e.templateNodeEdits = make(map[*parse.TemplateNode]string)
905 e.textNodeEdits = make(map[*parse.TextNode][]byte)
906 }
907
908
909 func (e *escaper) template(name string) *template.Template {
910
911
912 t := e.arbitraryTemplate().text.Lookup(name)
913 if t == nil {
914 t = e.derived[name]
915 }
916 return t
917 }
918
919
920
921 func (e *escaper) arbitraryTemplate() *Template {
922 for _, t := range e.ns.set {
923 return t
924 }
925 panic("no templates in name space")
926 }
927
928
929
930
931
932 func HTMLEscape(w io.Writer, b []byte) {
933 template.HTMLEscape(w, b)
934 }
935
936
937 func HTMLEscapeString(s string) string {
938 return template.HTMLEscapeString(s)
939 }
940
941
942
943 func HTMLEscaper(args ...any) string {
944 return template.HTMLEscaper(args...)
945 }
946
947
948 func JSEscape(w io.Writer, b []byte) {
949 template.JSEscape(w, b)
950 }
951
952
953 func JSEscapeString(s string) string {
954 return template.JSEscapeString(s)
955 }
956
957
958
959 func JSEscaper(args ...any) string {
960 return template.JSEscaper(args...)
961 }
962
963
964
965 func URLQueryEscaper(args ...any) string {
966 return template.URLQueryEscaper(args...)
967 }
968
View as plain text