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 func nudge(c context) context {
424 switch c.state {
425 case stateTag:
426
427 c.state = stateAttrName
428 case stateBeforeValue:
429
430 c.state, c.delim, c.attr = attrStartStates[c.attr], delimSpaceOrTagEnd, attrNone
431 case stateAfterName:
432
433 c.state, c.attr = stateAttrName, attrNone
434 }
435 return c
436 }
437
438
439
440
441 func join(a, b context, node parse.Node, nodeName string) context {
442 if a.state == stateError {
443 return a
444 }
445 if b.state == stateError {
446 return b
447 }
448 if a.state == stateDead {
449 return b
450 }
451 if b.state == stateDead {
452 return a
453 }
454 if a.eq(b) {
455 return a
456 }
457
458 c := a
459 c.urlPart = b.urlPart
460 if c.eq(b) {
461
462 c.urlPart = urlPartUnknown
463 return c
464 }
465
466 c = a
467 c.jsCtx = b.jsCtx
468 if c.eq(b) {
469
470 c.jsCtx = jsCtxUnknown
471 return c
472 }
473
474
475
476
477
478
479 if c, d := nudge(a), nudge(b); !(c.eq(a) && d.eq(b)) {
480 if e := join(c, d, node, nodeName); e.state != stateError {
481 return e
482 }
483 }
484
485 return context{
486 state: stateError,
487 err: errorf(ErrBranchEnd, node, 0, "{{%s}} branches end in different contexts: %v, %v", nodeName, a, b),
488 }
489 }
490
491
492 func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context {
493 if nodeName == "range" {
494 e.rangeContext = &rangeContext{outer: e.rangeContext}
495 }
496 c0 := e.escapeList(c, n.List)
497 if nodeName == "range" {
498 if c0.state != stateError {
499 c0 = joinRange(c0, e.rangeContext)
500 }
501 e.rangeContext = e.rangeContext.outer
502 if c0.state == stateError {
503 return c0
504 }
505
506
507
508
509 e.rangeContext = &rangeContext{outer: e.rangeContext}
510 c1, _ := e.escapeListConditionally(c0, n.List, nil)
511 c0 = join(c0, c1, n, nodeName)
512 if c0.state == stateError {
513 e.rangeContext = e.rangeContext.outer
514
515
516
517 c0.err.Line = n.Line
518 c0.err.Description = "on range loop re-entry: " + c0.err.Description
519 return c0
520 }
521 c0 = joinRange(c0, e.rangeContext)
522 e.rangeContext = e.rangeContext.outer
523 if c0.state == stateError {
524 return c0
525 }
526 }
527 c1 := e.escapeList(c, n.ElseList)
528 return join(c0, c1, n, nodeName)
529 }
530
531 func joinRange(c0 context, rc *rangeContext) context {
532
533
534
535 for _, c := range rc.breaks {
536 c0 = join(c0, c, c.n, "range")
537 if c0.state == stateError {
538 c0.err.Line = c.n.(*parse.BreakNode).Line
539 c0.err.Description = "at range loop break: " + c0.err.Description
540 return c0
541 }
542 }
543 for _, c := range rc.continues {
544 c0 = join(c0, c, c.n, "range")
545 if c0.state == stateError {
546 c0.err.Line = c.n.(*parse.ContinueNode).Line
547 c0.err.Description = "at range loop continue: " + c0.err.Description
548 return c0
549 }
550 }
551 return c0
552 }
553
554
555 func (e *escaper) escapeList(c context, n *parse.ListNode) context {
556 if n == nil {
557 return c
558 }
559 for _, m := range n.Nodes {
560 c = e.escape(c, m)
561 if c.state == stateDead {
562 break
563 }
564 }
565 return c
566 }
567
568
569
570
571
572 func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) {
573 e1 := makeEscaper(e.ns)
574 e1.rangeContext = e.rangeContext
575
576 for k, v := range e.output {
577 e1.output[k] = v
578 }
579 c = e1.escapeList(c, n)
580 ok := filter != nil && filter(&e1, c)
581 if ok {
582
583 for k, v := range e1.output {
584 e.output[k] = v
585 }
586 for k, v := range e1.derived {
587 e.derived[k] = v
588 }
589 for k, v := range e1.called {
590 e.called[k] = v
591 }
592 for k, v := range e1.actionNodeEdits {
593 e.editActionNode(k, v)
594 }
595 for k, v := range e1.templateNodeEdits {
596 e.editTemplateNode(k, v)
597 }
598 for k, v := range e1.textNodeEdits {
599 e.editTextNode(k, v)
600 }
601 }
602 return c, ok
603 }
604
605
606 func (e *escaper) escapeTemplate(c context, n *parse.TemplateNode) context {
607 c, name := e.escapeTree(c, n, n.Name, n.Line)
608 if name != n.Name {
609 e.editTemplateNode(n, name)
610 }
611 return c
612 }
613
614
615
616 func (e *escaper) escapeTree(c context, node parse.Node, name string, line int) (context, string) {
617
618
619 dname := c.mangle(name)
620 e.called[dname] = true
621 if out, ok := e.output[dname]; ok {
622
623 return out, dname
624 }
625 t := e.template(name)
626 if t == nil {
627
628
629 if e.ns.set[name] != nil {
630 return context{
631 state: stateError,
632 err: errorf(ErrNoSuchTemplate, node, line, "%q is an incomplete or empty template", name),
633 }, dname
634 }
635 return context{
636 state: stateError,
637 err: errorf(ErrNoSuchTemplate, node, line, "no such template %q", name),
638 }, dname
639 }
640 if dname != name {
641
642
643 dt := e.template(dname)
644 if dt == nil {
645 dt = template.New(dname)
646 dt.Tree = &parse.Tree{Name: dname, Root: t.Root.CopyList()}
647 e.derived[dname] = dt
648 }
649 t = dt
650 }
651 return e.computeOutCtx(c, t), dname
652 }
653
654
655
656 func (e *escaper) computeOutCtx(c context, t *template.Template) context {
657
658 c1, ok := e.escapeTemplateBody(c, t)
659 if !ok {
660
661 if c2, ok2 := e.escapeTemplateBody(c1, t); ok2 {
662 c1, ok = c2, true
663 }
664
665 }
666 if !ok && c1.state != stateError {
667 return context{
668 state: stateError,
669 err: errorf(ErrOutputContext, t.Tree.Root, 0, "cannot compute output context for template %s", t.Name()),
670 }
671 }
672 return c1
673 }
674
675
676
677
678 func (e *escaper) escapeTemplateBody(c context, t *template.Template) (context, bool) {
679 filter := func(e1 *escaper, c1 context) bool {
680 if c1.state == stateError {
681
682 return false
683 }
684 if !e1.called[t.Name()] {
685
686
687 return true
688 }
689
690 return c.eq(c1)
691 }
692
693
694
695
696 e.output[t.Name()] = c
697 return e.escapeListConditionally(c, t.Tree.Root, filter)
698 }
699
700
701 var delimEnds = [...]string{
702 delimDoubleQuote: `"`,
703 delimSingleQuote: "'",
704
705
706
707
708
709
710
711 delimSpaceOrTagEnd: " \t\n\f\r>",
712 }
713
714 var doctypeBytes = []byte("<!DOCTYPE")
715
716
717 func (e *escaper) escapeText(c context, n *parse.TextNode) context {
718 s, written, i, b := n.Text, 0, 0, new(bytes.Buffer)
719 for i != len(s) {
720 c1, nread := contextAfterText(c, s[i:])
721 i1 := i + nread
722 if c.state == stateText || c.state == stateRCDATA {
723 end := i1
724 if c1.state != c.state {
725 for j := end - 1; j >= i; j-- {
726 if s[j] == '<' {
727 end = j
728 break
729 }
730 }
731 }
732 for j := i; j < end; j++ {
733 if s[j] == '<' && !bytes.HasPrefix(bytes.ToUpper(s[j:]), doctypeBytes) {
734 b.Write(s[written:j])
735 b.WriteString("<")
736 written = j + 1
737 }
738 }
739 } else if isComment(c.state) && c.delim == delimNone {
740 switch c.state {
741 case stateJSBlockCmt:
742
743
744
745
746
747
748
749 if bytes.ContainsAny(s[written:i1], "\n\r\u2028\u2029") {
750 b.WriteByte('\n')
751 } else {
752 b.WriteByte(' ')
753 }
754 case stateCSSBlockCmt:
755 b.WriteByte(' ')
756 }
757 written = i1
758 }
759 if c.state != c1.state && isComment(c1.state) && c1.delim == delimNone {
760
761 cs := i1 - 2
762 if c1.state == stateHTMLCmt {
763
764 cs -= 2
765 }
766 b.Write(s[written:cs])
767 written = i1
768 }
769 if i == i1 && c.state == c1.state {
770 panic(fmt.Sprintf("infinite loop from %v to %v on %q..%q", c, c1, s[:i], s[i:]))
771 }
772 c, i = c1, i1
773 }
774
775 if written != 0 && c.state != stateError {
776 if !isComment(c.state) || c.delim != delimNone {
777 b.Write(n.Text[written:])
778 }
779 e.editTextNode(n, b.Bytes())
780 }
781 return c
782 }
783
784
785
786 func contextAfterText(c context, s []byte) (context, int) {
787 if c.delim == delimNone {
788 c1, i := tSpecialTagEnd(c, s)
789 if i == 0 {
790
791
792 return c1, 0
793 }
794
795 return transitionFunc[c.state](c, s[:i])
796 }
797
798
799
800 i := bytes.IndexAny(s, delimEnds[c.delim])
801 if i == -1 {
802 i = len(s)
803 }
804 if c.delim == delimSpaceOrTagEnd {
805
806
807
808
809
810
811
812 if j := bytes.IndexAny(s[:i], "\"'<=`"); j >= 0 {
813 return context{
814 state: stateError,
815 err: errorf(ErrBadHTML, nil, 0, "%q in unquoted attr: %q", s[j:j+1], s[:i]),
816 }, len(s)
817 }
818 }
819 if i == len(s) {
820
821
822
823
824 for u := []byte(html.UnescapeString(string(s))); len(u) != 0; {
825 c1, i1 := transitionFunc[c.state](c, u)
826 c, u = c1, u[i1:]
827 }
828 return c, len(s)
829 }
830
831 element := c.element
832
833
834 if c.state == stateAttr && c.element == elementScript && c.attr == attrScriptType && !isJSType(string(s[:i])) {
835 element = elementNone
836 }
837
838 if c.delim != delimSpaceOrTagEnd {
839
840 i++
841 }
842
843
844 return context{state: stateTag, element: element}, i
845 }
846
847
848 func (e *escaper) editActionNode(n *parse.ActionNode, cmds []string) {
849 if _, ok := e.actionNodeEdits[n]; ok {
850 panic(fmt.Sprintf("node %s shared between templates", n))
851 }
852 e.actionNodeEdits[n] = cmds
853 }
854
855
856 func (e *escaper) editTemplateNode(n *parse.TemplateNode, callee string) {
857 if _, ok := e.templateNodeEdits[n]; ok {
858 panic(fmt.Sprintf("node %s shared between templates", n))
859 }
860 e.templateNodeEdits[n] = callee
861 }
862
863
864 func (e *escaper) editTextNode(n *parse.TextNode, text []byte) {
865 if _, ok := e.textNodeEdits[n]; ok {
866 panic(fmt.Sprintf("node %s shared between templates", n))
867 }
868 e.textNodeEdits[n] = text
869 }
870
871
872
873 func (e *escaper) commit() {
874 for name := range e.output {
875 e.template(name).Funcs(funcMap)
876 }
877
878
879 tmpl := e.arbitraryTemplate()
880 for _, t := range e.derived {
881 if _, err := tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil {
882 panic("error adding derived template")
883 }
884 }
885 for n, s := range e.actionNodeEdits {
886 ensurePipelineContains(n.Pipe, s)
887 }
888 for n, name := range e.templateNodeEdits {
889 n.Name = name
890 }
891 for n, s := range e.textNodeEdits {
892 n.Text = s
893 }
894
895
896 e.called = make(map[string]bool)
897 e.actionNodeEdits = make(map[*parse.ActionNode][]string)
898 e.templateNodeEdits = make(map[*parse.TemplateNode]string)
899 e.textNodeEdits = make(map[*parse.TextNode][]byte)
900 }
901
902
903 func (e *escaper) template(name string) *template.Template {
904
905
906 t := e.arbitraryTemplate().text.Lookup(name)
907 if t == nil {
908 t = e.derived[name]
909 }
910 return t
911 }
912
913
914
915 func (e *escaper) arbitraryTemplate() *Template {
916 for _, t := range e.ns.set {
917 return t
918 }
919 panic("no templates in name space")
920 }
921
922
923
924
925
926 func HTMLEscape(w io.Writer, b []byte) {
927 template.HTMLEscape(w, b)
928 }
929
930
931 func HTMLEscapeString(s string) string {
932 return template.HTMLEscapeString(s)
933 }
934
935
936
937 func HTMLEscaper(args ...any) string {
938 return template.HTMLEscaper(args...)
939 }
940
941
942 func JSEscape(w io.Writer, b []byte) {
943 template.JSEscape(w, b)
944 }
945
946
947 func JSEscapeString(s string) string {
948 return template.JSEscapeString(s)
949 }
950
951
952
953 func JSEscaper(args ...any) string {
954 return template.JSEscaper(args...)
955 }
956
957
958
959 func URLQueryEscaper(args ...any) string {
960 return template.URLQueryEscaper(args...)
961 }
962
View as plain text