scripttemplate.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package templ
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "strings"
  8. )
  9. // ComponentScript is a templ Script template.
  10. type ComponentScript struct {
  11. // Name of the script, e.g. print.
  12. Name string
  13. // Function to render.
  14. Function string
  15. // Call of the function in JavaScript syntax, including parameters, and
  16. // ensures parameters are HTML escaped; useful for injecting into HTML
  17. // attributes like onclick, onhover, etc.
  18. //
  19. // Given:
  20. // functionName("some string",12345)
  21. // It would render:
  22. // __templ_functionName_sha("some string",12345))
  23. //
  24. // This is can be injected into HTML attributes:
  25. // <button onClick="__templ_functionName_sha(&#34;some string&#34;,12345))">Click Me</button>
  26. Call string
  27. // Call of the function in JavaScript syntax, including parameters. It
  28. // does not HTML escape parameters; useful for directly calling in script
  29. // elements.
  30. //
  31. // Given:
  32. // functionName("some string",12345)
  33. // It would render:
  34. // __templ_functionName_sha("some string",12345))
  35. //
  36. // This is can be used to call the function inside a script tag:
  37. // <script>__templ_functionName_sha("some string",12345))</script>
  38. CallInline string
  39. }
  40. var _ Component = ComponentScript{}
  41. func writeScriptHeader(ctx context.Context, w io.Writer) (err error) {
  42. var nonceAttr string
  43. if nonce := GetNonce(ctx); nonce != "" {
  44. nonceAttr = " nonce=\"" + EscapeString(nonce) + "\""
  45. }
  46. _, err = fmt.Fprintf(w, `<script type="text/javascript"%s>`, nonceAttr)
  47. return err
  48. }
  49. func (c ComponentScript) Render(ctx context.Context, w io.Writer) error {
  50. err := RenderScriptItems(ctx, w, c)
  51. if err != nil {
  52. return err
  53. }
  54. if len(c.Call) > 0 {
  55. if err = writeScriptHeader(ctx, w); err != nil {
  56. return err
  57. }
  58. if _, err = io.WriteString(w, c.CallInline); err != nil {
  59. return err
  60. }
  61. if _, err = io.WriteString(w, `</script>`); err != nil {
  62. return err
  63. }
  64. }
  65. return nil
  66. }
  67. // RenderScriptItems renders a <script> element, if the script has not already been rendered.
  68. func RenderScriptItems(ctx context.Context, w io.Writer, scripts ...ComponentScript) (err error) {
  69. if len(scripts) == 0 {
  70. return nil
  71. }
  72. _, v := getContext(ctx)
  73. sb := new(strings.Builder)
  74. for _, s := range scripts {
  75. if !v.hasScriptBeenRendered(s.Name) {
  76. sb.WriteString(s.Function)
  77. v.addScript(s.Name)
  78. }
  79. }
  80. if sb.Len() > 0 {
  81. if err = writeScriptHeader(ctx, w); err != nil {
  82. return err
  83. }
  84. if _, err = io.WriteString(w, sb.String()); err != nil {
  85. return err
  86. }
  87. if _, err = io.WriteString(w, `</script>`); err != nil {
  88. return err
  89. }
  90. }
  91. return nil
  92. }
  93. // JSExpression represents a JavaScript expression intended for use as an argument for script templates.
  94. // The string value of JSExpression will be inserted directly as JavaScript code in function call arguments.
  95. type JSExpression string
  96. // SafeScript encodes unknown parameters for safety for inside HTML attributes.
  97. func SafeScript(functionName string, params ...any) string {
  98. encodedParams := safeEncodeScriptParams(true, params)
  99. sb := new(strings.Builder)
  100. sb.WriteString(functionName)
  101. sb.WriteRune('(')
  102. sb.WriteString(strings.Join(encodedParams, ","))
  103. sb.WriteRune(')')
  104. return sb.String()
  105. }
  106. // SafeScript encodes unknown parameters for safety for inline scripts.
  107. func SafeScriptInline(functionName string, params ...any) string {
  108. encodedParams := safeEncodeScriptParams(false, params)
  109. sb := new(strings.Builder)
  110. sb.WriteString(functionName)
  111. sb.WriteRune('(')
  112. sb.WriteString(strings.Join(encodedParams, ","))
  113. sb.WriteRune(')')
  114. return sb.String()
  115. }
  116. func safeEncodeScriptParams(escapeHTML bool, params []any) []string {
  117. encodedParams := make([]string, len(params))
  118. for i := 0; i < len(encodedParams); i++ {
  119. if val, ok := params[i].(JSExpression); ok {
  120. encodedParams[i] = string(val)
  121. continue
  122. }
  123. enc, _ := json.Marshal(params[i])
  124. if !escapeHTML {
  125. encodedParams[i] = string(enc)
  126. continue
  127. }
  128. encodedParams[i] = EscapeString(string(enc))
  129. }
  130. return encodedParams
  131. }