diff --git a/bench_test.go b/bench_test.go index 220363e..211a795 100644 --- a/bench_test.go +++ b/bench_test.go @@ -1,7 +1,7 @@ package html import ( - "fmt" + "strconv" "testing" i18n "dappco.re/go/core/i18n" @@ -100,7 +100,7 @@ func BenchmarkImprint_Small(b *testing.B) { func BenchmarkImprint_Large(b *testing.B) { items := make([]string, 20) for i := range items { - items[i] = fmt.Sprintf("Item %d was created successfully", i) + items[i] = "Item " + strconv.Itoa(i) + " was created successfully" } page := NewLayout("HLCRF"). H(El("h1", Text("Building project"))). @@ -207,7 +207,7 @@ func BenchmarkLayout_Nested(b *testing.B) { func BenchmarkLayout_ManySlotChildren(b *testing.B) { nodes := make([]Node, 50) for i := range nodes { - nodes[i] = El("p", Raw(fmt.Sprintf("paragraph %d", i))) + nodes[i] = El("p", Raw("paragraph "+strconv.Itoa(i))) } layout := NewLayout("HLCRF"). H(Raw("header")). @@ -242,7 +242,7 @@ func benchEach(b *testing.B, n int) { items[i] = i } node := Each(items, func(i int) Node { - return El("li", Raw(fmt.Sprintf("item-%d", i))) + return El("li", Raw("item-"+strconv.Itoa(i))) }) ctx := NewContext() diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go index b72def3..46f67d1 100644 --- a/cmd/codegen/main.go +++ b/cmd/codegen/main.go @@ -1,3 +1,5 @@ +//go:build !js + // Package main provides a build-time CLI for generating Web Component JS bundles. // Reads a JSON slot map from stdin, writes the generated JS to stdout. // diff --git a/cmd/codegen/main_test.go b/cmd/codegen/main_test.go index edacf19..701a095 100644 --- a/cmd/codegen/main_test.go +++ b/cmd/codegen/main_test.go @@ -1,3 +1,5 @@ +//go:build !js + package main import ( @@ -9,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestRun_Good(t *testing.T) { +func TestRun_WritesBundle(t *testing.T) { input := strings.NewReader(`{"H":"nav-bar","C":"main-content"}`) var output bytes.Buffer @@ -23,7 +25,7 @@ func TestRun_Good(t *testing.T) { assert.Equal(t, 2, strings.Count(js, "extends HTMLElement")) } -func TestRun_Bad_InvalidJSON(t *testing.T) { +func TestRun_InvalidJSON(t *testing.T) { input := strings.NewReader(`not json`) var output bytes.Buffer @@ -32,7 +34,7 @@ func TestRun_Bad_InvalidJSON(t *testing.T) { assert.Contains(t, err.Error(), "invalid JSON") } -func TestRun_Bad_InvalidTag(t *testing.T) { +func TestRun_InvalidTag(t *testing.T) { input := strings.NewReader(`{"H":"notag"}`) var output bytes.Buffer @@ -41,7 +43,7 @@ func TestRun_Bad_InvalidTag(t *testing.T) { assert.Contains(t, err.Error(), "hyphen") } -func TestRun_Good_Empty(t *testing.T) { +func TestRun_EmptySlots(t *testing.T) { input := strings.NewReader(`{}`) var output bytes.Buffer diff --git a/cmd/wasm/register_test.go b/cmd/wasm/register_test.go index 65f2af2..255fab8 100644 --- a/cmd/wasm/register_test.go +++ b/cmd/wasm/register_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestBuildComponentJS_Good(t *testing.T) { +func TestBuildComponentJS_ValidJSON(t *testing.T) { slotsJSON := `{"H":"nav-bar","C":"main-content"}` js, err := buildComponentJS(slotsJSON) require.NoError(t, err) @@ -18,7 +18,7 @@ func TestBuildComponentJS_Good(t *testing.T) { assert.Contains(t, js, "customElements.define") } -func TestBuildComponentJS_Bad_InvalidJSON(t *testing.T) { +func TestBuildComponentJS_InvalidJSON(t *testing.T) { _, err := buildComponentJS("not json") assert.Error(t, err) } diff --git a/cmd/wasm/size_test.go b/cmd/wasm/size_test.go index ed759c1..79bac23 100644 --- a/cmd/wasm/size_test.go +++ b/cmd/wasm/size_test.go @@ -21,7 +21,7 @@ const ( wasmRawLimit = 3_670_016 // 3.5 MB raw size limit ) -func TestWASMBinarySize_Good(t *testing.T) { +func TestWASMBinarySize_WithinBudget(t *testing.T) { if testing.Short() { t.Skip("skipping WASM build test in short mode") } diff --git a/codegen/bench_test.go b/codegen/bench_test.go index 0fdaecd..4a678d9 100644 --- a/codegen/bench_test.go +++ b/codegen/bench_test.go @@ -1,3 +1,5 @@ +//go:build !js + package codegen import "testing" diff --git a/codegen/codegen.go b/codegen/codegen.go index 8692b0b..4963db4 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -1,7 +1,8 @@ +//go:build !js + package codegen import ( - "fmt" "strings" "text/template" @@ -51,7 +52,7 @@ func GenerateClass(tag, slot string) (string, error) { // GenerateRegistration produces the customElements.define() call. func GenerateRegistration(tag, className string) string { - return fmt.Sprintf(`customElements.define("%s", %s);`, tag, className) + return `customElements.define("` + tag + `", ` + className + `);` } // TagToClassName converts a kebab-case tag to PascalCase class name. diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index 28a6aa2..de3f643 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -1,3 +1,5 @@ +//go:build !js + package codegen import ( @@ -8,7 +10,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestGenerateClass_Good(t *testing.T) { +func TestGenerateClass_ValidTag(t *testing.T) { js, err := GenerateClass("photo-grid", "C") require.NoError(t, err) assert.Contains(t, js, "class PhotoGrid extends HTMLElement") @@ -17,19 +19,19 @@ func TestGenerateClass_Good(t *testing.T) { assert.Contains(t, js, "photo-grid") } -func TestGenerateClass_Bad_InvalidTag(t *testing.T) { +func TestGenerateClass_InvalidTag(t *testing.T) { _, err := GenerateClass("invalid", "C") assert.Error(t, err, "custom element names must contain a hyphen") } -func TestGenerateRegistration_Good(t *testing.T) { +func TestGenerateRegistration_DefinesCustomElement(t *testing.T) { js := GenerateRegistration("photo-grid", "PhotoGrid") assert.Contains(t, js, "customElements.define") assert.Contains(t, js, `"photo-grid"`) assert.Contains(t, js, "PhotoGrid") } -func TestTagToClassName_Good(t *testing.T) { +func TestTagToClassName_KebabCase(t *testing.T) { tests := []struct{ tag, want string }{ {"photo-grid", "PhotoGrid"}, {"nav-breadcrumb", "NavBreadcrumb"}, @@ -41,14 +43,16 @@ func TestTagToClassName_Good(t *testing.T) { } } -func TestGenerateBundle_Good(t *testing.T) { +func TestGenerateBundle_DeduplicatesRegistrations(t *testing.T) { slots := map[string]string{ "H": "nav-bar", "C": "main-content", + "F": "nav-bar", } js, err := GenerateBundle(slots) require.NoError(t, err) assert.Contains(t, js, "NavBar") assert.Contains(t, js, "MainContent") assert.Equal(t, 2, strings.Count(js, "extends HTMLElement")) + assert.Equal(t, 2, strings.Count(js, "customElements.define")) } diff --git a/codegen/doc.go b/codegen/doc.go new file mode 100644 index 0000000..afc9b13 --- /dev/null +++ b/codegen/doc.go @@ -0,0 +1,13 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package codegen generates Web Component bundles for go-html slot maps. +// +// Use it at build time, or through the cmd/codegen CLI: +// +// bundle, err := GenerateBundle(map[string]string{ +// "H": "site-header", +// "C": "app-main", +// }) +package codegen diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..b3c3317 --- /dev/null +++ b/doc.go @@ -0,0 +1,12 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package html renders semantic HTML from composable node trees. +// +// A typical page combines Layout, El, Text, and Render: +// +// page := NewLayout("HCF"). +// H(El("h1", Text("page.title"))). +// C(El("main", Text("page.body"))). +// F(El("small", Text("page.footer"))) +// out := Render(page, NewContext()) +package html diff --git a/docs/development.md b/docs/development.md index eb5c476..c5c6838 100644 --- a/docs/development.md +++ b/docs/development.md @@ -66,7 +66,7 @@ go test ./cmd/codegen/ go test ./cmd/wasm/ ``` -The WASM size gate test (`TestWASMBinarySize_Good`) builds the WASM binary as a subprocess. It is slow and is skipped under `-short`. It is also guarded with `//go:build !js` so it cannot run within the WASM environment itself. +The WASM size gate test (`TestWASMBinarySize_WithinBudget`) builds the WASM binary as a subprocess. It is slow and is skipped under `-short`. It is also guarded with `//go:build !js` so it cannot run within the WASM environment itself. ### Test Dependencies @@ -278,7 +278,7 @@ func TestIntegration_RenderThenReverse(t *testing.T) { ### Codegen Tests with Testify ```go -func TestGenerateClass_Good(t *testing.T) { +func TestGenerateClass_ValidTag(t *testing.T) { js, err := GenerateClass("photo-grid", "C") require.NoError(t, err) assert.Contains(t, js, "class PhotoGrid extends HTMLElement") diff --git a/docs/history.md b/docs/history.md index 4631ef2..8321ae2 100644 --- a/docs/history.md +++ b/docs/history.md @@ -78,7 +78,7 @@ The fix was applied in three distinct steps: ### Size gate test (`aae5d21`) -`cmd/wasm/size_test.go` was added to prevent regression. `TestWASMBinarySize_Good` builds the WASM binary in a temp directory, gzip-compresses it, and asserts: +`cmd/wasm/size_test.go` was added to prevent regression. `TestWASMBinarySize_WithinBudget` builds the WASM binary in a temp directory, gzip-compresses it, and asserts: - Gzip size < 1,048,576 bytes (1 MB). - Raw size < 3,145,728 bytes (3 MB). diff --git a/edge_test.go b/edge_test.go index 9ff9055..8d1261d 100644 --- a/edge_test.go +++ b/edge_test.go @@ -1,7 +1,7 @@ package html import ( - "fmt" + "strconv" "strings" "testing" @@ -196,7 +196,7 @@ func TestLayout_DeepNesting_10Levels(t *testing.T) { for i := 1; i < 10; i++ { expectedBlock += "-C-0" } - if !strings.Contains(got, fmt.Sprintf(`data-block="%s"`, expectedBlock)) { + if !strings.Contains(got, `data-block="`+expectedBlock+`"`) { t.Errorf("10 levels deep: missing expected block ID %q in:\n%s", expectedBlock, got) } @@ -251,7 +251,7 @@ func TestEach_LargeIteration_1000(t *testing.T) { } node := Each(items, func(i int) Node { - return El("li", Raw(fmt.Sprintf("%d", i))) + return El("li", Raw(strconv.Itoa(i))) }) got := node.Render(ctx) @@ -275,7 +275,7 @@ func TestEach_LargeIteration_5000(t *testing.T) { } node := Each(items, func(i int) Node { - return El("span", Raw(fmt.Sprintf("%d", i))) + return El("span", Raw(strconv.Itoa(i))) }) got := node.Render(ctx) @@ -292,7 +292,7 @@ func TestEach_NestedEach(t *testing.T) { node := Each(rows, func(row int) Node { return El("tr", Each(cols, func(col string) Node { - return El("td", Raw(fmt.Sprintf("%d-%s", row, col))) + return El("td", Raw(strconv.Itoa(row)+"-"+col)) })) })