From ab6811b93f5a8e81a6b14600edb45527d86c7e92 Mon Sep 17 00:00:00 2001 From: ross96D Date: Sat, 11 Nov 2023 10:55:06 -0500 Subject: [PATCH 1/9] fix: remove escaping for ConstantAttribute (#293) --- parser/v2/types.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/parser/v2/types.go b/parser/v2/types.go index bd0ecfd7a..11ca09788 100644 --- a/parser/v2/types.go +++ b/parser/v2/types.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "go/format" - "html" "io" "strings" "unicode" @@ -665,7 +664,7 @@ type ConstantAttribute struct { } func (ca ConstantAttribute) String() string { - return ca.Name + `="` + html.EscapeString(ca.Value) + `"` + return ca.Name + `="` + ca.Value + `"` } func (ca ConstantAttribute) Write(w io.Writer, indent int) error { From a0e0c6239a1cdbd5f275407ec8d56a2a735882d4 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Sat, 11 Nov 2023 10:04:19 +0000 Subject: [PATCH 2/9] feat: remove class name sanitization, since CSS class names are already HTML attribute escaped (#284) Fixes #211 --- .version | 2 +- .../10-css-style-management.md | 28 +- generator/test-css-usage/constants.go | 3 - generator/test-css-usage/expected.html | 31 +- generator/test-css-usage/render_test.go | 2 +- generator/test-css-usage/template.templ | 82 +++-- generator/test-css-usage/template_templ.go | 329 ++++++++++++------ runtime.go | 47 +-- runtime_test.go | 62 +--- 9 files changed, 315 insertions(+), 271 deletions(-) delete mode 100644 generator/test-css-usage/constants.go diff --git a/.version b/.version index 36cfcfb57..2dbed2514 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.461 \ No newline at end of file +0.2.455 \ No newline at end of file diff --git a/docs/docs/03-syntax-and-usage/10-css-style-management.md b/docs/docs/03-syntax-and-usage/10-css-style-management.md index cf83b128c..9241bafc0 100644 --- a/docs/docs/03-syntax-and-usage/10-css-style-management.md +++ b/docs/docs/03-syntax-and-usage/10-css-style-management.md @@ -2,7 +2,7 @@ ## HTML class attribute -templ supports the HTML `class` attribute. +The standard HTML `class` attribute can be added to components to set class names. ```templ templ button(text string) { @@ -38,38 +38,26 @@ templ button(text string, className string) { } ``` -## CSS class name sanitization - -CSS class names that are passed to the class expression attribute as variables are sanitized, since the Go string might come from an untrustworthy source such as user input. - -If the class name fails the sanitization check, it will be replaced with `--templ-css-class-safe-name`. - -If you know that the CSS class name is from a trustworthy source (e.g. a string constant under your control), you can bypass sanitization by marking the class name as safe with the `templ.SafeClass()` function. - -```templ title="component.templ" -package main - -templ button(text string) { - -} -``` - ### Dynamic class names Toggle addition of CSS classes to an element based on a boolean value by passing: * A `templ.KV` value containing the name of the class to add to the element, and a boolean that determines whether the class is added to the attribute at render time. * `templ.KV("is-primary", true)` - * `templ.KV(templ.SafeClass("hover:do_not_sanitize"), true)` + * `templ.KV("hover:red", true)` * A map of string class names to a boolean that determines if the class is added to the class attribute value at render time: * `map[string]bool` - * `map[CSSClass]bool` + * `map[CSSClass]bool` ```templ title="component.templ" package main +css red() { + background-color: #ff0000; +} + templ button(text string, isPrimary bool) { - + } ``` diff --git a/generator/test-css-usage/constants.go b/generator/test-css-usage/constants.go deleted file mode 100644 index 2e8a288dc..000000000 --- a/generator/test-css-usage/constants.go +++ /dev/null @@ -1,3 +0,0 @@ -package testcssusage - -const red = "#ff0000" diff --git a/generator/test-css-usage/expected.html b/generator/test-css-usage/expected.html index 2cb71a4a2..db8ab3b81 100644 --- a/generator/test-css-usage/expected.html +++ b/generator/test-css-usage/expected.html @@ -1,18 +1,17 @@ - - - - - -
-
+ .test { + color: #ff0000; + } + +
Style tags are supported
+ +
CSS components are supported
+
Both CSS components and constants are supported
+
Both CSS components and constants are supported
+
Maps can be used to determine if a class should be added or not.
- - - +
KV can be used to conditionally set classes.
+
Psuedo attributes and complex class names are supported.
+
+ Class names are HTML escaped. +
diff --git a/generator/test-css-usage/render_test.go b/generator/test-css-usage/render_test.go index 9796a31ff..342f692ad 100644 --- a/generator/test-css-usage/render_test.go +++ b/generator/test-css-usage/render_test.go @@ -11,7 +11,7 @@ import ( var expected string func Test(t *testing.T) { - component := ThreeButtons() + component := TestComponent() diff, err := htmldiff.Diff(component, expected) if err != nil { diff --git a/generator/test-css-usage/template.templ b/generator/test-css-usage/template.templ index 7bc413ab5..a71d0bfd0 100644 --- a/generator/test-css-usage/template.templ +++ b/generator/test-css-usage/template.templ @@ -1,14 +1,45 @@ package testcssusage -css green() { - color: #00ff00; +// Constant class. + +templ StyleTagsAreSupported() { + +
Style tags are supported
} -css className() { - background-color: #ffffff; +// CSS components. + +const red = "#00ff00" + +css cssComponentGreen() { color: { red }; } +templ CSSComponentsAreSupported() { +
CSS components are supported
+} + +// Both CSS components and constants are supported. +// Only string names are really required. There is no need to use templ.Class or templ.SafeClass. + +templ CSSComponentsAndConstantsAreSupported() { +
Both CSS components and constants are supported
+ // The following is also valid, but not required - you can put the class names in directly. +
Both CSS components and constants are supported
+} + +// Maps can be used to determine if a class should be added or not. + +templ MapsCanBeUsedToConditionallySetClasses() { +
Maps can be used to determine if a class should be added or not.
+} + +// The templ.KV function can be used to add a class if a condition is true. + css d() { font-size: 12pt; } @@ -17,37 +48,30 @@ css e() { font-size: 14pt; } -templ Button(text string) { - +templ KVCanBeUsedToConditionallySetClasses() { +
KV can be used to conditionally set classes.
} -templ LegacySupport() { -
-} +// Pseudo attributes can be used without any special syntax. -templ MapCSSExample() { -
+templ PsuedoAttributesAndComplexClassNamesAreSupported() { +
Psuedo attributes and complex class names are supported.
} -templ KVExample() { -
- -} +// Class names are HTML escaped. -templ PsuedoAttributes() { - +templ ClassNamesAreHTMLEscaped() { +
Class names are HTML escaped.
} -templ ThreeButtons() { - - {! Button("A") } - {! Button("B") } - - {! MapCSSExample() } - {! KVExample() } - {! PsuedoAttributes() } +// Combine all tests. + +templ TestComponent() { + @StyleTagsAreSupported() + @CSSComponentsAreSupported() + @CSSComponentsAndConstantsAreSupported() + @MapsCanBeUsedToConditionallySetClasses() + @KVCanBeUsedToConditionallySetClasses() + @PsuedoAttributesAndComplexClassNamesAreSupported() + @ClassNamesAreHTMLEscaped() } diff --git a/generator/test-css-usage/template_templ.go b/generator/test-css-usage/template_templ.go index c7bc89817..ded80499d 100644 --- a/generator/test-css-usage/template_templ.go +++ b/generator/test-css-usage/template_templ.go @@ -10,48 +10,9 @@ import "io" import "bytes" import "strings" -func green() templ.CSSClass { - var templ_7745c5c3_CSSBuilder strings.Builder - templ_7745c5c3_CSSBuilder.WriteString(`color:#00ff00;`) - templ_7745c5c3_CSSID := templ.CSSID(`green`, templ_7745c5c3_CSSBuilder.String()) - return templ.ComponentCSSClass{ - ID: templ_7745c5c3_CSSID, - Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), - } -} - -func className() templ.CSSClass { - var templ_7745c5c3_CSSBuilder strings.Builder - templ_7745c5c3_CSSBuilder.WriteString(`background-color:#ffffff;`) - templ_7745c5c3_CSSBuilder.WriteString(string(templ.SanitizeCSS(`color`, red))) - templ_7745c5c3_CSSID := templ.CSSID(`className`, templ_7745c5c3_CSSBuilder.String()) - return templ.ComponentCSSClass{ - ID: templ_7745c5c3_CSSID, - Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), - } -} - -func d() templ.CSSClass { - var templ_7745c5c3_CSSBuilder strings.Builder - templ_7745c5c3_CSSBuilder.WriteString(`font-size:12pt;`) - templ_7745c5c3_CSSID := templ.CSSID(`d`, templ_7745c5c3_CSSBuilder.String()) - return templ.ComponentCSSClass{ - ID: templ_7745c5c3_CSSID, - Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), - } -} - -func e() templ.CSSClass { - var templ_7745c5c3_CSSBuilder strings.Builder - templ_7745c5c3_CSSBuilder.WriteString(`font-size:14pt;`) - templ_7745c5c3_CSSID := templ.CSSID(`e`, templ_7745c5c3_CSSBuilder.String()) - return templ.ComponentCSSClass{ - ID: templ_7745c5c3_CSSID, - Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), - } -} +// Constant class. -func Button(text string) templ.Component { +func StyleTagsAreSupported() templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -64,29 +25,29 @@ func Button(text string) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var2 = []any{className(), templ.Class("&&&unsafe"), "safe", templ.SafeClass("safe2")} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -97,7 +58,21 @@ func Button(text string) templ.Component { }) } -func LegacySupport() templ.Component { +// CSS components. + +const red = "#00ff00" + +func cssComponentGreen() templ.CSSClass { + var templ_7745c5c3_CSSBuilder strings.Builder + templ_7745c5c3_CSSBuilder.WriteString(string(templ.SanitizeCSS(`color`, red))) + templ_7745c5c3_CSSID := templ.CSSID(`cssComponentGreen`, templ_7745c5c3_CSSBuilder.String()) + return templ.ComponentCSSClass{ + ID: templ_7745c5c3_CSSID, + Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), + } +} + +func CSSComponentsAreSupported() templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -110,7 +85,7 @@ func LegacySupport() templ.Component { templ_7745c5c3_Var4 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var5 = []any{templ.Classes(templ.Class("test"), "a")} + var templ_7745c5c3_Var5 = []any{cssComponentGreen()} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -123,7 +98,16 @@ func LegacySupport() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var6 := `CSS components are supported` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -134,7 +118,10 @@ func LegacySupport() templ.Component { }) } -func MapCSSExample() templ.Component { +// Both CSS components and constants are supported. +// Only string names are really required. There is no need to use templ.Class or templ.SafeClass. + +func CSSComponentsAndConstantsAreSupported() templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -142,13 +129,13 @@ func MapCSSExample() templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var6 := templ.GetChildren(ctx) - if templ_7745c5c3_Var6 == nil { - templ_7745c5c3_Var6 = templ.NopComponent + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var7 = []any{map[string]bool{"a": true, "b": false, "c": true}} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + var templ_7745c5c3_Var8 = []any{cssComponentGreen(), "classA", templ.Class("&&&classB"), templ.SafeClass("classC"), "d e"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -156,11 +143,46 @@ func MapCSSExample() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var7).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var8).String())) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" type=\"button\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var9 := `Both CSS components and constants are supported` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 = []any{templ.Classes(cssComponentGreen(), "classA", templ.Class("&&&classB"), templ.SafeClass("classC")), "d e"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var11 := `Both CSS components and constants are supported` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -171,7 +193,9 @@ func MapCSSExample() templ.Component { }) } -func KVExample() templ.Component { +// Maps can be used to determine if a class should be added or not. + +func MapsCanBeUsedToConditionallySetClasses() templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -179,13 +203,13 @@ func KVExample() templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var9 = []any{"a", templ.KV("b", false)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) + var templ_7745c5c3_Var13 = []any{map[string]bool{"a": true, "b": false, "c": true}} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -193,28 +217,88 @@ func KVExample() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var9).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var13).String())) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 = []any{"a", "b", "c", templ.KV("c", false), templ.KV(d(), false), templ.KV(e(), true)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) + templ_7745c5c3_Var14 := `Maps can be used to determine if a class should be added or not.` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var10).String())) + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +// The templ.KV function can be used to add a class if a condition is true. + +func d() templ.CSSClass { + var templ_7745c5c3_CSSBuilder strings.Builder + templ_7745c5c3_CSSBuilder.WriteString(`font-size:12pt;`) + templ_7745c5c3_CSSID := templ.CSSID(`d`, templ_7745c5c3_CSSBuilder.String()) + return templ.ComponentCSSClass{ + ID: templ_7745c5c3_CSSID, + Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), + } +} + +func e() templ.CSSClass { + var templ_7745c5c3_CSSBuilder strings.Builder + templ_7745c5c3_CSSBuilder.WriteString(`font-size:14pt;`) + templ_7745c5c3_CSSID := templ.CSSID(`e`, templ_7745c5c3_CSSBuilder.String()) + return templ.ComponentCSSClass{ + ID: templ_7745c5c3_CSSID, + Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`), + } +} + +func KVCanBeUsedToConditionallySetClasses() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var16 = []any{"a", templ.KV("b", false), "c", templ.KV(d(), false), templ.KV(e(), true)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" placeholder=\"your@email.com\" autocomplete=\"off\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var17 := `KV can be used to conditionally set classes.` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -225,7 +309,9 @@ func KVExample() templ.Component { }) } -func PsuedoAttributes() templ.Component { +// Pseudo attributes can be used without any special syntax. + +func PsuedoAttributesAndComplexClassNamesAreSupported() templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -233,21 +319,21 @@ func PsuedoAttributes() templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var11 := templ.GetChildren(ctx) - if templ_7745c5c3_Var11 == nil { - templ_7745c5c3_Var11 = templ.NopComponent + templ_7745c5c3_Var18 := templ.GetChildren(ctx) + if templ_7745c5c3_Var18 == nil { + templ_7745c5c3_Var18 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var12 = []any{"bg-violet-500", templ.KV(templ.SafeClass("hover:bg-violet-600"), true)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) + var templ_7745c5c3_Var19 = []any{"bg-violet-500", "hover:bg-red-600", "hover:bg-sky-700", "text-[#50d71e]", "w-[calc(100%-4rem)"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + templ_7745c5c3_Err = MapsCanBeUsedToConditionallySetClasses().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = MapCSSExample().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = KVCanBeUsedToConditionallySetClasses().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = KVExample().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = PsuedoAttributesAndComplexClassNamesAreSupported().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = PsuedoAttributes().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = ClassNamesAreHTMLEscaped().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/runtime.go b/runtime.go index 5db606eaa..75a694cc1 100644 --- a/runtime.go +++ b/runtime.go @@ -10,7 +10,6 @@ import ( "html" "io" "net/http" - "regexp" "sort" "strings" "sync" @@ -164,14 +163,14 @@ func (cp *cssProcessor) Add(item any) { switch c := item.(type) { case []string: for _, className := range c { - cp.AddUnsanitized(className, true) + cp.AddClassName(className, true) } case string: - cp.AddUnsanitized(c, true) + cp.AddClassName(c, true) case ConstantCSSClass: - cp.AddSanitized(c.ClassName(), true) + cp.AddClassName(c.ClassName(), true) case ComponentCSSClass: - cp.AddSanitized(c.ClassName(), true) + cp.AddClassName(c.ClassName(), true) case map[string]bool: // In Go, map keys are iterated in a randomized order. // So the keys in the map must be sorted to produce consistent output. @@ -183,43 +182,32 @@ func (cp *cssProcessor) Add(item any) { } sort.Strings(keys) for _, className := range keys { - cp.AddUnsanitized(className, c[className]) + cp.AddClassName(className, c[className]) } case []KeyValue[string, bool]: for _, kv := range c { - cp.AddUnsanitized(kv.Key, kv.Value) + cp.AddClassName(kv.Key, kv.Value) } case KeyValue[string, bool]: - cp.AddUnsanitized(c.Key, c.Value) + cp.AddClassName(c.Key, c.Value) case []KeyValue[CSSClass, bool]: for _, kv := range c { - cp.AddSanitized(kv.Key.ClassName(), kv.Value) + cp.AddClassName(kv.Key.ClassName(), kv.Value) } case KeyValue[CSSClass, bool]: - cp.AddSanitized(c.Key.ClassName(), c.Value) + cp.AddClassName(c.Key.ClassName(), c.Value) case CSSClasses: for _, item := range c { cp.Add(item) } case func() CSSClass: - cp.AddSanitized(c().ClassName(), true) + cp.AddClassName(c().ClassName(), true) default: - cp.AddSanitized(unknownTypeClassName, true) + cp.AddClassName(unknownTypeClassName, true) } } -func (cp *cssProcessor) AddUnsanitized(className string, enabled bool) { - for _, className := range strings.Split(className, " ") { - className = strings.TrimSpace(className) - if isSafe := safeClassName.MatchString(className); !isSafe { - className = fallbackClassName - enabled = true // Always display the fallback classname. - } - cp.AddSanitized(className, enabled) - } -} - -func (cp *cssProcessor) AddSanitized(className string, enabled bool) { +func (cp *cssProcessor) AddClassName(className string, enabled bool) { cp.classNameToEnabled[className] = enabled cp.orderedNames = append(cp.orderedNames, className) } @@ -256,20 +244,16 @@ func KV[TKey comparable, TValue any](key TKey, value TValue) KeyValue[TKey, TVal } } -var safeClassName = regexp.MustCompile(`^-?[_a-zA-Z]+[-_a-zA-Z0-9]*$`) - -const fallbackClassName = "--templ-css-class-safe-name" const unknownTypeClassName = "--templ-css-class-unknown-type" -// Class returns a sanitized CSS class name. +// Class returns a CSS class name. +// Deprecated: use a string instead. func Class(name string) CSSClass { - if !safeClassName.MatchString(name) { - return SafeClass(fallbackClassName) - } return SafeClass(name) } // SafeClass bypasses CSS class name validation. +// Deprecated: use a string instead. func SafeClass(name string) CSSClass { return ConstantCSSClass(name) } @@ -280,6 +264,7 @@ type CSSClass interface { } // ConstantCSSClass is a string constant of a CSS class name. +// Deprecated: use a string instead. type ConstantCSSClass string // ClassName of the CSS class. diff --git a/runtime_test.go b/runtime_test.go index b5b04cef4..2277d7985 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -244,43 +244,6 @@ func TestRenderCSS(t *testing.T) { } } -func TestClassSanitization(t *testing.T) { - tests := []struct { - input string - expected string - }{ - { - input: `safe`, - expected: `safe`, - }, - { - input: `safe-name`, - expected: "safe-name", - }, - { - input: `safe_name`, - expected: "safe_name", - }, - { - input: `!unsafe`, - expected: "--templ-css-class-safe-name", - }, - { - input: ``, - expected: "--templ-css-class-safe-name", - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.input, func(t *testing.T) { - actual := templ.Class(tt.input) - if actual.ClassName() != tt.expected { - t.Errorf("expected %q, got %q", tt.expected, actual.ClassName()) - } - }) - } -} - func TestClassesFunction(t *testing.T) { tests := []struct { name string @@ -288,14 +251,9 @@ func TestClassesFunction(t *testing.T) { expected string }{ { - name: "safe constants are allowed", - input: []any{"a", "b", "c"}, - expected: "a b c", - }, - { - name: "unsafe constants are filtered", - input: []any{"", "b", ""}, - expected: "--templ-css-class-safe-name b", + name: "constants are allowed", + input: []any{"a", "b", "c", ""}, + expected: "a b c ", }, { name: "legacy CSS types are supported", @@ -341,25 +299,17 @@ func TestClassesFunction(t *testing.T) { { name: "string arrays are supported", input: []any{ - []string{"a", "b", "c"}, - "d", - }, - expected: "a b c d", - }, - { - name: "string arrays are checked for unsafe class names", - input: []any{ - []string{"a", "b", "c "}, + []string{"a", "b", "c", ""}, "d", }, - expected: "a b c --templ-css-class-safe-name d", + expected: "a b c d", }, { name: "strings are broken up", input: []any{ "a ", }, - expected: "a --templ-css-class-safe-name", + expected: "a ", }, { name: "if a templ.CSSClasses is passed in, the nested CSSClasses are extracted", From ca6b438f1e9c17def4aea121948fdfd2f5ecc838 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Sat, 11 Nov 2023 10:05:04 +0000 Subject: [PATCH 3/9] chore: bump version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index 2dbed2514..98fea9c6c 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.455 \ No newline at end of file +0.2.464 \ No newline at end of file From f051a271e3149df252c313b3855dd050b167782a Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Sun, 12 Nov 2023 16:08:01 +0000 Subject: [PATCH 4/9] fix: remove incorrect document highlighting, fixes #283 --- .version | 2 +- cmd/templ/lspcmd/proxy/server.go | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/.version b/.version index 98fea9c6c..74d3cc8ed 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.464 \ No newline at end of file +0.2.465 \ No newline at end of file diff --git a/cmd/templ/lspcmd/proxy/server.go b/cmd/templ/lspcmd/proxy/server.go index 8fc746368..73a0a449e 100644 --- a/cmd/templ/lspcmd/proxy/server.go +++ b/cmd/templ/lspcmd/proxy/server.go @@ -624,22 +624,6 @@ func (p *Server) DocumentColor(ctx context.Context, params *lsp.DocumentColorPar func (p *Server) DocumentHighlight(ctx context.Context, params *lsp.DocumentHighlightParams) (result []lsp.DocumentHighlight, err error) { p.Log.Info("client -> server: DocumentHighlight") defer p.Log.Info("client -> server: DocumentHighlight end") - isTemplFile, goURI := convertTemplToGoURI(params.TextDocument.URI) - if !isTemplFile { - return p.Target.DocumentHighlight(ctx, params) - } - templURI := params.TextDocument.URI - params.TextDocument.URI = goURI - result, err = p.Target.DocumentHighlight(ctx, params) - if err != nil { - return - } - if result == nil { - return - } - for i := 0; i < len(result); i++ { - result[i].Range = p.convertGoRangeToTemplRange(templURI, result[i].Range) - } return } From b8298fe2dcaf4302225ddf0d0bd99483891cbc6e Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Sun, 12 Nov 2023 17:05:57 +0000 Subject: [PATCH 5/9] docs: document view model usage and testing --- docs/docs/04-core-concepts/03-testing.md | 99 ++++++++++++++++++++ docs/docs/04-core-concepts/04-view-models.md | 49 ++++++++++ 2 files changed, 148 insertions(+) create mode 100644 docs/docs/04-core-concepts/03-testing.md create mode 100644 docs/docs/04-core-concepts/04-view-models.md diff --git a/docs/docs/04-core-concepts/03-testing.md b/docs/docs/04-core-concepts/03-testing.md new file mode 100644 index 000000000..e13b3a518 --- /dev/null +++ b/docs/docs/04-core-concepts/03-testing.md @@ -0,0 +1,99 @@ +# Testing + +To test that data is rendered as expected, there are two main ways to do it: + +* Expectation testing - testing that specific expectations are met by the output. +* Snapshot testing - testing that outputs match a pre-written output. + +## Expectation testing + +Expectation validates that data appears in the output in the right format, and position. + +The example at https://github.com/a-h/templ/blob/main/examples/blog/posts_test.go uses the `goquery` library to make assertions on the HTML. + +```go +func TestPosts(t *testing.T) { + testPosts := []Post{ + {Name: "Name1", Author: "Author1"}, + {Name: "Name2", Author: "Author2"}, + } + r, w := io.Pipe() + go func() { + posts(testPosts).Render(context.Background(), w) + w.Close() + }() + doc, err := goquery.NewDocumentFromReader(r) + if err != nil { + t.Fatalf("failed to read template: %v", err) + } + // Assert. + // Expect the page title to be set correctly. + expectedTitle := "Posts" + if actualTitle := doc.Find("title").Text(); actualTitle != expectedTitle { + t.Errorf("expected title name %q, got %q", expectedTitle, actualTitle) + } + // Expect the header to be rendered. + if doc.Find(`[data-testid="headerTemplate"]`).Length() == 0 { + t.Error("expected data-testid attribute to be rendered, but it wasn't") + } + // Expect the navigation to be rendered. + if doc.Find(`[data-testid="navTemplate"]`).Length() == 0 { + t.Error("expected nav to be rendered, but it wasn't") + } + // Expect the posts to be rendered. + if doc.Find(`[data-testid="postsTemplate"]`).Length() == 0 { + t.Error("expected posts to be rendered, but it wasn't") + } + // Expect both posts to be rendered. + if actualPostCount := doc.Find(`[data-testid="postsTemplatePost"]`).Length(); actualPostCount != len(testPosts) { + t.Fatalf("expected %d posts to be rendered, found %d", len(testPosts), actualPostCount) + } + // Expect the posts to contain the author name. + doc.Find(`[data-testid="postsTemplatePost"]`).Each(func(index int, sel *goquery.Selection) { + expectedName := testPosts[index].Name + if actualName := sel.Find(`[data-testid="postsTemplatePostName"]`).Text(); actualName != expectedName { + t.Errorf("expected name %q, got %q", actualName, expectedName) + } + expectedAuthor := testPosts[index].Author + if actualAuthor := sel.Find(`[data-testid="postsTemplatePostAuthor"]`).Text(); actualAuthor != expectedAuthor { + t.Errorf("expected author %q, got %q", actualAuthor, expectedAuthor) + } + }) +} +``` + +## Snapshot testing + +Snapshot testing is a more broad check. It simply checks that the output hasn't changed since the last time you took a copy of the output. + +It relies on manually checking the output to make sure it's correct, and then "locking it in" by using the snapshot. + +templ uses this strategy to check for regressions in behaviour between releases, as per https://github.com/a-h/templ/blob/main/generator/test-html-comment/render_test.go + +To make it easier to compare the output against the expected HTML, templ uses a HTML formatting library before executing the diff. + +```go +package testcomment + +import ( + _ "embed" + "testing" + + "github.com/a-h/templ/generator/htmldiff" +) + +//go:embed expected.html +var expected string + +func Test(t *testing.T) { + component := render("sample content") + + diff, err := htmldiff.Diff(component, expected) + if err != nil { + t.Fatal(err) + } + if diff != "" { + t.Error(diff) + } +} +``` diff --git a/docs/docs/04-core-concepts/04-view-models.md b/docs/docs/04-core-concepts/04-view-models.md new file mode 100644 index 000000000..28617bde0 --- /dev/null +++ b/docs/docs/04-core-concepts/04-view-models.md @@ -0,0 +1,49 @@ +# View models + +With templ, you can pass any Go type into your template as parameters, and you can call arbitrary functions. + +However, if the parameters of your template don't closely map to what you're displaying to users, you may find yourself calling a lot of functions within your templ files to reshape or adjust data, or to carry out complex repeated string interpolation or URL constructions. + +This can make template rendering hard to test, because you need to set up complex data structures in the right way in order to render the HTML. If the template calls APIs or accesses databases from within the templates, it's even harder to test, because then testing your templates becomes an integration test. + +A more reliable approach can be to create a "View model" that only contains the fields that you intend to display, and where the data structure closely matches the structure of the visual layout. + +```go +package invitesget + +type Handler struct { + Invites *InviteService +} + +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + invites, err := h.Invites.Get(getUserIDFromContext(r.Context())) + if err != nil { + //TODO: Log error server side. + } + m := NewInviteComponentViewModel(invites, err) + teamInviteComponentModel(m).Render(r.Context(), w) +} + +func NewInviteComponentViewModel(invites []models.Invite, err error) (m InviteComponentViewModel) { + m.InviteCount = len(invites) + if err != nil { + m.ErrorMessage = "Failed to load invites, please try again" + } + return m +} + + +type InviteComponentViewModel struct { + InviteCount int + ErrorMessage string +} + +templ teamInviteComponent(model InviteComponentViewModel) { + if model.InviteCount > 0 { +
You have { fmt.Sprintf("%d", model.InviteCount) } pending invites
+ } + if model.ErrorMessage != "" { +
{ model.ErrorMessage }
+ } +} +``` From 8f35e13c4259fd936bddaf3fe6f57391e1a6321f Mon Sep 17 00:00:00 2001 From: Justin Derby Date: Mon, 13 Nov 2023 00:21:03 -0800 Subject: [PATCH 6/9] feat: add ability to call script components inline (#285) --- .../visualize/sourcemapvisualisation_templ.go | 6 +- .../11-script-templates.md | 64 ++++++++++++++++- .../external-libraries/components_templ.go | 3 +- generator/generator.go | 4 ++ generator/test-script-inline/expected.html | 18 +++++ generator/test-script-inline/render_test.go | 23 ++++++ generator/test-script-inline/template.templ | 17 +++++ .../test-script-inline/template_templ.go | 64 +++++++++++++++++ generator/test-script-usage/template_templ.go | 28 ++++---- runtime.go | 70 +++++++++++++++++-- 10 files changed, 277 insertions(+), 20 deletions(-) create mode 100644 generator/test-script-inline/expected.html create mode 100644 generator/test-script-inline/render_test.go create mode 100644 generator/test-script-inline/template.templ create mode 100644 generator/test-script-inline/template_templ.go diff --git a/cmd/templ/visualize/sourcemapvisualisation_templ.go b/cmd/templ/visualize/sourcemapvisualisation_templ.go index 4aa38e8f8..d63b053cf 100644 --- a/cmd/templ/visualize/sourcemapvisualisation_templ.go +++ b/cmd/templ/visualize/sourcemapvisualisation_templ.go @@ -179,7 +179,8 @@ func highlight(sourceId, targetId string) templ.ComponentScript { for(let i = 0; i < items.length; i ++) { items[i].classList.add("highlighted"); }}`, - Call: templ.SafeScript(`__templ_highlight_ae80`, sourceId, targetId), + Call: templ.SafeScript(`__templ_highlight_ae80`, sourceId, targetId), + CallInline: templ.SafeScriptInline(`__templ_highlight_ae80`, sourceId, targetId), } } @@ -194,7 +195,8 @@ func removeHighlight(sourceId, targetId string) templ.ComponentScript { for(let i = 0; i < items.length; i ++) { items[i].classList.remove("highlighted"); }}`, - Call: templ.SafeScript(`__templ_removeHighlight_58f2`, sourceId, targetId), + Call: templ.SafeScript(`__templ_removeHighlight_58f2`, sourceId, targetId), + CallInline: templ.SafeScriptInline(`__templ_removeHighlight_58f2`, sourceId, targetId), } } diff --git a/docs/docs/03-syntax-and-usage/11-script-templates.md b/docs/docs/03-syntax-and-usage/11-script-templates.md index 892564d3b..25694c7ca 100644 --- a/docs/docs/03-syntax-and-usage/11-script-templates.md +++ b/docs/docs/03-syntax-and-usage/11-script-templates.md @@ -62,7 +62,7 @@ templ page(data []TimeValue) { The data is loaded by the backend into the template. This example uses a constant, but it could easily have collected the `[]TimeValue` from a database. -```go +```go title="main.go" package main import ( @@ -103,3 +103,65 @@ func main() { } } ``` + +`script` elements are templ Components, so you can also directly render the Javascript function, passing in Go data, using the `@` expression: + +```templ +package main + +import "fmt" + +script printToConsole(content string) { + console.log(content) +} + +templ page(content string) { + + + @printToConsole(content) + @printToConsole(fmt.Sprintf("Again: %s", content)) + + +} +``` + +The data passed into the Javascript funtion will be JSON encoded, which then can be used inside the function. + +```go title="main.go" +package main + +import ( + "fmt" + "log" + "net/http" + "time" +) + +func main() { + mux := http.NewServeMux() + + // Handle template. + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Format the current time and pass it into our template + page(time.Now().String()).Render(r.Context(), w) + }) + + // Start the server. + fmt.Println("listening on :8080") + if err := http.ListenAndServe(":8080", mux); err != nil { + log.Printf("error listening: %v", err) + } +} +``` + +After building and running the executable, running `curl http://localhost:8080/` would render: + +```html title="Output" + + + + + + + +``` diff --git a/examples/external-libraries/components_templ.go b/examples/external-libraries/components_templ.go index a23e13d4c..fc80ecf07 100644 --- a/examples/external-libraries/components_templ.go +++ b/examples/external-libraries/components_templ.go @@ -15,7 +15,8 @@ func graph(data []TimeValue) templ.ComponentScript { Function: `function __templ_graph_c2ba(data){const chart = LightweightCharts.createChart(document.body, { width: 400, height: 300 }); const lineSeries = chart.addLineSeries(); lineSeries.setData(data);}`, - Call: templ.SafeScript(`__templ_graph_c2ba`, data), + Call: templ.SafeScript(`__templ_graph_c2ba`, data), + CallInline: templ.SafeScriptInline(`__templ_graph_c2ba`, data), } } diff --git a/generator/generator.go b/generator/generator.go index bbbeb2877..faf7acfbc 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -1355,6 +1355,10 @@ func (g *generator) writeScript(t parser.ScriptTemplate) error { if _, err = g.w.WriteIndent(indentLevel, "Call: templ.SafeScript("+goFn+", "+stripTypes(t.Parameters.Value)+"),\n"); err != nil { return err } + // CallInline: templ.SafeScriptInline(scriptName, a, b, c) + if _, err = g.w.WriteIndent(indentLevel, "CallInline: templ.SafeScriptInline("+goFn+", "+stripTypes(t.Parameters.Value)+"),\n"); err != nil { + return err + } indentLevel-- } // } diff --git a/generator/test-script-inline/expected.html b/generator/test-script-inline/expected.html new file mode 100644 index 000000000..99c37df34 --- /dev/null +++ b/generator/test-script-inline/expected.html @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/generator/test-script-inline/render_test.go b/generator/test-script-inline/render_test.go new file mode 100644 index 000000000..0507e448c --- /dev/null +++ b/generator/test-script-inline/render_test.go @@ -0,0 +1,23 @@ +package testscriptinline + +import ( + _ "embed" + "testing" + + "github.com/a-h/templ/generator/htmldiff" +) + +//go:embed expected.html +var expected string + +func Test(t *testing.T) { + component := InlineJavascript("injected") + + diff, err := htmldiff.Diff(component, expected) + if err != nil { + t.Fatal(err) + } + if diff != "" { + t.Error(diff) + } +} diff --git a/generator/test-script-inline/template.templ b/generator/test-script-inline/template.templ new file mode 100644 index 000000000..f85b35726 --- /dev/null +++ b/generator/test-script-inline/template.templ @@ -0,0 +1,17 @@ +package testscriptinline + +script withParameters(a string, b string, c int) { + console.log(a, b, c); +} + +script withoutParameters() { + alert("hello"); +} + +templ InlineJavascript(a string) { + @withoutParameters() + @withParameters(a, "test", 123) + // Call once more, to ensure it's defined only once + @withoutParameters() + @withParameters(a, "test", 123) +} diff --git a/generator/test-script-inline/template_templ.go b/generator/test-script-inline/template_templ.go new file mode 100644 index 000000000..54b9d93f5 --- /dev/null +++ b/generator/test-script-inline/template_templ.go @@ -0,0 +1,64 @@ +// Code generated by templ - DO NOT EDIT. + +package testscriptinline + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func withParameters(a string, b string, c int) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withParameters_1056`, + Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}`, + Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + CallInline: templ.SafeScriptInline(`__templ_withParameters_1056`, a, b, c), + } +} + +func withoutParameters() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withoutParameters_6bbf`, + Function: `function __templ_withoutParameters_6bbf(){alert("hello");}`, + Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + CallInline: templ.SafeScriptInline(`__templ_withoutParameters_6bbf`), + } +} + +func InlineJavascript(a string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = withoutParameters().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = withParameters(a, "test", 123).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = withoutParameters().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = withParameters(a, "test", 123).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/generator/test-script-usage/template_templ.go b/generator/test-script-usage/template_templ.go index 37c43f831..c8be34d3a 100644 --- a/generator/test-script-usage/template_templ.go +++ b/generator/test-script-usage/template_templ.go @@ -11,25 +11,28 @@ import "bytes" func withParameters(a string, b string, c int) templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_withParameters_1056`, - Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}`, - Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + Name: `__templ_withParameters_1056`, + Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}`, + Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + CallInline: templ.SafeScriptInline(`__templ_withParameters_1056`, a, b, c), } } func withoutParameters() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_withoutParameters_6bbf`, - Function: `function __templ_withoutParameters_6bbf(){alert("hello");}`, - Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + Name: `__templ_withoutParameters_6bbf`, + Function: `function __templ_withoutParameters_6bbf(){alert("hello");}`, + Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + CallInline: templ.SafeScriptInline(`__templ_withoutParameters_6bbf`), } } func onClick() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_onClick_657d`, - Function: `function __templ_onClick_657d(){alert("clicked");}`, - Call: templ.SafeScript(`__templ_onClick_657d`), + Name: `__templ_onClick_657d`, + Function: `function __templ_onClick_657d(){alert("clicked");}`, + Call: templ.SafeScript(`__templ_onClick_657d`), + CallInline: templ.SafeScriptInline(`__templ_onClick_657d`), } } @@ -170,9 +173,10 @@ func ThreeButtons() templ.Component { func conditionalScript() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_conditionalScript_de41`, - Function: `function __templ_conditionalScript_de41(){alert("conditional");}`, - Call: templ.SafeScript(`__templ_conditionalScript_de41`), + Name: `__templ_conditionalScript_de41`, + Function: `function __templ_conditionalScript_de41(){alert("conditional");}`, + Call: templ.SafeScript(`__templ_conditionalScript_de41`), + CallInline: templ.SafeScriptInline(`__templ_conditionalScript_de41`), } } diff --git a/runtime.go b/runtime.go index 75a694cc1..c6f618d95 100644 --- a/runtime.go +++ b/runtime.go @@ -456,13 +456,33 @@ type SafeURL string // Script handling. -// SafeScript encodes unknown parameters for safety. -func SafeScript(functionName string, params ...interface{}) string { +func safeEncodeScriptParams(escapeHTML bool, params []any) []string { encodedParams := make([]string, len(params)) for i := 0; i < len(encodedParams); i++ { enc, _ := json.Marshal(params[i]) + if !escapeHTML { + encodedParams[i] = string(enc) + continue + } encodedParams[i] = EscapeString(string(enc)) } + return encodedParams +} + +// SafeScript encodes unknown parameters for safety for inside HTML attributes. +func SafeScript(functionName string, params ...any) string { + encodedParams := safeEncodeScriptParams(true, params) + sb := new(strings.Builder) + sb.WriteString(functionName) + sb.WriteRune('(') + sb.WriteString(strings.Join(encodedParams, ",")) + sb.WriteRune(')') + return sb.String() +} + +// SafeScript encodes unknown parameters for safety for inline scripts. +func SafeScriptInline(functionName string, params ...any) string { + encodedParams := safeEncodeScriptParams(false, params) sb := new(strings.Builder) sb.WriteString(functionName) sb.WriteRune('(') @@ -535,9 +555,51 @@ type ComponentScript struct { Name string // Function to render. Function string - // Call of the function in JavaScript syntax, including parameters. - // e.g. print({ x: 1 }) + // Call of the function in JavaScript syntax, including parameters, and + // ensures parameters are HTML escaped; useful for injecting into HTML + // attributes like onclick, onhover, etc. + // + // Given: + // functionName("some string",12345) + // It would render: + // __templ_functionName_sha("some string",12345)) + // + // This is can be injected into HTML attributes: + // Call string + // Call of the function in JavaScript syntax, including parameters. It + // does not HTML escape parameters; useful for directly calling in script + // elements. + // + // Given: + // functionName("some string",12345) + // It would render: + // __templ_functionName_sha("some string",12345)) + // + // This is can be used to call the function inside a script tag: + // + CallInline string +} + +var _ Component = ComponentScript{} + +func (c ComponentScript) Render(ctx context.Context, w io.Writer) error { + err := RenderScriptItems(ctx, w, c) + if err != nil { + return err + } + if len(c.Call) > 0 { + if _, err = io.WriteString(w, ``); err != nil { + return err + } + } + return nil } // RenderScriptItems renders a