From 8abd428227711bad0e545f4e455e8ae27b40aaab Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 09:49:42 +0000 Subject: [PATCH] fix(codegen): validate custom element tags Co-Authored-By: Virgil --- cmd/codegen/main_test.go | 9 +++++++++ codegen/codegen.go | 29 +++++++++++++++++++++++++++-- codegen/codegen_test.go | 21 +++++++++++++++++++++ codegen/typescript.go | 2 +- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/cmd/codegen/main_test.go b/cmd/codegen/main_test.go index 7376b43..703d0ab 100644 --- a/cmd/codegen/main_test.go +++ b/cmd/codegen/main_test.go @@ -48,6 +48,15 @@ func TestRun_InvalidTag_Bad(t *testing.T) { assert.Contains(t, err.Error(), "hyphen") } +func TestRun_InvalidTagCharacters_Bad(t *testing.T) { + input := core.NewReader(`{"H":"Nav-Bar","C":"nav bar"}`) + output := core.NewBuilder() + + err := run(input, output, false) + assert.Error(t, err) + assert.Contains(t, err.Error(), "lowercase hyphenated name") +} + func TestRun_EmptySlots_Good(t *testing.T) { input := core.NewReader(`{}`) output := core.NewBuilder() diff --git a/codegen/codegen.go b/codegen/codegen.go index 1c26320..2fbcebf 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -10,6 +10,31 @@ import ( log "dappco.re/go/core/log" ) +// isValidCustomElementTag reports whether tag is a safe custom element name. +// The generator rejects values that would fail at customElements.define() time. +func isValidCustomElementTag(tag string) bool { + if tag == "" || !core.Contains(tag, "-") { + return false + } + + if tag[0] < 'a' || tag[0] > 'z' { + return false + } + + for i := range len(tag) { + ch := tag[i] + switch { + case ch >= 'a' && ch <= 'z': + case ch >= '0' && ch <= '9': + case ch == '-' || ch == '.' || ch == '_': + default: + return false + } + } + + return true +} + // wcTemplate is the Web Component class template. // Uses closed Shadow DOM for isolation. Content is set via the shadow root's // DOM API using trusted go-html codegen output (never user input). @@ -35,8 +60,8 @@ var wcTemplate = template.Must(template.New("wc").Parse(`class {{.ClassName}} ex // GenerateClass produces a JS class definition for a custom element. // Usage example: js, err := GenerateClass("nav-bar", "H") func GenerateClass(tag, slot string) (string, error) { - if !core.Contains(tag, "-") { - return "", log.E("codegen.GenerateClass", "custom element tag must contain a hyphen: "+tag, nil) + if !isValidCustomElementTag(tag) { + return "", log.E("codegen.GenerateClass", "custom element tag must be a lowercase hyphenated name: "+tag, nil) } b := core.NewBuilder() err := wcTemplate.Execute(b, struct { diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index 39a1bcd..3fdcdc4 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -22,6 +22,12 @@ func TestGenerateClass_ValidTag_Good(t *testing.T) { func TestGenerateClass_InvalidTag_Bad(t *testing.T) { _, err := GenerateClass("invalid", "C") assert.Error(t, err, "custom element names must contain a hyphen") + + _, err = GenerateClass("Nav-Bar", "C") + assert.Error(t, err, "custom element names must be lowercase") + + _, err = GenerateClass("nav bar", "C") + assert.Error(t, err, "custom element names must reject spaces") } func TestGenerateRegistration_DefinesCustomElement_Good(t *testing.T) { @@ -99,6 +105,21 @@ func TestGenerateTypeScriptDefinitions_DeduplicatesAndOrders_Good(t *testing.T) assert.Less(t, strings.Index(dts, `"alpha-panel": AlphaPanel;`), strings.Index(dts, `"zed-panel": ZedPanel;`)) } +func TestGenerateTypeScriptDefinitions_SkipsInvalidTags_Good(t *testing.T) { + slots := map[string]string{ + "H": "nav-bar", + "C": "Nav-Bar", + "F": "nav bar", + } + + dts := GenerateTypeScriptDefinitions(slots) + + assert.Contains(t, dts, `"nav-bar": NavBar;`) + assert.NotContains(t, dts, "Nav-Bar") + assert.NotContains(t, dts, "nav bar") + assert.Equal(t, 1, countSubstr(dts, `export declare class NavBar extends HTMLElement`)) +} + func countSubstr(s, substr string) int { if substr == "" { return len(s) + 1 diff --git a/codegen/typescript.go b/codegen/typescript.go index a71199b..035a0e7 100644 --- a/codegen/typescript.go +++ b/codegen/typescript.go @@ -28,7 +28,7 @@ func GenerateTypeScriptDefinitions(slots map[string]string) string { b.WriteString(" interface HTMLElementTagNameMap {\n") for _, slot := range keys { tag := slots[slot] - if seen[tag] { + if !isValidCustomElementTag(tag) || seen[tag] { continue } seen[tag] = true