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",