chore: polish ax v0.8.0 conventions
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b8d06460d6
commit
3616ad3a76
24 changed files with 181 additions and 114 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
52
edge_test.go
52
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 != "" {
|
||||
|
|
|
|||
1
go.mod
1
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
|
||||
)
|
||||
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
11
node.go
11
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("<strong>trusted</strong>")
|
||||
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}
|
||||
}
|
||||
|
|
|
|||
50
node_test.go
50
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("<script>alert('xss')</script>")
|
||||
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"),
|
||||
|
|
|
|||
1
path.go
1
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 == "" {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
// StripTags removes HTML tags from rendered output, returning plain text.
|
||||
// Usage example: text := StripTags("<main>Hello <strong>world</strong></main>")
|
||||
// Tag boundaries are collapsed into single spaces; result is trimmed.
|
||||
// Does not handle script/style element content (go-html does not generate these).
|
||||
func StripTags(html string) string {
|
||||
|
|
@ -45,6 +46,7 @@ func StripTags(html string) string {
|
|||
|
||||
// Imprint renders a node tree to HTML, strips tags, tokenises the text,
|
||||
// and returns a GrammarImprint — the full render-reverse pipeline.
|
||||
// Usage example: imp := Imprint(Text("welcome"), NewContext())
|
||||
func Imprint(node Node, ctx *Context) reversal.GrammarImprint {
|
||||
if ctx == nil {
|
||||
ctx = NewContext()
|
||||
|
|
@ -58,6 +60,7 @@ func Imprint(node Node, ctx *Context) reversal.GrammarImprint {
|
|||
|
||||
// CompareVariants runs the imprint pipeline on each responsive variant independently
|
||||
// and returns pairwise similarity scores. Key format: "name1:name2".
|
||||
// Usage example: scores := CompareVariants(NewResponsive(), NewContext())
|
||||
func CompareVariants(r *Responsive, ctx *Context) map[string]float64 {
|
||||
if ctx == nil {
|
||||
ctx = NewContext()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
i18n "dappco.re/go/core/i18n"
|
||||
)
|
||||
|
||||
func TestStripTags_Simple(t *testing.T) {
|
||||
func TestStripTags_Simple_Good(t *testing.T) {
|
||||
got := StripTags(`<div>hello</div>`)
|
||||
want := "hello"
|
||||
if got != want {
|
||||
|
|
@ -16,7 +16,7 @@ func TestStripTags_Simple(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStripTags_Nested(t *testing.T) {
|
||||
func TestStripTags_Nested_Good(t *testing.T) {
|
||||
got := StripTags(`<header role="banner"><h1>Title</h1></header>`)
|
||||
want := "Title"
|
||||
if got != want {
|
||||
|
|
@ -24,7 +24,7 @@ func TestStripTags_Nested(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStripTags_MultipleRegions(t *testing.T) {
|
||||
func TestStripTags_MultipleRegions_Good(t *testing.T) {
|
||||
got := StripTags(`<header>Head</header><main>Body</main><footer>Foot</footer>`)
|
||||
want := "Head Body Foot"
|
||||
if got != want {
|
||||
|
|
@ -32,21 +32,21 @@ func TestStripTags_MultipleRegions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStripTags_Empty(t *testing.T) {
|
||||
func TestStripTags_Empty_Ugly(t *testing.T) {
|
||||
got := StripTags("")
|
||||
if got != "" {
|
||||
t.Errorf("StripTags(\"\") = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_NoTags(t *testing.T) {
|
||||
func TestStripTags_NoTags_Good(t *testing.T) {
|
||||
got := StripTags("plain text")
|
||||
if got != "plain text" {
|
||||
t.Errorf("StripTags(plain) = %q, want %q", got, "plain text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_Entities(t *testing.T) {
|
||||
func TestStripTags_Entities_Good(t *testing.T) {
|
||||
got := StripTags(`<script>`)
|
||||
want := "<script>"
|
||||
if got != want {
|
||||
|
|
@ -54,7 +54,7 @@ func TestStripTags_Entities(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestImprint_FromNode(t *testing.T) {
|
||||
func TestImprint_FromNode_Good(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
|
@ -74,7 +74,7 @@ func TestImprint_FromNode(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestImprint_SimilarPages(t *testing.T) {
|
||||
func TestImprint_SimilarPages_Good(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
|
@ -102,7 +102,7 @@ func TestImprint_SimilarPages(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCompareVariants(t *testing.T) {
|
||||
func TestCompareVariants_Good(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package html
|
||||
|
||||
// Render is a convenience function that renders a node tree to HTML.
|
||||
// Usage example: html := Render(El("main", Text("welcome")), NewContext())
|
||||
func Render(node Node, ctx *Context) string {
|
||||
if ctx == nil {
|
||||
ctx = NewContext()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
i18n "dappco.re/go/core/i18n"
|
||||
)
|
||||
|
||||
func TestRender_FullPage(t *testing.T) {
|
||||
func TestRender_FullPage_Good(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
|
@ -49,7 +49,7 @@ func TestRender_FullPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRender_EntitlementGating(t *testing.T) {
|
||||
func TestRender_EntitlementGating_Good(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
|
@ -77,7 +77,7 @@ func TestRender_EntitlementGating(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRender_XSSPrevention(t *testing.T) {
|
||||
func TestRender_XSSPrevention_Good(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package html
|
||||
|
||||
// Responsive wraps multiple Layout variants for breakpoint-aware rendering.
|
||||
// Usage example: r := NewResponsive().Variant("mobile", NewLayout("C"))
|
||||
// Each variant is rendered inside a container with data-variant for CSS targeting.
|
||||
type Responsive struct {
|
||||
variants []responsiveVariant
|
||||
|
|
@ -12,11 +13,13 @@ type responsiveVariant struct {
|
|||
}
|
||||
|
||||
// NewResponsive creates a new multi-variant responsive compositor.
|
||||
// Usage example: r := NewResponsive()
|
||||
func NewResponsive() *Responsive {
|
||||
return &Responsive{}
|
||||
}
|
||||
|
||||
// Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
|
||||
// Usage example: NewResponsive().Variant("desktop", NewLayout("HLCRF"))
|
||||
// Variants render in insertion order.
|
||||
func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
|
||||
r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
|
||||
|
|
@ -24,6 +27,7 @@ func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
|
|||
}
|
||||
|
||||
// Render produces HTML with each variant in a data-variant container.
|
||||
// Usage example: html := NewResponsive().Variant("mobile", NewLayout("C")).Render(NewContext())
|
||||
func (r *Responsive) Render(ctx *Context) string {
|
||||
b := newTextBuilder()
|
||||
for _, v := range r.variants {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestResponsive_SingleVariant(t *testing.T) {
|
||||
func TestResponsive_SingleVariant_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").
|
||||
|
|
@ -19,7 +19,7 @@ func TestResponsive_SingleVariant(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResponsive_MultiVariant(t *testing.T) {
|
||||
func TestResponsive_MultiVariant_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").H(Raw("h")).L(Raw("l")).C(Raw("c")).R(Raw("r")).F(Raw("f"))).
|
||||
|
|
@ -35,7 +35,7 @@ func TestResponsive_MultiVariant(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResponsive_VariantOrder(t *testing.T) {
|
||||
func TestResponsive_VariantOrder_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").C(Raw("d"))).
|
||||
|
|
@ -53,7 +53,7 @@ func TestResponsive_VariantOrder(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResponsive_NestedPaths(t *testing.T) {
|
||||
func TestResponsive_NestedPaths_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
inner := NewLayout("HCF").H(Raw("ih")).C(Raw("ic")).F(Raw("if"))
|
||||
r := NewResponsive().
|
||||
|
|
@ -69,7 +69,7 @@ func TestResponsive_NestedPaths(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResponsive_VariantsIndependent(t *testing.T) {
|
||||
func TestResponsive_VariantsIndependent_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("a", NewLayout("HLCRF").C(Raw("content-a"))).
|
||||
|
|
@ -83,6 +83,6 @@ func TestResponsive_VariantsIndependent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResponsive_ImplementsNode(t *testing.T) {
|
||||
func TestResponsive_ImplementsNode_Ugly(t *testing.T) {
|
||||
var _ Node = NewResponsive()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue