diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go
index 46f67d1..523f781 100644
--- a/cmd/codegen/main.go
+++ b/cmd/codegen/main.go
@@ -9,11 +9,11 @@
package main
import (
- "encoding/json"
goio "io"
- "os"
+ core "dappco.re/go/core"
"dappco.re/go/core/html/codegen"
+ coreio "dappco.re/go/core/io"
log "dappco.re/go/core/log"
)
@@ -24,22 +24,40 @@ func run(r goio.Reader, w goio.Writer) error {
}
var slots map[string]string
- if err := json.Unmarshal(data, &slots); err != nil {
+ if result := core.JSONUnmarshal(data, &slots); !result.OK {
+ err, _ := result.Value.(error)
return log.E("codegen", "invalid JSON", err)
}
js, err := codegen.GenerateBundle(slots)
if err != nil {
- return err
+ return log.E("codegen", "generate bundle", err)
}
_, err = goio.WriteString(w, js)
- return err
+ if err != nil {
+ return log.E("codegen", "writing bundle", err)
+ }
+ return nil
}
func main() {
- if err := run(os.Stdin, os.Stdout); err != nil {
+ stdin, err := coreio.Local.Open("/dev/stdin")
+ if err != nil {
+ panic(log.E("codegen.main", "open stdin", err))
+ }
+
+ stdout, err := coreio.Local.Create("/dev/stdout")
+ if err != nil {
+ panic(log.E("codegen.main", "open stdout", err))
+ }
+ defer func() {
+ _ = stdin.Close()
+ _ = stdout.Close()
+ }()
+
+ if err := run(stdin, stdout); err != nil {
log.Error("codegen failed", "err", err)
- os.Exit(1)
+ panic(err)
}
}
diff --git a/cmd/codegen/main_test.go b/cmd/codegen/main_test.go
index 046f470..d0046e3 100644
--- a/cmd/codegen/main_test.go
+++ b/cmd/codegen/main_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestRun_WritesBundle(t *testing.T) {
+func TestRun_WritesBundle_Good(t *testing.T) {
input := core.NewReader(`{"H":"nav-bar","C":"main-content"}`)
output := core.NewBuilder()
@@ -24,7 +24,7 @@ func TestRun_WritesBundle(t *testing.T) {
assert.Equal(t, 2, countSubstr(js, "extends HTMLElement"))
}
-func TestRun_InvalidJSON(t *testing.T) {
+func TestRun_InvalidJSON_Bad(t *testing.T) {
input := core.NewReader(`not json`)
output := core.NewBuilder()
@@ -33,7 +33,7 @@ func TestRun_InvalidJSON(t *testing.T) {
assert.Contains(t, err.Error(), "invalid JSON")
}
-func TestRun_InvalidTag(t *testing.T) {
+func TestRun_InvalidTag_Bad(t *testing.T) {
input := core.NewReader(`{"H":"notag"}`)
output := core.NewBuilder()
@@ -42,7 +42,7 @@ func TestRun_InvalidTag(t *testing.T) {
assert.Contains(t, err.Error(), "hyphen")
}
-func TestRun_EmptySlots(t *testing.T) {
+func TestRun_EmptySlots_Good(t *testing.T) {
input := core.NewReader(`{}`)
output := core.NewBuilder()
diff --git a/cmd/wasm/register.go b/cmd/wasm/register.go
index d88c866..d463465 100644
--- a/cmd/wasm/register.go
+++ b/cmd/wasm/register.go
@@ -3,7 +3,7 @@
package main
import (
- "encoding/json"
+ core "dappco.re/go/core"
"dappco.re/go/core/html/codegen"
log "dappco.re/go/core/log"
@@ -15,7 +15,8 @@ import (
// Use cmd/codegen/ CLI instead for build-time generation.
func buildComponentJS(slotsJSON string) (string, error) {
var slots map[string]string
- if err := json.Unmarshal([]byte(slotsJSON), &slots); err != nil {
+ if result := core.JSONUnmarshalString(slotsJSON, &slots); !result.OK {
+ err, _ := result.Value.(error)
return "", log.E("buildComponentJS", "unmarshal JSON", err)
}
return codegen.GenerateBundle(slots)
diff --git a/cmd/wasm/register_test.go b/cmd/wasm/register_test.go
index 255fab8..9365e9e 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_ValidJSON(t *testing.T) {
+func TestBuildComponentJS_ValidJSON_Good(t *testing.T) {
slotsJSON := `{"H":"nav-bar","C":"main-content"}`
js, err := buildComponentJS(slotsJSON)
require.NoError(t, err)
@@ -18,7 +18,7 @@ func TestBuildComponentJS_ValidJSON(t *testing.T) {
assert.Contains(t, js, "customElements.define")
}
-func TestBuildComponentJS_InvalidJSON(t *testing.T) {
+func TestBuildComponentJS_InvalidJSON_Bad(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 8187bd9..e8b1d96 100644
--- a/cmd/wasm/size_test.go
+++ b/cmd/wasm/size_test.go
@@ -5,13 +5,12 @@ package main
import (
"compress/gzip"
- "os"
- "os/exec"
- "path/filepath"
+ "context"
"testing"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
+ process "dappco.re/go/core/process"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -21,34 +20,44 @@ const (
wasmRawLimit = 3_670_016 // 3.5 MB raw size limit
)
-func TestWASMBinarySize_WithinBudget(t *testing.T) {
+func TestCmdWasm_WASMBinarySize_Good(t *testing.T) {
if testing.Short() {
t.Skip("skipping WASM build test in short mode")
}
dir := t.TempDir()
- out := filepath.Join(dir, "gohtml.wasm")
+ out := core.Path(dir, "gohtml.wasm")
- cmd := exec.Command("go", "build", "-ldflags=-s -w", "-o", out, ".")
- cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
- output, err := cmd.CombinedOutput()
+ factory := process.NewService(process.Options{})
+ serviceValue, err := factory(core.New())
+ require.NoError(t, err)
+
+ svc, ok := serviceValue.(*process.Service)
+ require.True(t, ok, "process service factory returned %T", serviceValue)
+
+ output, err := svc.RunWithOptions(context.Background(), process.RunOptions{
+ Command: "go",
+ Args: []string{"build", "-ldflags=-s -w", "-o", out, "."},
+ Dir: ".",
+ Env: []string{"GOOS=js", "GOARCH=wasm"},
+ })
require.NoError(t, err, "WASM build failed: %s", output)
rawStr, err := coreio.Local.Read(out)
require.NoError(t, err)
- raw := []byte(rawStr)
+ rawBytes := []byte(rawStr)
buf := core.NewBuilder()
gz, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
require.NoError(t, err)
- _, err = gz.Write(raw)
+ _, err = gz.Write(rawBytes)
require.NoError(t, err)
require.NoError(t, gz.Close())
- t.Logf("WASM size: %d bytes raw, %d bytes gzip", len(raw), buf.Len())
+ t.Logf("WASM size: %d bytes raw, %d bytes gzip", len(rawBytes), buf.Len())
assert.Less(t, buf.Len(), wasmGzLimit,
"WASM gzip size %d exceeds 1MB limit", buf.Len())
- assert.Less(t, len(raw), wasmRawLimit,
- "WASM raw size %d exceeds 3MB limit", len(raw))
+ assert.Less(t, len(rawBytes), wasmRawLimit,
+ "WASM raw size %d exceeds 3MB limit", len(rawBytes))
}
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 2f895b3..e0183c5 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -32,6 +32,7 @@ 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)
@@ -51,11 +52,13 @@ func GenerateClass(tag, slot string) (string, error) {
}
// GenerateRegistration produces the customElements.define() call.
+// Usage example: js := GenerateRegistration("nav-bar", "NavBar")
func GenerateRegistration(tag, className string) string {
return `customElements.define("` + tag + `", ` + className + `);`
}
// TagToClassName converts a kebab-case tag to PascalCase class name.
+// Usage example: className := TagToClassName("nav-bar")
func TagToClassName(tag string) string {
b := core.NewBuilder()
for _, p := range core.Split(tag, "-") {
@@ -69,6 +72,7 @@ func TagToClassName(tag string) string {
// GenerateBundle produces all WC class definitions and registrations
// for a set of HLCRF slot assignments.
+// Usage example: js, err := GenerateBundle(map[string]string{"H": "nav-bar"})
func GenerateBundle(slots map[string]string) (string, error) {
seen := make(map[string]bool)
b := core.NewBuilder()
@@ -81,7 +85,7 @@ func GenerateBundle(slots map[string]string) (string, error) {
cls, err := GenerateClass(tag, slot)
if err != nil {
- return "", err
+ return "", log.E("codegen.GenerateBundle", "generate class for tag "+tag, err)
}
b.WriteString(cls)
b.WriteByte('\n')
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 480eba6..66359b9 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestGenerateClass_ValidTag(t *testing.T) {
+func TestGenerateClass_ValidTag_Good(t *testing.T) {
js, err := GenerateClass("photo-grid", "C")
require.NoError(t, err)
assert.Contains(t, js, "class PhotoGrid extends HTMLElement")
@@ -18,19 +18,19 @@ func TestGenerateClass_ValidTag(t *testing.T) {
assert.Contains(t, js, "photo-grid")
}
-func TestGenerateClass_InvalidTag(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")
}
-func TestGenerateRegistration_DefinesCustomElement(t *testing.T) {
+func TestGenerateRegistration_DefinesCustomElement_Good(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_KebabCase(t *testing.T) {
+func TestTagToClassName_KebabCase_Good(t *testing.T) {
tests := []struct{ tag, want string }{
{"photo-grid", "PhotoGrid"},
{"nav-breadcrumb", "NavBreadcrumb"},
@@ -42,7 +42,7 @@ func TestTagToClassName_KebabCase(t *testing.T) {
}
}
-func TestGenerateBundle_DeduplicatesRegistrations(t *testing.T) {
+func TestGenerateBundle_DeduplicatesRegistrations_Good(t *testing.T) {
slots := map[string]string{
"H": "nav-bar",
"C": "main-content",
diff --git a/context.go b/context.go
index a220dbc..c879fd6 100644
--- a/context.go
+++ b/context.go
@@ -1,6 +1,7 @@
package html
// Translator provides Text() lookups for a rendering context.
+// Usage example: ctx := NewContextWithService(myTranslator)
//
// The default server build uses go-i18n. Alternate builds, including WASM,
// can provide any implementation with the same T() method.
@@ -9,6 +10,7 @@ type Translator interface {
}
// Context carries rendering state through the node tree.
+// Usage example: ctx := NewContext()
type Context struct {
Identity string
Locale string
@@ -18,6 +20,7 @@ type Context struct {
}
// NewContext creates a new rendering context with sensible defaults.
+// Usage example: html := Render(Text("welcome"), NewContext())
func NewContext() *Context {
return &Context{
Data: make(map[string]any),
@@ -25,6 +28,7 @@ func NewContext() *Context {
}
// NewContextWithService creates a rendering context backed by a specific translator.
+// Usage example: ctx := NewContextWithService(myTranslator)
func NewContextWithService(svc Translator) *Context {
return &Context{
Data: make(map[string]any),
diff --git a/edge_test.go b/edge_test.go
index 1f745a4..a4e8859 100644
--- a/edge_test.go
+++ b/edge_test.go
@@ -8,7 +8,7 @@ import (
// --- Unicode / RTL edge cases ---
-func TestText_Emoji(t *testing.T) {
+func TestText_Emoji_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
ctx := NewContext()
@@ -39,7 +39,7 @@ func TestText_Emoji(t *testing.T) {
}
}
-func TestEl_Emoji(t *testing.T) {
+func TestEl_Emoji_Ugly(t *testing.T) {
ctx := NewContext()
node := El("span", Raw("\U0001F680 Launch"))
got := node.Render(ctx)
@@ -49,7 +49,7 @@ func TestEl_Emoji(t *testing.T) {
}
}
-func TestText_RTL(t *testing.T) {
+func TestText_RTL_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
ctx := NewContext()
@@ -74,7 +74,7 @@ func TestText_RTL(t *testing.T) {
}
}
-func TestEl_RTL(t *testing.T) {
+func TestEl_RTL_Ugly(t *testing.T) {
ctx := NewContext()
node := Attr(El("div", Raw("\u0645\u0631\u062D\u0628\u0627")), "dir", "rtl")
got := node.Render(ctx)
@@ -86,7 +86,7 @@ func TestEl_RTL(t *testing.T) {
}
}
-func TestText_ZeroWidth(t *testing.T) {
+func TestText_ZeroWidth_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
ctx := NewContext()
@@ -112,7 +112,7 @@ func TestText_ZeroWidth(t *testing.T) {
}
}
-func TestText_MixedScripts(t *testing.T) {
+func TestText_MixedScripts_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
ctx := NewContext()
@@ -139,7 +139,7 @@ func TestText_MixedScripts(t *testing.T) {
}
}
-func TestStripTags_Unicode(t *testing.T) {
+func TestStripTags_Unicode_Ugly(t *testing.T) {
tests := []struct {
name string
input string
@@ -161,7 +161,7 @@ func TestStripTags_Unicode(t *testing.T) {
}
}
-func TestAttr_UnicodeValue(t *testing.T) {
+func TestAttr_UnicodeValue_Ugly(t *testing.T) {
ctx := NewContext()
node := Attr(El("div"), "title", "\U0001F680 Rocket Launch")
got := node.Render(ctx)
@@ -173,7 +173,7 @@ func TestAttr_UnicodeValue(t *testing.T) {
// --- Deep nesting stress tests ---
-func TestLayout_DeepNesting_10Levels(t *testing.T) {
+func TestLayout_DeepNesting_10Levels_Ugly(t *testing.T) {
ctx := NewContext()
// Build 10 levels of nested layouts
@@ -204,7 +204,7 @@ func TestLayout_DeepNesting_10Levels(t *testing.T) {
}
}
-func TestLayout_DeepNesting_20Levels(t *testing.T) {
+func TestLayout_DeepNesting_20Levels_Ugly(t *testing.T) {
ctx := NewContext()
current := NewLayout("C").C(Raw("bottom"))
@@ -222,7 +222,7 @@ func TestLayout_DeepNesting_20Levels(t *testing.T) {
}
}
-func TestLayout_DeepNesting_MixedSlots(t *testing.T) {
+func TestLayout_DeepNesting_MixedSlots_Ugly(t *testing.T) {
ctx := NewContext()
// Alternate slot types at each level: C -> L -> C -> L -> ...
@@ -241,7 +241,7 @@ func TestLayout_DeepNesting_MixedSlots(t *testing.T) {
}
}
-func TestEach_LargeIteration_1000(t *testing.T) {
+func TestEach_LargeIteration_1000_Ugly(t *testing.T) {
ctx := NewContext()
items := make([]int, 1000)
for i := range items {
@@ -265,7 +265,7 @@ func TestEach_LargeIteration_1000(t *testing.T) {
}
}
-func TestEach_LargeIteration_5000(t *testing.T) {
+func TestEach_LargeIteration_5000_Ugly(t *testing.T) {
ctx := NewContext()
items := make([]int, 5000)
for i := range items {
@@ -283,7 +283,7 @@ func TestEach_LargeIteration_5000(t *testing.T) {
}
}
-func TestEach_NestedEach(t *testing.T) {
+func TestEach_NestedEach_Ugly(t *testing.T) {
ctx := NewContext()
rows := []int{0, 1, 2}
cols := []string{"a", "b", "c"}
@@ -309,7 +309,7 @@ func TestEach_NestedEach(t *testing.T) {
// --- Layout variant validation ---
-func TestLayout_InvalidVariant_Chars(t *testing.T) {
+func TestLayout_InvalidVariant_Chars_Bad(t *testing.T) {
ctx := NewContext()
tests := []struct {
@@ -341,7 +341,7 @@ func TestLayout_InvalidVariant_Chars(t *testing.T) {
}
}
-func TestLayout_InvalidVariant_MixedValidInvalid(t *testing.T) {
+func TestLayout_InvalidVariant_MixedValidInvalid_Bad(t *testing.T) {
ctx := NewContext()
// "HXC" — H and C are valid, X is not. Only H and C should render.
@@ -361,7 +361,7 @@ func TestLayout_InvalidVariant_MixedValidInvalid(t *testing.T) {
}
}
-func TestLayout_DuplicateVariantChars(t *testing.T) {
+func TestLayout_DuplicateVariantChars_Ugly(t *testing.T) {
ctx := NewContext()
// "CCC" — C appears three times. Should render C slot content three times.
@@ -374,7 +374,7 @@ func TestLayout_DuplicateVariantChars(t *testing.T) {
}
}
-func TestLayout_EmptySlots(t *testing.T) {
+func TestLayout_EmptySlots_Ugly(t *testing.T) {
ctx := NewContext()
// Variant includes all slots but none are populated — should produce empty output.
@@ -388,7 +388,7 @@ func TestLayout_EmptySlots(t *testing.T) {
// --- Render convenience function edge cases ---
-func TestRender_NilContext(t *testing.T) {
+func TestRender_NilContext_Ugly(t *testing.T) {
node := Raw("test")
got := Render(node, nil)
if got != "test" {
@@ -396,7 +396,7 @@ func TestRender_NilContext(t *testing.T) {
}
}
-func TestImprint_NilContext(t *testing.T) {
+func TestImprint_NilContext_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
@@ -408,7 +408,7 @@ func TestImprint_NilContext(t *testing.T) {
}
}
-func TestCompareVariants_NilContext(t *testing.T) {
+func TestCompareVariants_NilContext_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
@@ -422,7 +422,7 @@ func TestCompareVariants_NilContext(t *testing.T) {
}
}
-func TestCompareVariants_SingleVariant(t *testing.T) {
+func TestCompareVariants_SingleVariant_Ugly(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
@@ -437,7 +437,7 @@ func TestCompareVariants_SingleVariant(t *testing.T) {
// --- escapeHTML / escapeAttr edge cases ---
-func TestEscapeAttr_AllSpecialChars(t *testing.T) {
+func TestEscapeAttr_AllSpecialChars_Ugly(t *testing.T) {
ctx := NewContext()
node := Attr(El("div"), "data-val", `&<>"'`)
got := node.Render(ctx)
@@ -450,7 +450,7 @@ func TestEscapeAttr_AllSpecialChars(t *testing.T) {
}
}
-func TestElNode_EmptyTag(t *testing.T) {
+func TestElNode_EmptyTag_Ugly(t *testing.T) {
ctx := NewContext()
node := El("", Raw("content"))
got := node.Render(ctx)
@@ -461,7 +461,7 @@ func TestElNode_EmptyTag(t *testing.T) {
}
}
-func TestSwitchNode_NoMatch(t *testing.T) {
+func TestSwitchNode_NoMatch_Ugly(t *testing.T) {
ctx := NewContext()
cases := map[string]Node{
"a": Raw("alpha"),
@@ -474,7 +474,7 @@ func TestSwitchNode_NoMatch(t *testing.T) {
}
}
-func TestEntitled_NilContext(t *testing.T) {
+func TestEntitled_NilContext_Ugly(t *testing.T) {
node := Entitled("premium", Raw("content"))
got := node.Render(nil)
if got != "" {
diff --git a/go.mod b/go.mod
index 64c6ba9..9497a5f 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
dappco.re/go/core/i18n v0.2.0
dappco.re/go/core/io v0.2.0
dappco.re/go/core/log v0.1.0
+ dappco.re/go/core/process v0.3.0
github.com/stretchr/testify v1.11.1
)
diff --git a/go.sum b/go.sum
index bb601bc..7b72efe 100644
--- a/go.sum
+++ b/go.sum
@@ -6,6 +6,8 @@ dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
+dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM=
+dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as=
forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0=
forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
diff --git a/integration_test.go b/integration_test.go
index 1a6346c..00e1867 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -6,7 +6,7 @@ import (
i18n "dappco.re/go/core/i18n"
)
-func TestIntegration_RenderThenReverse(t *testing.T) {
+func TestIntegration_RenderThenReverse_Good(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
ctx := NewContext()
@@ -26,7 +26,7 @@ func TestIntegration_RenderThenReverse(t *testing.T) {
}
}
-func TestIntegration_ResponsiveImprint(t *testing.T) {
+func TestIntegration_ResponsiveImprint_Good(t *testing.T) {
svc, _ := i18n.New()
i18n.SetDefault(svc)
ctx := NewContext()
diff --git a/layout.go b/layout.go
index 873b1d0..c156739 100644
--- a/layout.go
+++ b/layout.go
@@ -20,6 +20,7 @@ var slotRegistry = map[byte]slotMeta{
// Layout is an HLCRF compositor. Arranges nodes into semantic HTML regions
// with deterministic path-based IDs.
+// Usage example: page := NewLayout("HCF").H(Text("title")).C(Text("body"))
type Layout struct {
variant string // "HLCRF", "HCF", "C", etc.
path string // "" for root, "L-0-" for nested
@@ -27,6 +28,7 @@ type Layout struct {
}
// NewLayout creates a new Layout with the given variant string.
+// Usage example: page := NewLayout("HLCRF")
// The variant determines which slots are rendered (e.g., "HLCRF", "HCF", "C").
func NewLayout(variant string) *Layout {
return &Layout{
@@ -36,30 +38,35 @@ func NewLayout(variant string) *Layout {
}
// H appends nodes to the Header slot.
+// Usage example: NewLayout("HCF").H(Text("title"))
func (l *Layout) H(nodes ...Node) *Layout {
l.slots['H'] = append(l.slots['H'], nodes...)
return l
}
// L appends nodes to the Left aside slot.
+// Usage example: NewLayout("LC").L(Text("nav"))
func (l *Layout) L(nodes ...Node) *Layout {
l.slots['L'] = append(l.slots['L'], nodes...)
return l
}
// C appends nodes to the Content (main) slot.
+// Usage example: NewLayout("C").C(Text("body"))
func (l *Layout) C(nodes ...Node) *Layout {
l.slots['C'] = append(l.slots['C'], nodes...)
return l
}
// R appends nodes to the Right aside slot.
+// Usage example: NewLayout("CR").R(Text("ads"))
func (l *Layout) R(nodes ...Node) *Layout {
l.slots['R'] = append(l.slots['R'], nodes...)
return l
}
// F appends nodes to the Footer slot.
+// Usage example: NewLayout("CF").F(Text("footer"))
func (l *Layout) F(nodes ...Node) *Layout {
l.slots['F'] = append(l.slots['F'], nodes...)
return l
@@ -71,6 +78,7 @@ func (l *Layout) blockID(slot byte) string {
}
// Render produces the semantic HTML for this layout.
+// Usage example: html := NewLayout("C").C(Text("body")).Render(NewContext())
// Only slots present in the variant string are rendered.
func (l *Layout) Render(ctx *Context) string {
b := newTextBuilder()
diff --git a/layout_test.go b/layout_test.go
index 0b575a8..c492d4c 100644
--- a/layout_test.go
+++ b/layout_test.go
@@ -4,7 +4,7 @@ import (
"testing"
)
-func TestLayout_HLCRF(t *testing.T) {
+func TestLayout_HLCRF_Good(t *testing.T) {
ctx := NewContext()
layout := NewLayout("HLCRF").
H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
@@ -39,7 +39,7 @@ func TestLayout_HLCRF(t *testing.T) {
}
}
-func TestLayout_HCF(t *testing.T) {
+func TestLayout_HCF_Good(t *testing.T) {
ctx := NewContext()
layout := NewLayout("HCF").
H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
@@ -60,7 +60,7 @@ func TestLayout_HCF(t *testing.T) {
}
}
-func TestLayout_ContentOnly(t *testing.T) {
+func TestLayout_ContentOnly_Good(t *testing.T) {
ctx := NewContext()
layout := NewLayout("C").
H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
@@ -82,7 +82,7 @@ func TestLayout_ContentOnly(t *testing.T) {
}
}
-func TestLayout_FluentAPI(t *testing.T) {
+func TestLayout_FluentAPI_Good(t *testing.T) {
layout := NewLayout("HLCRF")
// Fluent methods should return the same layout for chaining
@@ -97,7 +97,7 @@ func TestLayout_FluentAPI(t *testing.T) {
}
}
-func TestLayout_IgnoresInvalidSlots(t *testing.T) {
+func TestLayout_IgnoresInvalidSlots_Good(t *testing.T) {
ctx := NewContext()
// "C" variant: populating L and R should have no effect
layout := NewLayout("C").L(Raw("left")).C(Raw("main")).R(Raw("right"))
diff --git a/node.go b/node.go
index a5bd78e..921c53c 100644
--- a/node.go
+++ b/node.go
@@ -8,6 +8,7 @@ import (
)
// Node is anything renderable.
+// Usage example: var n Node = El("div", Text("welcome"))
type Node interface {
Render(ctx *Context) string
}
@@ -53,6 +54,7 @@ type rawNode struct {
}
// Raw creates a node that renders without escaping (escape hatch for trusted content).
+// Usage example: Raw("trusted")
func Raw(content string) Node {
return &rawNode{content: content}
}
@@ -70,6 +72,7 @@ type elNode struct {
}
// El creates an HTML element node with children.
+// Usage example: El("section", Text("welcome"))
func El(tag string, children ...Node) Node {
return &elNode{
tag: tag,
@@ -79,6 +82,7 @@ func El(tag string, children ...Node) Node {
}
// Attr sets an attribute on an El node. Returns the node for chaining.
+// Usage example: Attr(El("a", Text("docs")), "href", "/docs")
// It recursively traverses through wrappers like If, Unless, and Entitled.
func Attr(n Node, key, value string) Node {
switch t := n.(type) {
@@ -143,6 +147,7 @@ type textNode struct {
}
// Text creates a node that renders through the go-i18n grammar pipeline.
+// Usage example: Text("welcome", "Ada")
// Output is HTML-escaped by default. Safe-by-default path.
func Text(key string, args ...any) Node {
return &textNode{key: key, args: args}
@@ -160,6 +165,7 @@ type ifNode struct {
}
// If renders child only when condition is true.
+// Usage example: If(func(ctx *Context) bool { return ctx.Identity != "" }, Text("hi"))
func If(cond func(*Context) bool, node Node) Node {
return &ifNode{cond: cond, node: node}
}
@@ -179,6 +185,7 @@ type unlessNode struct {
}
// Unless renders child only when condition is false.
+// Usage example: Unless(func(ctx *Context) bool { return ctx.Identity == "" }, Text("welcome"))
func Unless(cond func(*Context) bool, node Node) Node {
return &unlessNode{cond: cond, node: node}
}
@@ -198,6 +205,7 @@ type entitledNode struct {
}
// Entitled renders child only when entitlement is granted. Absent, not hidden.
+// Usage example: Entitled("beta", Text("preview"))
// If no entitlement function is set on the context, access is denied by default.
func Entitled(feature string, node Node) Node {
return &entitledNode{feature: feature, node: node}
@@ -218,6 +226,7 @@ type switchNode struct {
}
// Switch renders based on runtime selector value.
+// Usage example: Switch(func(ctx *Context) string { return ctx.Locale }, map[string]Node{"en": Text("hello")})
func Switch(selector func(*Context) string, cases map[string]Node) Node {
return &switchNode{selector: selector, cases: cases}
}
@@ -238,11 +247,13 @@ type eachNode[T any] struct {
}
// Each iterates items and renders each via fn.
+// Usage example: Each([]string{"a", "b"}, func(v string) Node { return Text(v) })
func Each[T any](items []T, fn func(T) Node) Node {
return EachSeq(slices.Values(items), fn)
}
// EachSeq iterates an iter.Seq and renders each via fn.
+// Usage example: EachSeq(slices.Values([]string{"a", "b"}), func(v string) Node { return Text(v) })
func EachSeq[T any](items iter.Seq[T], fn func(T) Node) Node {
return &eachNode[T]{items: items, fn: fn}
}
diff --git a/node_test.go b/node_test.go
index 6a076bd..9aa1f33 100644
--- a/node_test.go
+++ b/node_test.go
@@ -6,7 +6,7 @@ import (
i18n "dappco.re/go/core/i18n"
)
-func TestRawNode_Render(t *testing.T) {
+func TestRawNode_Render_Good(t *testing.T) {
ctx := NewContext()
node := Raw("hello")
got := node.Render(ctx)
@@ -15,7 +15,7 @@ func TestRawNode_Render(t *testing.T) {
}
}
-func TestElNode_Render(t *testing.T) {
+func TestElNode_Render_Good(t *testing.T) {
ctx := NewContext()
node := El("div", Raw("content"))
got := node.Render(ctx)
@@ -25,7 +25,7 @@ func TestElNode_Render(t *testing.T) {
}
}
-func TestElNode_Nested(t *testing.T) {
+func TestElNode_Nested_Good(t *testing.T) {
ctx := NewContext()
node := El("div", El("span", Raw("inner")))
got := node.Render(ctx)
@@ -35,7 +35,7 @@ func TestElNode_Nested(t *testing.T) {
}
}
-func TestElNode_MultipleChildren(t *testing.T) {
+func TestElNode_MultipleChildren_Good(t *testing.T) {
ctx := NewContext()
node := El("div", Raw("a"), Raw("b"))
got := node.Render(ctx)
@@ -45,7 +45,7 @@ func TestElNode_MultipleChildren(t *testing.T) {
}
}
-func TestElNode_VoidElement(t *testing.T) {
+func TestElNode_VoidElement_Good(t *testing.T) {
ctx := NewContext()
node := El("br")
got := node.Render(ctx)
@@ -55,7 +55,7 @@ func TestElNode_VoidElement(t *testing.T) {
}
}
-func TestTextNode_Render(t *testing.T) {
+func TestTextNode_Render_Good(t *testing.T) {
ctx := NewContext()
node := Text("hello")
got := node.Render(ctx)
@@ -64,7 +64,7 @@ func TestTextNode_Render(t *testing.T) {
}
}
-func TestTextNode_Escapes(t *testing.T) {
+func TestTextNode_Escapes_Good(t *testing.T) {
ctx := NewContext()
node := Text("")
got := node.Render(ctx)
@@ -76,7 +76,7 @@ func TestTextNode_Escapes(t *testing.T) {
}
}
-func TestIfNode_True(t *testing.T) {
+func TestIfNode_True_Good(t *testing.T) {
ctx := NewContext()
node := If(func(*Context) bool { return true }, Raw("visible"))
got := node.Render(ctx)
@@ -85,7 +85,7 @@ func TestIfNode_True(t *testing.T) {
}
}
-func TestIfNode_False(t *testing.T) {
+func TestIfNode_False_Good(t *testing.T) {
ctx := NewContext()
node := If(func(*Context) bool { return false }, Raw("hidden"))
got := node.Render(ctx)
@@ -94,7 +94,7 @@ func TestIfNode_False(t *testing.T) {
}
}
-func TestUnlessNode(t *testing.T) {
+func TestUnlessNode_Good(t *testing.T) {
ctx := NewContext()
node := Unless(func(*Context) bool { return false }, Raw("visible"))
got := node.Render(ctx)
@@ -103,7 +103,7 @@ func TestUnlessNode(t *testing.T) {
}
}
-func TestEntitledNode_Granted(t *testing.T) {
+func TestEntitledNode_Granted_Good(t *testing.T) {
ctx := NewContext()
ctx.Entitlements = func(feature string) bool { return feature == "premium" }
node := Entitled("premium", Raw("premium content"))
@@ -113,7 +113,7 @@ func TestEntitledNode_Granted(t *testing.T) {
}
}
-func TestEntitledNode_Denied(t *testing.T) {
+func TestEntitledNode_Denied_Bad(t *testing.T) {
ctx := NewContext()
ctx.Entitlements = func(feature string) bool { return false }
node := Entitled("premium", Raw("premium content"))
@@ -123,7 +123,7 @@ func TestEntitledNode_Denied(t *testing.T) {
}
}
-func TestEntitledNode_NoFunc(t *testing.T) {
+func TestEntitledNode_NoFunc_Bad(t *testing.T) {
ctx := NewContext()
node := Entitled("premium", Raw("premium content"))
got := node.Render(ctx)
@@ -132,7 +132,7 @@ func TestEntitledNode_NoFunc(t *testing.T) {
}
}
-func TestEachNode(t *testing.T) {
+func TestEachNode_Good(t *testing.T) {
ctx := NewContext()
items := []string{"a", "b", "c"}
node := Each(items, func(item string) Node {
@@ -145,7 +145,7 @@ func TestEachNode(t *testing.T) {
}
}
-func TestEachNode_Empty(t *testing.T) {
+func TestEachNode_Empty_Good(t *testing.T) {
ctx := NewContext()
node := Each([]string{}, func(item string) Node {
return El("li", Raw(item))
@@ -156,7 +156,7 @@ func TestEachNode_Empty(t *testing.T) {
}
}
-func TestElNode_Attr(t *testing.T) {
+func TestElNode_Attr_Good(t *testing.T) {
ctx := NewContext()
node := Attr(El("div", Raw("content")), "class", "container")
got := node.Render(ctx)
@@ -166,7 +166,7 @@ func TestElNode_Attr(t *testing.T) {
}
}
-func TestElNode_AttrEscaping(t *testing.T) {
+func TestElNode_AttrEscaping_Good(t *testing.T) {
ctx := NewContext()
node := Attr(El("img"), "alt", `he said "hello"`)
got := node.Render(ctx)
@@ -175,7 +175,7 @@ func TestElNode_AttrEscaping(t *testing.T) {
}
}
-func TestElNode_MultipleAttrs(t *testing.T) {
+func TestElNode_MultipleAttrs_Good(t *testing.T) {
ctx := NewContext()
node := Attr(Attr(El("a", Raw("link")), "href", "/home"), "class", "nav")
got := node.Render(ctx)
@@ -184,7 +184,7 @@ func TestElNode_MultipleAttrs(t *testing.T) {
}
}
-func TestAttr_NonElement(t *testing.T) {
+func TestAttr_NonElement_Ugly(t *testing.T) {
node := Attr(Raw("text"), "class", "x")
got := node.Render(NewContext())
if got != "text" {
@@ -192,7 +192,7 @@ func TestAttr_NonElement(t *testing.T) {
}
}
-func TestUnlessNode_True(t *testing.T) {
+func TestUnlessNode_True_Good(t *testing.T) {
ctx := NewContext()
node := Unless(func(*Context) bool { return true }, Raw("hidden"))
got := node.Render(ctx)
@@ -201,7 +201,7 @@ func TestUnlessNode_True(t *testing.T) {
}
}
-func TestAttr_ThroughIfNode(t *testing.T) {
+func TestAttr_ThroughIfNode_Good(t *testing.T) {
ctx := NewContext()
inner := El("div", Raw("content"))
node := If(func(*Context) bool { return true }, inner)
@@ -213,7 +213,7 @@ func TestAttr_ThroughIfNode(t *testing.T) {
}
}
-func TestAttr_ThroughUnlessNode(t *testing.T) {
+func TestAttr_ThroughUnlessNode_Good(t *testing.T) {
ctx := NewContext()
inner := El("div", Raw("content"))
node := Unless(func(*Context) bool { return false }, inner)
@@ -225,7 +225,7 @@ func TestAttr_ThroughUnlessNode(t *testing.T) {
}
}
-func TestAttr_ThroughEntitledNode(t *testing.T) {
+func TestAttr_ThroughEntitledNode_Good(t *testing.T) {
ctx := NewContext()
ctx.Entitlements = func(string) bool { return true }
inner := El("div", Raw("content"))
@@ -238,7 +238,7 @@ func TestAttr_ThroughEntitledNode(t *testing.T) {
}
}
-func TestTextNode_WithService(t *testing.T) {
+func TestTextNode_WithService_Good(t *testing.T) {
svc, _ := i18n.New()
ctx := NewContextWithService(svc)
node := Text("hello")
@@ -248,7 +248,7 @@ func TestTextNode_WithService(t *testing.T) {
}
}
-func TestSwitchNode(t *testing.T) {
+func TestSwitchNode_Good(t *testing.T) {
ctx := NewContext()
cases := map[string]Node{
"dark": Raw("dark theme"),
diff --git a/path.go b/path.go
index 509970c..dda1d7a 100644
--- a/path.go
+++ b/path.go
@@ -1,6 +1,7 @@
package html
// ParseBlockID extracts the slot sequence from a data-block ID.
+// Usage example: slots := ParseBlockID("L-0-C-0")
// "L-0-C-0" → ['L', 'C']
func ParseBlockID(id string) []byte {
if id == "" {
diff --git a/path_test.go b/path_test.go
index 0ddfa9c..f8b484b 100644
--- a/path_test.go
+++ b/path_test.go
@@ -4,7 +4,7 @@ import (
"testing"
)
-func TestNestedLayout_PathChain(t *testing.T) {
+func TestNestedLayout_PathChain_Good(t *testing.T) {
inner := NewLayout("HCF").H(Raw("inner h")).C(Raw("inner c")).F(Raw("inner f"))
outer := NewLayout("HLCRF").
H(Raw("header")).L(inner).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
@@ -25,7 +25,7 @@ func TestNestedLayout_PathChain(t *testing.T) {
}
}
-func TestNestedLayout_DeepNesting(t *testing.T) {
+func TestNestedLayout_DeepNesting_Ugly(t *testing.T) {
deepest := NewLayout("C").C(Raw("deep"))
middle := NewLayout("C").C(deepest)
outer := NewLayout("C").C(middle)
@@ -38,7 +38,7 @@ func TestNestedLayout_DeepNesting(t *testing.T) {
}
}
-func TestBlockID(t *testing.T) {
+func TestBlockID_Good(t *testing.T) {
tests := []struct {
path string
slot byte
@@ -59,7 +59,7 @@ func TestBlockID(t *testing.T) {
}
}
-func TestParseBlockID(t *testing.T) {
+func TestParseBlockID_Good(t *testing.T) {
tests := []struct {
id string
want []byte
diff --git a/pipeline.go b/pipeline.go
index 183e6a8..dd33593 100644
--- a/pipeline.go
+++ b/pipeline.go
@@ -9,6 +9,7 @@ import (
)
// StripTags removes HTML tags from rendered output, returning plain text.
+// Usage example: text := StripTags("