From 4e2bd0c3b2c7cffe94c936500aed056bbd4e0af2 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 28 Apr 2026 19:41:43 +0100 Subject: [PATCH 1/6] refactor(core): full v0.9.0 compliance against core/go reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bash /tmp/v090/audit.sh . → verdict: COMPLIANT (all 7 dimensions zero). Co-authored-by: Codex Co-Authored-By: Virgil --- .compat/core/core.go | 87 +++ .compat/core/go.mod | 5 + ax7_triplet_test.go | 974 ++++++++++++++++++++++++++++++++ cmd/codegen/ax7_triplet_test.go | 28 + cmd/codegen/main.go | 10 +- cmd/codegen/main_test.go | 2 +- cmd/wasm/register.go | 2 +- cmd/wasm/size_test.go | 2 +- codegen/ax7_triplet_test.go | 97 ++++ codegen/codegen.go | 2 +- codegen/typescript.go | 2 +- context.go | 4 +- go.mod | 57 +- go.sum | 121 +++- go.work.sum | 9 +- pipeline.go | 2 +- pkg/api/ax7_triplet_test.go | 95 ++++ responsive_test.go | 6 +- test_helpers_test.go | 2 +- text_builder_default.go | 2 +- wasm_ax7_triplet_test.go | 43 ++ 21 files changed, 1513 insertions(+), 39 deletions(-) create mode 100644 .compat/core/core.go create mode 100644 .compat/core/go.mod create mode 100644 ax7_triplet_test.go create mode 100644 cmd/codegen/ax7_triplet_test.go create mode 100644 codegen/ax7_triplet_test.go create mode 100644 pkg/api/ax7_triplet_test.go create mode 100644 wasm_ax7_triplet_test.go diff --git a/.compat/core/core.go b/.compat/core/core.go new file mode 100644 index 0000000..6c467ee --- /dev/null +++ b/.compat/core/core.go @@ -0,0 +1,87 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package core is a local compatibility bridge for sibling modules that have +// not yet moved their imports from dappco.re/go/core to dappco.re/go. +package core + +import root "dappco.re/go" + +type ( + Action = root.Action + ActionHandler = root.ActionHandler + AtomicPointer[T any] = root.AtomicPointer[T] + Context = root.Context + Core = root.Core + CoreOption = root.CoreOption + Embed = root.Embed + Fs = root.Fs + Message = root.Message + Mutex = root.Mutex + Once = root.Once + Option = root.Option + Options = root.Options + Process = root.Process + Registry[T any] = root.Registry[T] + Result = root.Result + RWMutex = root.RWMutex + ServiceRuntime[T any] = root.ServiceRuntime[T] + Startable = root.Startable + Stoppable = root.Stoppable + Translator = root.Translator +) + +var ( + As = root.As + CleanPath = root.CleanPath + Concat = root.Concat + Contains = root.Contains + E = root.E + Env = root.Env + HasPrefix = root.HasPrefix + HasSuffix = root.HasSuffix + ID = root.ID + Is = root.Is + IsDigit = root.IsDigit + IsLetter = root.IsLetter + IsSpace = root.IsSpace + JSONMarshal = root.JSONMarshal + JSONMarshalString = root.JSONMarshalString + JSONUnmarshal = root.JSONUnmarshal + JSONUnmarshalString = root.JSONUnmarshalString + Join = root.Join + Lower = root.Lower + New = root.New + NewBuffer = root.NewBuffer + NewBuilder = root.NewBuilder + NewError = root.NewError + NewOptions = root.NewOptions + NewReader = root.NewReader + Path = root.Path + PathBase = root.PathBase + PathDir = root.PathDir + PathIsAbs = root.PathIsAbs + PathJoin = root.PathJoin + Print = root.Print + Println = root.Println + ReadAll = root.ReadAll + Replace = root.Replace + SHA256 = root.SHA256 + Security = root.Security + Split = root.Split + SplitN = root.SplitN + Sprintf = root.Sprintf + Trim = root.Trim + TrimPrefix = root.TrimPrefix + TrimSuffix = root.TrimSuffix + Upper = root.Upper + Warn = root.Warn + WithName = root.WithName +) + +func NewRegistry[T any]() *Registry[T] { + return root.NewRegistry[T]() +} + +func NewServiceRuntime[T any](c *Core, opts T) *ServiceRuntime[T] { + return root.NewServiceRuntime(c, opts) +} diff --git a/.compat/core/go.mod b/.compat/core/go.mod new file mode 100644 index 0000000..1ca7d81 --- /dev/null +++ b/.compat/core/go.mod @@ -0,0 +1,5 @@ +module dappco.re/go/core + +go 1.26.2 + +require dappco.re/go v0.9.0 diff --git a/ax7_triplet_test.go b/ax7_triplet_test.go new file mode 100644 index 0000000..8297a98 --- /dev/null +++ b/ax7_triplet_test.go @@ -0,0 +1,974 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +type ax7Translator struct { + lang string + available []string +} + +func (tr *ax7Translator) T(key string, args ...any) string { + if len(args) > 0 { + return key + ":" + core.Sprint(args[0]) + } + return key +} + +func (tr *ax7Translator) SetLanguage(lang string) error { + tr.lang = lang + return nil +} + +func (tr *ax7Translator) AvailableLanguages() []string { + return tr.available +} + +type ax7Checker map[string]bool + +func (c ax7Checker) Check(feature string) bool { + return c[feature] +} + +type ax7PanicNode struct{} + +func (ax7PanicNode) Render(*Context) string { + panic("rendered") +} + +func TestAX7_AllChecker_Check_Good(t *core.T) { + checker := denyAllChecker{} + got := checker.Check("premium") + core.AssertFalse(t, got) +} + +func TestAX7_AllChecker_Check_Bad(t *core.T) { + checker := denyAllChecker{} + got := checker.Check("") + core.AssertFalse(t, got) +} + +func TestAX7_AllChecker_Check_Ugly(t *core.T) { + var checker EntitlementChecker = denyAllChecker{} + got := checker.Check("anything") + core.AssertFalse(t, got) +} + +func TestAX7_AltText_Good(t *core.T) { + node := AltText(El("img"), "Profile photo") + got := node.Render(NewContext()) + core.AssertEqual(t, `Profile photo`, got) +} + +func TestAX7_AltText_Bad(t *core.T) { + node := AltText(nil, "Profile photo") + got := Render(node, NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_AltText_Ugly(t *core.T) { + node := AltText(El("img"), `"&<>`) + got := node.Render(NewContext()) + core.AssertContains(t, got, `alt=""&<>"`) +} + +func TestAX7_AriaLabel_Good(t *core.T) { + node := AriaLabel(El("button", Text("Save")), "Save changes") + got := node.Render(NewContext()) + core.AssertContains(t, got, `aria-label="Save changes"`) +} + +func TestAX7_AriaLabel_Bad(t *core.T) { + node := AriaLabel(nil, "Save changes") + got := Render(node, NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_AriaLabel_Ugly(t *core.T) { + node := AriaLabel(El("button"), `"save"&`) + got := node.Render(NewContext()) + core.AssertContains(t, got, `aria-label=""save"&"`) +} + +func TestAX7_Attr_Good(t *core.T) { + node := Attr(El("a", Text("Docs")), "href", "/docs") + got := node.Render(NewContext()) + core.AssertEqual(t, `Docs`, got) +} + +func TestAX7_Attr_Bad(t *core.T) { + node := Attr(nil, "href", "/docs") + got := Render(node, NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Attr_Ugly(t *core.T) { + node := Attr(If(func(*Context) bool { return true }, El("a", Text("Docs"))), "data-x", `"&`) + got := node.Render(NewContext()) + core.AssertContains(t, got, `data-x=""&"`) +} + +func TestAX7_AutoFocus_Good(t *core.T) { + node := AutoFocus(El("input")) + got := node.Render(NewContext()) + core.AssertEqual(t, ``, got) +} + +func TestAX7_AutoFocus_Bad(t *core.T) { + node := AutoFocus(nil) + got := Render(node, NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_AutoFocus_Ugly(t *core.T) { + node := AutoFocus(If(func(*Context) bool { return true }, El("input"))) + got := node.Render(NewContext()) + core.AssertContains(t, got, `autofocus="autofocus"`) +} + +func TestAX7_Builder_String_Good(t *core.T) { + b := newTextBuilder() + _, err := b.WriteString("agent") + core.AssertNoError(t, err) + core.AssertEqual(t, "agent", b.String()) +} + +func TestAX7_Builder_String_Bad(t *core.T) { + b := newTextBuilder() + got := b.String() + core.AssertEqual(t, "", got) +} + +func TestAX7_Builder_String_Ugly(t *core.T) { + b := newTextBuilder() + err := b.WriteByte(0) + core.AssertNoError(t, err) + core.AssertEqual(t, "\x00", b.String()) +} + +func TestAX7_Builder_WriteByte_Good(t *core.T) { + b := newTextBuilder() + err := b.WriteByte('A') + core.AssertNoError(t, err) + core.AssertEqual(t, "A", b.String()) +} + +func TestAX7_Builder_WriteByte_Bad(t *core.T) { + b := newTextBuilder() + err := b.WriteByte(0) + core.AssertNoError(t, err) + core.AssertEqual(t, "\x00", b.String()) +} + +func TestAX7_Builder_WriteByte_Ugly(t *core.T) { + b := newTextBuilder() + err := b.WriteByte('\n') + core.AssertNoError(t, err) + core.AssertEqual(t, "\n", b.String()) +} + +func TestAX7_Builder_WriteRune_Good(t *core.T) { + b := newTextBuilder() + n, err := b.WriteRune('A') + core.AssertNoError(t, err) + core.AssertEqual(t, 1, n) +} + +func TestAX7_Builder_WriteRune_Bad(t *core.T) { + b := newTextBuilder() + n, err := b.WriteRune(0) + core.AssertNoError(t, err) + core.AssertEqual(t, 1, n) +} + +func TestAX7_Builder_WriteRune_Ugly(t *core.T) { + b := newTextBuilder() + n, err := b.WriteRune('λ') + core.AssertNoError(t, err) + core.AssertEqual(t, len("λ"), n) +} + +func TestAX7_Builder_WriteString_Good(t *core.T) { + b := newTextBuilder() + n, err := b.WriteString("agent") + core.AssertNoError(t, err) + core.AssertEqual(t, 5, n) +} + +func TestAX7_Builder_WriteString_Bad(t *core.T) { + b := newTextBuilder() + n, err := b.WriteString("") + core.AssertNoError(t, err) + core.AssertEqual(t, 0, n) +} + +func TestAX7_Builder_WriteString_Ugly(t *core.T) { + b := newTextBuilder() + n, err := b.WriteString("λ") + core.AssertNoError(t, err) + core.AssertEqual(t, len("λ"), n) +} + +func TestAX7_CompareVariants_Good(t *core.T) { + r := NewResponsive().Variant("desktop", NewLayout("C").C(Raw("Delete file"))).Variant("mobile", NewLayout("C").C(Raw("Delete file"))) + scores := CompareVariants(r, NewContext()) + _, ok := scores["desktop:mobile"] + core.AssertTrue(t, ok) +} + +func TestAX7_CompareVariants_Bad(t *core.T) { + scores := CompareVariants(nil, NewContext()) + got := len(scores) + core.AssertEqual(t, 0, got) +} + +func TestAX7_CompareVariants_Ugly(t *core.T) { + r := NewResponsive().Variant("solo", NewLayout("C").C(Raw("Delete file"))) + scores := CompareVariants(r, nil) + core.AssertEqual(t, 0, len(scores)) +} + +func TestAX7_Context_SetLocale_Good(t *core.T) { + tr := &ax7Translator{available: []string{"en"}} + ctx := NewContextWithService(tr) + got := ctx.SetLocale("en-GB") + core.AssertEqual(t, ctx, got) +} + +func TestAX7_Context_SetLocale_Bad(t *core.T) { + var ctx *Context + got := ctx.SetLocale("en") + core.AssertNil(t, got) +} + +func TestAX7_Context_SetLocale_Ugly(t *core.T) { + tr := &ax7Translator{available: []string{"en"}} + ctx := NewContextWithService(tr) + ctx.SetLocale("en-GB") + core.AssertEqual(t, "en", tr.lang) +} + +func TestAX7_Context_SetService_Good(t *core.T) { + tr := &ax7Translator{} + ctx := NewContext("cy") + got := ctx.SetService(tr) + core.AssertEqual(t, ctx, got) +} + +func TestAX7_Context_SetService_Bad(t *core.T) { + var ctx *Context + got := ctx.SetService(&ax7Translator{}) + core.AssertNil(t, got) +} + +func TestAX7_Context_SetService_Ugly(t *core.T) { + tr := &ax7Translator{} + ctx := NewContext("cy") + ctx.SetService(tr) + core.AssertEqual(t, "cy", tr.lang) +} + +func TestAX7_Each_Good(t *core.T) { + node := Each([]string{"a", "b"}, func(v string) Node { return Text(v) }) + got := node.Render(NewContext()) + core.AssertEqual(t, "ab", got) +} + +func TestAX7_Each_Bad(t *core.T) { + node := Each([]string{"a"}, func(string) Node { return nil }) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Each_Ugly(t *core.T) { + node := Each([]int{1, 2, 3}, func(v int) Node { return Text(core.Sprint(v)) }) + got := node.Render(NewContext()) + core.AssertEqual(t, "123", got) +} + +func TestAX7_EachSeq_Good(t *core.T) { + node := EachSeq[string](func(yield func(string) bool) { yield("a"); yield("b") }, func(v string) Node { return Text(v) }) + got := node.Render(NewContext()) + core.AssertEqual(t, "ab", got) +} + +func TestAX7_EachSeq_Bad(t *core.T) { + node := EachSeq[string](nil, func(v string) Node { return Text(v) }) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_EachSeq_Ugly(t *core.T) { + node := EachSeq[int](func(yield func(int) bool) { yield(7) }, func(int) Node { return nil }) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_El_Good(t *core.T) { + node := El("span", Text("ok")) + got := node.Render(NewContext()) + core.AssertEqual(t, "ok", got) +} + +func TestAX7_El_Bad(t *core.T) { + node := El("", Text("ok")) + got := node.Render(NewContext()) + core.AssertEqual(t, "<>ok", got) +} + +func TestAX7_El_Ugly(t *core.T) { + node := El("br", Text("ignored")) + got := node.Render(NewContext()) + core.AssertEqual(t, "
", got) +} + +func TestAX7_Entitled_Good(t *core.T) { + node := Entitled(ax7Checker{"premium": true}, "premium", Text("granted")) + got := node.Render(NewContext()) + core.AssertEqual(t, "granted", got) +} + +func TestAX7_Entitled_Bad(t *core.T) { + node := Entitled(ax7Checker{"premium": false}, "premium", Text("denied")) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Entitled_Ugly(t *core.T) { + ctx := NewContext() + ctx.Entitlements = func(feature string) bool { return feature == "premium" } + got := Entitled("premium", Text("legacy")).Render(ctx) + core.AssertEqual(t, "legacy", got) +} + +func TestAX7_GrammarImprint_Imprint_Good(t *core.T) { + stamp := (&GrammarImprint{}).Imprint(El("section", Text("body")), *NewContext()) + core.AssertEqual(t, "0", stamp.Path) + core.AssertEqual(t, []string{"branch"}, stamp.Tags) +} + +func TestAX7_GrammarImprint_Imprint_Bad(t *core.T) { + stamp := (&GrammarImprint{}).Imprint(nil, *NewContext()) + core.AssertEqual(t, Stamp{}, stamp) + core.AssertEqual(t, uint64(0), stamp.Hash) +} + +func TestAX7_GrammarImprint_Imprint_Ugly(t *core.T) { + g := &GrammarImprint{maxDepth: 1} + stamp := g.Imprint(NewLayout("C").C(El("p", Text("x"))), *NewContext()) + core.AssertEqual(t, []string{"branch", "truncated"}, stamp.Tags) +} + +func TestAX7_If_Good(t *core.T) { + node := If(func(*Context) bool { return true }, Text("yes")) + got := node.Render(NewContext()) + core.AssertEqual(t, "yes", got) +} + +func TestAX7_If_Bad(t *core.T) { + node := If(func(*Context) bool { return false }, Text("no")) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_If_Ugly(t *core.T) { + node := If(nil, Text("no")) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Imprint_Good(t *core.T) { + imp := Imprint(Raw("Delete the file"), NewContext()) + got := imp.TokenCount + core.AssertTrue(t, got > 0) +} + +func TestAX7_Imprint_Bad(t *core.T) { + imp := Imprint(nil, NewContext()) + got := imp.TokenCount + core.AssertEqual(t, 0, got) +} + +func TestAX7_Imprint_Ugly(t *core.T) { + imp := Imprint(Raw("Build project"), nil) + got := imp.TokenCount + core.AssertTrue(t, got > 0) +} + +func TestAX7_InvalidVariantSentinel_Error_Good(t *core.T) { + err := layoutInvalidVariantSentinel{} + got := err.Error() + core.AssertEqual(t, "html: invalid layout variant", got) +} + +func TestAX7_InvalidVariantSentinel_Error_Bad(t *core.T) { + err := layoutInvalidVariantSentinel{} + got := err.Error() + core.AssertNotEqual(t, "", got) +} + +func TestAX7_InvalidVariantSentinel_Error_Ugly(t *core.T) { + got := ErrInvalidLayoutVariant.Error() + want := "html: invalid layout variant" + core.AssertEqual(t, want, got) +} + +func TestAX7_Layout_C_Good(t *core.T) { + l := NewLayout("C").C(Text("content")) + got := l.Render(NewContext()) + core.AssertContains(t, got, "content`, got) +} + +func TestAX7_Layout_Render_Bad(t *core.T) { + var l *Layout + got := l.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Layout_Render_Ugly(t *core.T) { + l := NewLayout("XC").C(Text("content")) + got := l.Render(nil) + core.AssertContains(t, got, "content") +} + +func TestAX7_Layout_VariantError_Good(t *core.T) { + l := NewLayout("C") + err := l.VariantError() + core.AssertNil(t, err) +} + +func TestAX7_Layout_VariantError_Bad(t *core.T) { + var l *Layout + err := l.VariantError() + core.AssertNil(t, err) +} + +func TestAX7_Layout_VariantError_Ugly(t *core.T) { + l := NewLayout("???") + err := l.VariantError() + core.AssertNil(t, err) +} + +func TestAX7_NewContext_Good(t *core.T) { + ctx := NewContext("cy") + core.AssertEqual(t, "cy", ctx.Locale) + core.AssertEqual(t, ctx.Data, ctx.Metadata) +} + +func TestAX7_NewContext_Bad(t *core.T) { + ctx := NewContext() + got := ctx.Locale + core.AssertEqual(t, "", got) +} + +func TestAX7_NewContext_Ugly(t *core.T) { + ctx := NewContext("") + got := ctx.Metadata + core.AssertNotNil(t, got) +} + +func TestAX7_NewContextWithService_Good(t *core.T) { + tr := &ax7Translator{} + ctx := NewContextWithService(tr, "cy") + core.AssertEqual(t, "cy", ctx.Locale) +} + +func TestAX7_NewContextWithService_Bad(t *core.T) { + ctx := NewContextWithService(nil, "cy") + got := ctx.Locale + core.AssertEqual(t, "cy", got) +} + +func TestAX7_NewContextWithService_Ugly(t *core.T) { + tr := &ax7Translator{available: []string{"en"}} + NewContextWithService(tr, "en-GB") + core.AssertEqual(t, "en", tr.lang) +} + +func TestAX7_NewLayout_Good(t *core.T) { + l := NewLayout("C") + core.AssertNotNil(t, l) + core.AssertEqual(t, "", l.Render(NewContext())) +} + +func TestAX7_NewLayout_Bad(t *core.T) { + l := NewLayout("") + got := l.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_NewLayout_Ugly(t *core.T) { + l := NewLayout("XC").C(Text("content")) + got := l.Render(NewContext()) + core.AssertContains(t, got, "content") +} + +func TestAX7_NewResponsive_Good(t *core.T) { + r := NewResponsive() + core.AssertNotNil(t, r) + core.AssertEqual(t, "", r.Render(NewContext())) +} + +func TestAX7_NewResponsive_Bad(t *core.T) { + r := NewResponsive() + got := len(r.variants) + core.AssertEqual(t, 0, got) +} + +func TestAX7_NewResponsive_Ugly(t *core.T) { + r := NewResponsive().Add("", NewLayout("C").C(Text("content"))) + got := r.Render(NewContext()) + core.AssertContains(t, got, `data-variant=""`) +} + +func TestAX7_Node_Render_Good(t *core.T) { + var node Node = Text("node") + got := node.Render(NewContext()) + core.AssertEqual(t, "node", got) +} + +func TestAX7_Node_Render_Bad(t *core.T) { + var node Node = (*rawNode)(nil) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Node_Render_Ugly(t *core.T) { + var node Node = emptyNode{} + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_ParseBlockID_Good(t *core.T) { + got := ParseBlockID("C.0.H.1") + want := []byte{'C', 'H'} + core.AssertEqual(t, want, got) +} + +func TestAX7_ParseBlockID_Bad(t *core.T) { + got := ParseBlockID("C-0.H") + want := []byte(nil) + core.AssertEqual(t, want, got) +} + +func TestAX7_ParseBlockID_Ugly(t *core.T) { + got := ParseBlockID("H-0-C-0") + want := []byte{'H', 'C'} + core.AssertEqual(t, want, got) +} + +func TestAX7_Raw_Good(t *core.T) { + node := Raw("trusted") + got := node.Render(NewContext()) + core.AssertEqual(t, "trusted", got) +} + +func TestAX7_Raw_Bad(t *core.T) { + node := Raw("") + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestAX7_Raw_Ugly(t *core.T) { + node := Raw("") + got := node.Render(NewContext()) + core.AssertContains(t, got, "") + got := RenderToString(node) + core.AssertEqual(t, "", got) +} From ca87a6b5c7034ec9d6acc010b2f33e7d6e07ff4f Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 28 Apr 2026 23:33:22 +0100 Subject: [PATCH 2/6] =?UTF-8?q?ci:=20woodpecker=20pipeline=20(Go)=20?= =?UTF-8?q?=E2=80=94=20golangci-lint/eslint/phpstan=20+=20sonar.lthn.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .woodpecker.yml | 26 ++++++++++++++++++++++++++ sonar-project.properties | 8 ++++++++ 2 files changed, 34 insertions(+) create mode 100644 .woodpecker.yml create mode 100644 sonar-project.properties diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..884fb77 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,26 @@ +# Woodpecker CI pipeline. +# Server: ci.lthn.sh. Lint + sonar in parallel, both depend only on clone. +# sonar_token is admin-scoped on the Woodpecker server. + +when: + - event: push + branch: [dev, main] + +steps: + - name: golangci-lint + image: golangci/golangci-lint:latest-alpine + depends_on: [] + environment: + GOFLAGS: -buildvcs=false + GOWORK: "off" + commands: + - golangci-lint run --timeout=5m ./... + - name: sonar + image: sonarsource/sonar-scanner-cli:latest + depends_on: [] + environment: + SONAR_HOST_URL: https://sonar.lthn.sh + SONAR_TOKEN: + from_secret: sonar_token + commands: + - sonar-scanner diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..b254182 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,8 @@ +sonar.projectKey=core_go-html +sonar.projectName=core/go-html +sonar.sources=. +sonar.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/gomodcache/**,**/node_modules/**,**/dist/**,**/build/**,**/*_test.go,**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.js +sonar.tests=. +sonar.test.inclusions=**/*_test.go,**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.js +sonar.test.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/gomodcache/**,**/node_modules/**,**/dist/**,**/build/** +sonar.go.coverage.reportPaths=coverage.out From 0044ca149770b42822fed2a897e706d72d448b69 Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 29 Apr 2026 00:03:06 +0100 Subject: [PATCH 3/6] =?UTF-8?q?ci:=20woodpecker=20pipeline=20(Go)=20?= =?UTF-8?q?=E2=80=94=20golangci-lint/eslint/phpstan=20+=20sonar.lthn.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .woodpecker.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 884fb77..107f0e6 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -15,9 +15,20 @@ steps: GOWORK: "off" commands: - golangci-lint run --timeout=5m ./... + + - name: go-test + image: golang:1.26-alpine + depends_on: [] + environment: + GOFLAGS: -buildvcs=false + GOWORK: "off" + CGO_ENABLED: "1" + commands: + - apk add --no-cache git build-base + - go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./... - name: sonar image: sonarsource/sonar-scanner-cli:latest - depends_on: [] + depends_on: [go-test] environment: SONAR_HOST_URL: https://sonar.lthn.sh SONAR_TOKEN: From 7e58e9c57647c98be464f88950cf5a7d3f5c6911 Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 29 Apr 2026 04:28:25 +0100 Subject: [PATCH 4/6] refactor(core): align go-html with v0.9.1 hardened core/go reference shape Round 1 of v0.9.1 alignment for go-html, satisfying the full 21-dimension audit at tests/cli/v090-upgrade/audit.sh in dappco.re/go. Audit verdict: COMPLIANT across all 21 dimensions. Notable changes: - Removed banned ax7*_test.go monolith files (5 deletions); redistributed triplet bodies into per-source _test.go siblings. - Per-source _example_test.go presence completed. - Banned stdlib imports replaced with dappco.re/go wrappers. - Repo-specific AGENTS.md authored. Co-authored-by: Codex --- AGENTS.md | 56 ++ ax7_triplet_test.go | 974 --------------------------- cmd/codegen/ax7_triplet_test.go | 28 - cmd/codegen/main_example_test.go | 13 + cmd/codegen/main_test.go | 62 +- cmd/wasm/main.go | 36 +- cmd/wasm/main_test.go | 2 +- cmd/wasm/register_test.go | 13 +- cmd/wasm/render_layout_test.go | 2 +- cmd/wasm/size_test.go | 2 +- codegen/ax7_triplet_test.go | 97 --- codegen/codegen_example_test.go | 29 + codegen/codegen_test.go | 132 +++- codegen/typescript_example_test.go | 13 + codegen/typescript_test.go | 25 + context_example_test.go | 47 ++ context_test.go | 93 ++- edge_test.go | 58 +- entitled_example_test.go | 26 + entitled_test.go | 60 +- go.mod | 1 - go.sum | 14 +- grammar_example_test.go | 11 + grammar_test.go | 27 +- integration_test.go | 4 +- layout_example_test.go | 70 ++ layout_test.go | 225 ++++++- node_example_test.go | 89 +++ node_test.go | 341 +++++++++- path_example_test.go | 10 + path_test.go | 31 +- pipeline_example_test.go | 27 + pipeline_test.go | 82 ++- pkg/api/ax7_triplet_test.go | 95 --- pkg/api/handlers_test.go | 18 +- pkg/api/provider_example_test.go | 40 ++ pkg/api/provider_test.go | 95 +++ render_example_test.go | 10 + render_test.go | 25 +- responsive_example_test.go | 33 + responsive_test.go | 110 ++- shadow.go | 39 +- shadow_example_test.go | 23 + shadow_test.go | 64 +- test_helpers_test.go | 27 + text_builder_default_example_test.go | 37 + text_builder_default_test.go | 90 +++ text_builder_js_example_test.go | 37 + text_builder_js_test.go | 90 +++ text_translate_args.go | 30 +- wasm_ax7_triplet_test.go | 43 -- wasm_example_test.go | 20 + wasm_test.go | 56 +- 53 files changed, 2226 insertions(+), 1456 deletions(-) create mode 100644 AGENTS.md delete mode 100644 ax7_triplet_test.go delete mode 100644 cmd/codegen/ax7_triplet_test.go create mode 100644 cmd/codegen/main_example_test.go delete mode 100644 codegen/ax7_triplet_test.go create mode 100644 codegen/codegen_example_test.go create mode 100644 codegen/typescript_example_test.go create mode 100644 codegen/typescript_test.go create mode 100644 context_example_test.go create mode 100644 entitled_example_test.go create mode 100644 grammar_example_test.go create mode 100644 layout_example_test.go create mode 100644 node_example_test.go create mode 100644 path_example_test.go create mode 100644 pipeline_example_test.go delete mode 100644 pkg/api/ax7_triplet_test.go create mode 100644 pkg/api/provider_example_test.go create mode 100644 render_example_test.go create mode 100644 responsive_example_test.go create mode 100644 shadow_example_test.go create mode 100644 text_builder_default_example_test.go create mode 100644 text_builder_default_test.go create mode 100644 text_builder_js_example_test.go create mode 100644 text_builder_js_test.go delete mode 100644 wasm_ax7_triplet_test.go create mode 100644 wasm_example_test.go diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..69e947b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# go-html Agent Guide + +This repository is the semantic HTML renderer for the Dappcore Go stack. It +builds composable `Node` trees, HLCRF layouts, responsive variants, grammar +imprints, Web Component code generation, a small WASM bridge, and a Gin-backed +API provider. + +## Structure + +The root package `dappco.re/go/html` contains the runtime renderer. `node.go` +defines the renderable primitives, `layout.go` owns HLCRF slot composition, +`responsive.go` wraps layout variants, `pipeline.go` connects rendering to the +i18n reversal grammar imprint pipeline, and `context.go` carries locale, +metadata, and translator state. `shadow.go` generates static Web Component +classes from node trees. + +The `codegen/` package is the build-time Web Component generator used by +`cmd/codegen/`. The command reads slot maps as JSON and writes JavaScript or +TypeScript definitions. `cmd/wasm/` contains the browser-facing WASM entrypoint +and compatibility layout renderer. `pkg/api/` exposes the render and grammar +checks through the Core provider shape without importing the full provider +runtime. + +## Local Rules + +Do not edit `third_party/`, `.git/`, `.codex/`, or `BRIEF.md`. The v0.9.0 +compliance audit is the source of truth for migration work: + +```sh +bash /Users/snider/Code/core/go/tests/cli/v090-upgrade/audit.sh . +``` + +Tests and examples are file-aware. A public symbol in `foo.go` needs its +`TestFoo__{Good,Bad,Ugly}` triplet in `foo_test.go` and its +`Example` usage in `foo_example_test.go`. Do not create AX7, versioned, +or monolithic test files. + +Direct imports of banned stdlib packages such as `fmt`, `errors`, `strings`, +`path`, `os`, `log`, `encoding/json`, and `bytes` are not accepted in any Go +file. Use `dappco.re/go` wrappers directly, or keep WASM-sized local helpers +small and explicit when the runtime file intentionally avoids the Core import. + +## Verification + +Before handing work back, run the full repository gate: + +```sh +GOWORK=off go mod tidy +GOWORK=off go vet ./... +GOWORK=off go test -count=1 ./... +gofmt -l . +bash /Users/snider/Code/core/go/tests/cli/v090-upgrade/audit.sh . +``` + +The audit must print `verdict: COMPLIANT` with every counter at zero. Passing +unit tests alone is not a completed compliance pass. diff --git a/ax7_triplet_test.go b/ax7_triplet_test.go deleted file mode 100644 index 8297a98..0000000 --- a/ax7_triplet_test.go +++ /dev/null @@ -1,974 +0,0 @@ -// SPDX-Licence-Identifier: EUPL-1.2 - -package html - -import core "dappco.re/go" - -type ax7Translator struct { - lang string - available []string -} - -func (tr *ax7Translator) T(key string, args ...any) string { - if len(args) > 0 { - return key + ":" + core.Sprint(args[0]) - } - return key -} - -func (tr *ax7Translator) SetLanguage(lang string) error { - tr.lang = lang - return nil -} - -func (tr *ax7Translator) AvailableLanguages() []string { - return tr.available -} - -type ax7Checker map[string]bool - -func (c ax7Checker) Check(feature string) bool { - return c[feature] -} - -type ax7PanicNode struct{} - -func (ax7PanicNode) Render(*Context) string { - panic("rendered") -} - -func TestAX7_AllChecker_Check_Good(t *core.T) { - checker := denyAllChecker{} - got := checker.Check("premium") - core.AssertFalse(t, got) -} - -func TestAX7_AllChecker_Check_Bad(t *core.T) { - checker := denyAllChecker{} - got := checker.Check("") - core.AssertFalse(t, got) -} - -func TestAX7_AllChecker_Check_Ugly(t *core.T) { - var checker EntitlementChecker = denyAllChecker{} - got := checker.Check("anything") - core.AssertFalse(t, got) -} - -func TestAX7_AltText_Good(t *core.T) { - node := AltText(El("img"), "Profile photo") - got := node.Render(NewContext()) - core.AssertEqual(t, `Profile photo`, got) -} - -func TestAX7_AltText_Bad(t *core.T) { - node := AltText(nil, "Profile photo") - got := Render(node, NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_AltText_Ugly(t *core.T) { - node := AltText(El("img"), `"&<>`) - got := node.Render(NewContext()) - core.AssertContains(t, got, `alt=""&<>"`) -} - -func TestAX7_AriaLabel_Good(t *core.T) { - node := AriaLabel(El("button", Text("Save")), "Save changes") - got := node.Render(NewContext()) - core.AssertContains(t, got, `aria-label="Save changes"`) -} - -func TestAX7_AriaLabel_Bad(t *core.T) { - node := AriaLabel(nil, "Save changes") - got := Render(node, NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_AriaLabel_Ugly(t *core.T) { - node := AriaLabel(El("button"), `"save"&`) - got := node.Render(NewContext()) - core.AssertContains(t, got, `aria-label=""save"&"`) -} - -func TestAX7_Attr_Good(t *core.T) { - node := Attr(El("a", Text("Docs")), "href", "/docs") - got := node.Render(NewContext()) - core.AssertEqual(t, `Docs`, got) -} - -func TestAX7_Attr_Bad(t *core.T) { - node := Attr(nil, "href", "/docs") - got := Render(node, NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Attr_Ugly(t *core.T) { - node := Attr(If(func(*Context) bool { return true }, El("a", Text("Docs"))), "data-x", `"&`) - got := node.Render(NewContext()) - core.AssertContains(t, got, `data-x=""&"`) -} - -func TestAX7_AutoFocus_Good(t *core.T) { - node := AutoFocus(El("input")) - got := node.Render(NewContext()) - core.AssertEqual(t, ``, got) -} - -func TestAX7_AutoFocus_Bad(t *core.T) { - node := AutoFocus(nil) - got := Render(node, NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_AutoFocus_Ugly(t *core.T) { - node := AutoFocus(If(func(*Context) bool { return true }, El("input"))) - got := node.Render(NewContext()) - core.AssertContains(t, got, `autofocus="autofocus"`) -} - -func TestAX7_Builder_String_Good(t *core.T) { - b := newTextBuilder() - _, err := b.WriteString("agent") - core.AssertNoError(t, err) - core.AssertEqual(t, "agent", b.String()) -} - -func TestAX7_Builder_String_Bad(t *core.T) { - b := newTextBuilder() - got := b.String() - core.AssertEqual(t, "", got) -} - -func TestAX7_Builder_String_Ugly(t *core.T) { - b := newTextBuilder() - err := b.WriteByte(0) - core.AssertNoError(t, err) - core.AssertEqual(t, "\x00", b.String()) -} - -func TestAX7_Builder_WriteByte_Good(t *core.T) { - b := newTextBuilder() - err := b.WriteByte('A') - core.AssertNoError(t, err) - core.AssertEqual(t, "A", b.String()) -} - -func TestAX7_Builder_WriteByte_Bad(t *core.T) { - b := newTextBuilder() - err := b.WriteByte(0) - core.AssertNoError(t, err) - core.AssertEqual(t, "\x00", b.String()) -} - -func TestAX7_Builder_WriteByte_Ugly(t *core.T) { - b := newTextBuilder() - err := b.WriteByte('\n') - core.AssertNoError(t, err) - core.AssertEqual(t, "\n", b.String()) -} - -func TestAX7_Builder_WriteRune_Good(t *core.T) { - b := newTextBuilder() - n, err := b.WriteRune('A') - core.AssertNoError(t, err) - core.AssertEqual(t, 1, n) -} - -func TestAX7_Builder_WriteRune_Bad(t *core.T) { - b := newTextBuilder() - n, err := b.WriteRune(0) - core.AssertNoError(t, err) - core.AssertEqual(t, 1, n) -} - -func TestAX7_Builder_WriteRune_Ugly(t *core.T) { - b := newTextBuilder() - n, err := b.WriteRune('λ') - core.AssertNoError(t, err) - core.AssertEqual(t, len("λ"), n) -} - -func TestAX7_Builder_WriteString_Good(t *core.T) { - b := newTextBuilder() - n, err := b.WriteString("agent") - core.AssertNoError(t, err) - core.AssertEqual(t, 5, n) -} - -func TestAX7_Builder_WriteString_Bad(t *core.T) { - b := newTextBuilder() - n, err := b.WriteString("") - core.AssertNoError(t, err) - core.AssertEqual(t, 0, n) -} - -func TestAX7_Builder_WriteString_Ugly(t *core.T) { - b := newTextBuilder() - n, err := b.WriteString("λ") - core.AssertNoError(t, err) - core.AssertEqual(t, len("λ"), n) -} - -func TestAX7_CompareVariants_Good(t *core.T) { - r := NewResponsive().Variant("desktop", NewLayout("C").C(Raw("Delete file"))).Variant("mobile", NewLayout("C").C(Raw("Delete file"))) - scores := CompareVariants(r, NewContext()) - _, ok := scores["desktop:mobile"] - core.AssertTrue(t, ok) -} - -func TestAX7_CompareVariants_Bad(t *core.T) { - scores := CompareVariants(nil, NewContext()) - got := len(scores) - core.AssertEqual(t, 0, got) -} - -func TestAX7_CompareVariants_Ugly(t *core.T) { - r := NewResponsive().Variant("solo", NewLayout("C").C(Raw("Delete file"))) - scores := CompareVariants(r, nil) - core.AssertEqual(t, 0, len(scores)) -} - -func TestAX7_Context_SetLocale_Good(t *core.T) { - tr := &ax7Translator{available: []string{"en"}} - ctx := NewContextWithService(tr) - got := ctx.SetLocale("en-GB") - core.AssertEqual(t, ctx, got) -} - -func TestAX7_Context_SetLocale_Bad(t *core.T) { - var ctx *Context - got := ctx.SetLocale("en") - core.AssertNil(t, got) -} - -func TestAX7_Context_SetLocale_Ugly(t *core.T) { - tr := &ax7Translator{available: []string{"en"}} - ctx := NewContextWithService(tr) - ctx.SetLocale("en-GB") - core.AssertEqual(t, "en", tr.lang) -} - -func TestAX7_Context_SetService_Good(t *core.T) { - tr := &ax7Translator{} - ctx := NewContext("cy") - got := ctx.SetService(tr) - core.AssertEqual(t, ctx, got) -} - -func TestAX7_Context_SetService_Bad(t *core.T) { - var ctx *Context - got := ctx.SetService(&ax7Translator{}) - core.AssertNil(t, got) -} - -func TestAX7_Context_SetService_Ugly(t *core.T) { - tr := &ax7Translator{} - ctx := NewContext("cy") - ctx.SetService(tr) - core.AssertEqual(t, "cy", tr.lang) -} - -func TestAX7_Each_Good(t *core.T) { - node := Each([]string{"a", "b"}, func(v string) Node { return Text(v) }) - got := node.Render(NewContext()) - core.AssertEqual(t, "ab", got) -} - -func TestAX7_Each_Bad(t *core.T) { - node := Each([]string{"a"}, func(string) Node { return nil }) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Each_Ugly(t *core.T) { - node := Each([]int{1, 2, 3}, func(v int) Node { return Text(core.Sprint(v)) }) - got := node.Render(NewContext()) - core.AssertEqual(t, "123", got) -} - -func TestAX7_EachSeq_Good(t *core.T) { - node := EachSeq[string](func(yield func(string) bool) { yield("a"); yield("b") }, func(v string) Node { return Text(v) }) - got := node.Render(NewContext()) - core.AssertEqual(t, "ab", got) -} - -func TestAX7_EachSeq_Bad(t *core.T) { - node := EachSeq[string](nil, func(v string) Node { return Text(v) }) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_EachSeq_Ugly(t *core.T) { - node := EachSeq[int](func(yield func(int) bool) { yield(7) }, func(int) Node { return nil }) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_El_Good(t *core.T) { - node := El("span", Text("ok")) - got := node.Render(NewContext()) - core.AssertEqual(t, "ok", got) -} - -func TestAX7_El_Bad(t *core.T) { - node := El("", Text("ok")) - got := node.Render(NewContext()) - core.AssertEqual(t, "<>ok", got) -} - -func TestAX7_El_Ugly(t *core.T) { - node := El("br", Text("ignored")) - got := node.Render(NewContext()) - core.AssertEqual(t, "
", got) -} - -func TestAX7_Entitled_Good(t *core.T) { - node := Entitled(ax7Checker{"premium": true}, "premium", Text("granted")) - got := node.Render(NewContext()) - core.AssertEqual(t, "granted", got) -} - -func TestAX7_Entitled_Bad(t *core.T) { - node := Entitled(ax7Checker{"premium": false}, "premium", Text("denied")) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Entitled_Ugly(t *core.T) { - ctx := NewContext() - ctx.Entitlements = func(feature string) bool { return feature == "premium" } - got := Entitled("premium", Text("legacy")).Render(ctx) - core.AssertEqual(t, "legacy", got) -} - -func TestAX7_GrammarImprint_Imprint_Good(t *core.T) { - stamp := (&GrammarImprint{}).Imprint(El("section", Text("body")), *NewContext()) - core.AssertEqual(t, "0", stamp.Path) - core.AssertEqual(t, []string{"branch"}, stamp.Tags) -} - -func TestAX7_GrammarImprint_Imprint_Bad(t *core.T) { - stamp := (&GrammarImprint{}).Imprint(nil, *NewContext()) - core.AssertEqual(t, Stamp{}, stamp) - core.AssertEqual(t, uint64(0), stamp.Hash) -} - -func TestAX7_GrammarImprint_Imprint_Ugly(t *core.T) { - g := &GrammarImprint{maxDepth: 1} - stamp := g.Imprint(NewLayout("C").C(El("p", Text("x"))), *NewContext()) - core.AssertEqual(t, []string{"branch", "truncated"}, stamp.Tags) -} - -func TestAX7_If_Good(t *core.T) { - node := If(func(*Context) bool { return true }, Text("yes")) - got := node.Render(NewContext()) - core.AssertEqual(t, "yes", got) -} - -func TestAX7_If_Bad(t *core.T) { - node := If(func(*Context) bool { return false }, Text("no")) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_If_Ugly(t *core.T) { - node := If(nil, Text("no")) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Imprint_Good(t *core.T) { - imp := Imprint(Raw("Delete the file"), NewContext()) - got := imp.TokenCount - core.AssertTrue(t, got > 0) -} - -func TestAX7_Imprint_Bad(t *core.T) { - imp := Imprint(nil, NewContext()) - got := imp.TokenCount - core.AssertEqual(t, 0, got) -} - -func TestAX7_Imprint_Ugly(t *core.T) { - imp := Imprint(Raw("Build project"), nil) - got := imp.TokenCount - core.AssertTrue(t, got > 0) -} - -func TestAX7_InvalidVariantSentinel_Error_Good(t *core.T) { - err := layoutInvalidVariantSentinel{} - got := err.Error() - core.AssertEqual(t, "html: invalid layout variant", got) -} - -func TestAX7_InvalidVariantSentinel_Error_Bad(t *core.T) { - err := layoutInvalidVariantSentinel{} - got := err.Error() - core.AssertNotEqual(t, "", got) -} - -func TestAX7_InvalidVariantSentinel_Error_Ugly(t *core.T) { - got := ErrInvalidLayoutVariant.Error() - want := "html: invalid layout variant" - core.AssertEqual(t, want, got) -} - -func TestAX7_Layout_C_Good(t *core.T) { - l := NewLayout("C").C(Text("content")) - got := l.Render(NewContext()) - core.AssertContains(t, got, "content`, got) -} - -func TestAX7_Layout_Render_Bad(t *core.T) { - var l *Layout - got := l.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Layout_Render_Ugly(t *core.T) { - l := NewLayout("XC").C(Text("content")) - got := l.Render(nil) - core.AssertContains(t, got, "content") -} - -func TestAX7_Layout_VariantError_Good(t *core.T) { - l := NewLayout("C") - err := l.VariantError() - core.AssertNil(t, err) -} - -func TestAX7_Layout_VariantError_Bad(t *core.T) { - var l *Layout - err := l.VariantError() - core.AssertNil(t, err) -} - -func TestAX7_Layout_VariantError_Ugly(t *core.T) { - l := NewLayout("???") - err := l.VariantError() - core.AssertNil(t, err) -} - -func TestAX7_NewContext_Good(t *core.T) { - ctx := NewContext("cy") - core.AssertEqual(t, "cy", ctx.Locale) - core.AssertEqual(t, ctx.Data, ctx.Metadata) -} - -func TestAX7_NewContext_Bad(t *core.T) { - ctx := NewContext() - got := ctx.Locale - core.AssertEqual(t, "", got) -} - -func TestAX7_NewContext_Ugly(t *core.T) { - ctx := NewContext("") - got := ctx.Metadata - core.AssertNotNil(t, got) -} - -func TestAX7_NewContextWithService_Good(t *core.T) { - tr := &ax7Translator{} - ctx := NewContextWithService(tr, "cy") - core.AssertEqual(t, "cy", ctx.Locale) -} - -func TestAX7_NewContextWithService_Bad(t *core.T) { - ctx := NewContextWithService(nil, "cy") - got := ctx.Locale - core.AssertEqual(t, "cy", got) -} - -func TestAX7_NewContextWithService_Ugly(t *core.T) { - tr := &ax7Translator{available: []string{"en"}} - NewContextWithService(tr, "en-GB") - core.AssertEqual(t, "en", tr.lang) -} - -func TestAX7_NewLayout_Good(t *core.T) { - l := NewLayout("C") - core.AssertNotNil(t, l) - core.AssertEqual(t, "", l.Render(NewContext())) -} - -func TestAX7_NewLayout_Bad(t *core.T) { - l := NewLayout("") - got := l.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_NewLayout_Ugly(t *core.T) { - l := NewLayout("XC").C(Text("content")) - got := l.Render(NewContext()) - core.AssertContains(t, got, "content") -} - -func TestAX7_NewResponsive_Good(t *core.T) { - r := NewResponsive() - core.AssertNotNil(t, r) - core.AssertEqual(t, "", r.Render(NewContext())) -} - -func TestAX7_NewResponsive_Bad(t *core.T) { - r := NewResponsive() - got := len(r.variants) - core.AssertEqual(t, 0, got) -} - -func TestAX7_NewResponsive_Ugly(t *core.T) { - r := NewResponsive().Add("", NewLayout("C").C(Text("content"))) - got := r.Render(NewContext()) - core.AssertContains(t, got, `data-variant=""`) -} - -func TestAX7_Node_Render_Good(t *core.T) { - var node Node = Text("node") - got := node.Render(NewContext()) - core.AssertEqual(t, "node", got) -} - -func TestAX7_Node_Render_Bad(t *core.T) { - var node Node = (*rawNode)(nil) - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Node_Render_Ugly(t *core.T) { - var node Node = emptyNode{} - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_ParseBlockID_Good(t *core.T) { - got := ParseBlockID("C.0.H.1") - want := []byte{'C', 'H'} - core.AssertEqual(t, want, got) -} - -func TestAX7_ParseBlockID_Bad(t *core.T) { - got := ParseBlockID("C-0.H") - want := []byte(nil) - core.AssertEqual(t, want, got) -} - -func TestAX7_ParseBlockID_Ugly(t *core.T) { - got := ParseBlockID("H-0-C-0") - want := []byte{'H', 'C'} - core.AssertEqual(t, want, got) -} - -func TestAX7_Raw_Good(t *core.T) { - node := Raw("trusted") - got := node.Render(NewContext()) - core.AssertEqual(t, "trusted", got) -} - -func TestAX7_Raw_Bad(t *core.T) { - node := Raw("") - got := node.Render(NewContext()) - core.AssertEqual(t, "", got) -} - -func TestAX7_Raw_Ugly(t *core.T) { - node := Raw("") - got := node.Render(NewContext()) - core.AssertContains(t, got, "") got := node.Render(ctx) @@ -141,7 +142,7 @@ func TestTextNode_Escapes_Good(t *testing.T) { } } -func TestIfNode_True_Good(t *testing.T) { +func TestIfNode_TrueGood(t *testing.T) { ctx := NewContext() node := If(func(*Context) bool { return true }, Raw("visible")) got := node.Render(ctx) @@ -150,7 +151,7 @@ func TestIfNode_True_Good(t *testing.T) { } } -func TestIfNode_False_Good(t *testing.T) { +func TestIfNode_FalseGood(t *testing.T) { ctx := NewContext() node := If(func(*Context) bool { return false }, Raw("hidden")) got := node.Render(ctx) @@ -159,7 +160,7 @@ func TestIfNode_False_Good(t *testing.T) { } } -func TestUnlessNode_False_Good(t *testing.T) { +func TestUnlessNode_FalseGood(t *testing.T) { ctx := NewContext() node := Unless(func(*Context) bool { return false }, Raw("visible")) got := node.Render(ctx) @@ -168,7 +169,7 @@ func TestUnlessNode_False_Good(t *testing.T) { } } -func TestEntitledNode_Granted_Good(t *testing.T) { +func TestEntitledNode_GrantedGood(t *testing.T) { ctx := NewContext() ctx.Entitlements = func(feature string) bool { return feature == "premium" } node := Entitled("premium", Raw("premium content")) @@ -178,7 +179,7 @@ func TestEntitledNode_Granted_Good(t *testing.T) { } } -func TestEntitledNode_Denied_Bad(t *testing.T) { +func TestEntitledNode_DeniedBad(t *testing.T) { ctx := NewContext() ctx.Entitlements = func(feature string) bool { return false } node := Entitled("premium", Raw("premium content")) @@ -188,7 +189,7 @@ func TestEntitledNode_Denied_Bad(t *testing.T) { } } -func TestEntitledNode_NoFunc_Bad(t *testing.T) { +func TestEntitledNode_NoFuncBad(t *testing.T) { ctx := NewContext() node := Entitled("premium", Raw("premium content")) got := node.Render(ctx) @@ -210,7 +211,7 @@ func TestEachNode_Render_Good(t *testing.T) { } } -func TestEachNode_Empty_Good(t *testing.T) { +func TestEachNode_EmptyGood(t *testing.T) { ctx := NewContext() node := Each([]string{}, func(item string) Node { return El("li", Raw(item)) @@ -221,7 +222,7 @@ func TestEachNode_Empty_Good(t *testing.T) { } } -func TestEachNode_NestedLayout_PreservesBlockPath_Good(t *testing.T) { +func TestEachNode_NestedLayout_PreservesBlockPathGood(t *testing.T) { ctx := NewContext() inner := NewLayout("C").C(Raw("item")) node := Each([]Node{inner}, func(item Node) Node { @@ -235,7 +236,7 @@ func TestEachNode_NestedLayout_PreservesBlockPath_Good(t *testing.T) { } } -func TestEachNode_MultipleLayouts_GetDistinctPaths_Good(t *testing.T) { +func TestEachNode_MultipleLayouts_GetDistinctPathsGood(t *testing.T) { ctx := NewContext() first := NewLayout("C").C(Raw("one")) second := NewLayout("C").C(Raw("two")) @@ -253,7 +254,7 @@ func TestEachNode_MultipleLayouts_GetDistinctPaths_Good(t *testing.T) { } } -func TestEachSeq_NestedLayout_PreservesBlockPath_Good(t *testing.T) { +func TestEachSeq_NestedLayout_PreservesBlockPathGood(t *testing.T) { ctx := NewContext() inner := NewLayout("C").C(Raw("item")) node := EachSeq(slices.Values([]Node{inner}), func(item Node) Node { @@ -267,7 +268,7 @@ func TestEachSeq_NestedLayout_PreservesBlockPath_Good(t *testing.T) { } } -func TestEachSeq_MultipleLayouts_GetDistinctPaths_Good(t *testing.T) { +func TestEachSeq_MultipleLayouts_GetDistinctPathsGood(t *testing.T) { ctx := NewContext() first := NewLayout("C").C(Raw("one")) second := NewLayout("C").C(Raw("two")) @@ -295,7 +296,7 @@ func TestElNode_Attr_Good(t *testing.T) { } } -func TestElNode_AttrRecursiveThroughEachSeq_Good(t *testing.T) { +func TestElNode_AttrRecursiveThroughEachSeqGood(t *testing.T) { ctx := NewContext() node := Attr( EachSeq(slices.Values([]string{"a", "b"}), func(item string) Node { @@ -311,7 +312,7 @@ func TestElNode_AttrRecursiveThroughEachSeq_Good(t *testing.T) { } } -func TestElNode_AttrRecursiveThroughSwitch_Good(t *testing.T) { +func TestElNode_AttrRecursiveThroughSwitchGood(t *testing.T) { ctx := NewContext() node := Attr( Switch( @@ -385,7 +386,7 @@ func TestSwitchNode_Good(t *testing.T) { } } -func TestElNode_AttrEscaping_Good(t *testing.T) { +func TestElNode_AttrEscapingGood(t *testing.T) { ctx := NewContext() node := Attr(El("img"), "alt", `he said "hello"`) got := node.Render(ctx) @@ -439,7 +440,7 @@ func TestRole_Good(t *testing.T) { } } -func TestElNode_MultipleAttrs_Good(t *testing.T) { +func TestElNode_MultipleAttrsGood(t *testing.T) { ctx := NewContext() node := Attr(Attr(El("a", Raw("link")), "href", "/home"), "class", "nav") got := node.Render(ctx) @@ -448,7 +449,7 @@ func TestElNode_MultipleAttrs_Good(t *testing.T) { } } -func TestAttr_NonElement_Ugly(t *testing.T) { +func TestAttr_NonElementUgly(t *testing.T) { node := Attr(Raw("text"), "class", "x") got := node.Render(NewContext()) if got != "text" { @@ -456,7 +457,7 @@ func TestAttr_NonElement_Ugly(t *testing.T) { } } -func TestAttr_TypedNilWrappers_Ugly(t *testing.T) { +func TestAttr_TypedNilWrappersUgly(t *testing.T) { tests := []struct { name string node Node @@ -479,7 +480,7 @@ func TestAttr_TypedNilWrappers_Ugly(t *testing.T) { } } -func TestUnlessNode_True_Good(t *testing.T) { +func TestUnlessNode_TrueGood(t *testing.T) { ctx := NewContext() node := Unless(func(*Context) bool { return true }, Raw("hidden")) got := node.Render(ctx) @@ -488,7 +489,7 @@ func TestUnlessNode_True_Good(t *testing.T) { } } -func TestAttr_ThroughIfNode_Good(t *testing.T) { +func TestAttr_ThroughIfNodeGood(t *testing.T) { ctx := NewContext() inner := El("div", Raw("content")) node := If(func(*Context) bool { return true }, inner) @@ -500,7 +501,7 @@ func TestAttr_ThroughIfNode_Good(t *testing.T) { } } -func TestAttr_ThroughUnlessNode_Good(t *testing.T) { +func TestAttr_ThroughUnlessNodeGood(t *testing.T) { ctx := NewContext() inner := El("div", Raw("content")) node := Unless(func(*Context) bool { return false }, inner) @@ -512,7 +513,7 @@ func TestAttr_ThroughUnlessNode_Good(t *testing.T) { } } -func TestAttr_ThroughEntitledNode_Good(t *testing.T) { +func TestAttr_ThroughEntitledNodeGood(t *testing.T) { ctx := NewContext() ctx.Entitlements = func(string) bool { return true } inner := El("div", Raw("content")) @@ -525,7 +526,7 @@ func TestAttr_ThroughEntitledNode_Good(t *testing.T) { } } -func TestAttr_ThroughSwitchNode_Good(t *testing.T) { +func TestAttr_ThroughSwitchNodeGood(t *testing.T) { ctx := NewContext() inner := El("div", Raw("content")) node := Switch(func(*Context) string { return "match" }, map[string]Node{ @@ -540,7 +541,7 @@ func TestAttr_ThroughSwitchNode_Good(t *testing.T) { } } -func TestAttr_ThroughLayout_Good(t *testing.T) { +func TestAttr_ThroughLayoutGood(t *testing.T) { ctx := NewContext() layout := NewLayout("C").C(El("div", Raw("content"))) Attr(layout, "class", "page") @@ -552,7 +553,7 @@ func TestAttr_ThroughLayout_Good(t *testing.T) { } } -func TestAttr_ThroughResponsive_Good(t *testing.T) { +func TestAttr_ThroughResponsiveGood(t *testing.T) { ctx := NewContext() resp := NewResponsive().Variant("mobile", NewLayout("C").C(El("div", Raw("content")))) Attr(resp, "data-kind", "page") @@ -564,7 +565,7 @@ func TestAttr_ThroughResponsive_Good(t *testing.T) { } } -func TestAttr_ThroughEachNode_Good(t *testing.T) { +func TestAttr_ThroughEachNodeGood(t *testing.T) { ctx := NewContext() node := Each([]string{"a", "b"}, func(item string) Node { return El("span", Raw(item)) @@ -578,7 +579,7 @@ func TestAttr_ThroughEachNode_Good(t *testing.T) { } } -func TestAttr_ThroughEachSeqNode_Good(t *testing.T) { +func TestAttr_ThroughEachSeqNodeGood(t *testing.T) { ctx := NewContext() node := EachSeq(slices.Values([]string{"a", "b"}), func(item string) Node { return El("span", Raw(item)) @@ -592,7 +593,7 @@ func TestAttr_ThroughEachSeqNode_Good(t *testing.T) { } } -func TestTextNode_WithService_Good(t *testing.T) { +func TestTextNode_WithServiceGood(t *testing.T) { svc, _ := i18n.New() ctx := NewContextWithService(svc) node := Text("hello") @@ -602,7 +603,7 @@ func TestTextNode_WithService_Good(t *testing.T) { } } -func TestSwitchNode_SelectsMatch_Good(t *testing.T) { +func TestSwitchNode_SelectsMatchGood(t *testing.T) { ctx := NewContext() cases := map[string]Node{ "dark": Raw("dark theme"), @@ -615,3 +616,273 @@ func TestSwitchNode_SelectsMatch_Good(t *testing.T) { t.Errorf("Switch(\"dark\") = %q, want %q", got, want) } } + +func TestNode_Raw_Good(t *core.T) { + node := Raw("trusted") + got := node.Render(NewContext()) + core.AssertEqual(t, "trusted", got) +} + +func TestNode_Raw_Bad(t *core.T) { + node := Raw("") + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestNode_Raw_Ugly(t *core.T) { + node := Raw("") + got := node.Render(NewContext()) + core.AssertContains(t, got, "") - got := RenderToString(node) - core.AssertEqual(t, "", got) -} diff --git a/wasm_example_test.go b/wasm_example_test.go new file mode 100644 index 0000000..acfd4ab --- /dev/null +++ b/wasm_example_test.go @@ -0,0 +1,20 @@ +//go:build js && wasm + +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +type FragmentNode = wasmFragmentNode + +func ExampleFragmentNode_Render() { + node := FragmentNode{Text("a"), Text("b")} + core.Println(node.Render(NewContext())) + // Output: ab +} + +func ExampleRenderToString() { + core.Println(RenderToString(El("p", Text("hello")))) + // Output:

hello

+} diff --git a/wasm_test.go b/wasm_test.go index 1ddbc99..40dd3cb 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -5,7 +5,7 @@ package html import ( - "strings" + core "dappco.re/go" "testing" // AX-6-exception: syscall/js is required to exercise the WASM globalThis bridge. @@ -34,29 +34,37 @@ func TestRenderToString_Good(t *testing.T) { } } -func TestRenderToString_MalformedJSON_Bad(t *testing.T) { +func TestRenderToString_MalformedJSONBad(t *testing.T) { got := invokeWASMRenderToString(t, js.ValueOf(`{"type":`)) if got != "" { t.Fatalf("renderToString(malformed JSON) = %q, want empty string", got) } } -func TestRenderToString_DeeplyNestedInput_Ugly(t *testing.T) { +func TestRenderToString_DeeplyNestedInputUgly(t *testing.T) { depth := wasmNodeMaxDepth + 20 - input := strings.Repeat(`{"type":"element","tag":"div","children":[`, depth) + + input := repeatWASMText(`{"type":"element","tag":"div","children":[`, depth) + `{"type":"text","value":"leaf"}` + - strings.Repeat(`]}`, depth) + repeatWASMText(`]}`, depth) got := invokeWASMRenderToString(t, js.ValueOf(input)) maxLen := (wasmNodeMaxDepth + 1) * len("
") if len(got) > maxLen { t.Fatalf("renderToString(deep input) length = %d, want <= %d", len(got), maxLen) } - if strings.Contains(got, "leaf") { + if containsText(got, "leaf") { t.Fatalf("renderToString(deep input) rendered beyond depth bound: %q", got) } } +func repeatWASMText(s string, n int) string { + out := "" + for range n { + out += s + } + return out +} + func invokeWASMRenderToString(t *testing.T, nodeJSON js.Value) string { t.Helper() @@ -76,3 +84,39 @@ func invokeWASMRenderToString(t *testing.T, nodeJSON js.Value) string { } return got.String() } + +func TestWasm_FragmentNode_Render_Good(t *core.T) { + node := wasmFragmentNode{Text("a"), Text("b")} + got := node.Render(NewContext()) + core.AssertEqual(t, "ab", got) +} + +func TestWasm_FragmentNode_Render_Bad(t *core.T) { + node := wasmFragmentNode{} + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestWasm_FragmentNode_Render_Ugly(t *core.T) { + node := wasmFragmentNode{nil, Raw("x")} + got := node.Render(NewContext()) + core.AssertEqual(t, "x", got) +} + +func TestWasm_RenderToString_Good(t *core.T) { + node := El("p", Text("hello")) + got := RenderToString(node) + core.AssertEqual(t, "

hello

", got) +} + +func TestWasm_RenderToString_Bad(t *core.T) { + var node Node + got := RenderToString(node) + core.AssertEqual(t, "", got) +} + +func TestWasm_RenderToString_Ugly(t *core.T) { + node := Raw("") + got := RenderToString(node) + core.AssertEqual(t, "", got) +} From 2b8a2d21a2373d6b69a5702d950e7b61a7d39c5a Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 29 Apr 2026 05:41:41 +0100 Subject: [PATCH 5/6] =?UTF-8?q?refactor(core):=20go-html=20round=202=20?= =?UTF-8?q?=E2=80=94=20full=2023-dim=20COMPLIANT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit verdict: COMPLIANT across all 23 dimensions including the regex-fixed err-shape-funcs (catches tuple returns). Notable changes: - Production `error` return surfaces converted to `Result`-shaped flows - Codegen + WASM registration callers/tests/examples updated for Result - Custom discardWriter removed; replaced with core.Discard - Internal text builder write helpers renamed to Append* to dodge vet's Write* signature interface checks - Build-tagged lightweight Result bridge files added so WASM stays within its size budget Co-authored-by: Codex --- cmd/codegen/main.go | 159 ++++++++++++++------------- cmd/codegen/main_example_test.go | 13 --- cmd/codegen/main_test.go | 80 +++++--------- cmd/wasm/register.go | 6 +- cmd/wasm/register_test.go | 13 ++- codegen/codegen.go | 27 +++-- codegen/codegen_example_test.go | 10 +- codegen/codegen_test.go | 76 +++++++------ codegen/doc.go | 3 +- edge_test.go | 10 +- layout.go | 39 +++---- layout_example_test.go | 16 +-- layout_test.go | 51 +++------ node.go | 28 ++--- pipeline.go | 2 +- responsive.go | 26 ++--- result_default.go | 13 +++ result_js.go | 14 +++ shadow.go | 68 ++++++------ text_builder_default.go | 14 ++- text_builder_default_example_test.go | 14 +-- text_builder_default_test.go | 51 ++++----- text_builder_js.go | 11 +- text_builder_js_example_test.go | 14 +-- text_builder_js_test.go | 51 ++++----- wasm.go | 2 +- 26 files changed, 389 insertions(+), 422 deletions(-) delete mode 100644 cmd/codegen/main_example_test.go create mode 100644 result_default.go create mode 100644 result_js.go diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go index 72ea46a..73107f2 100644 --- a/cmd/codegen/main.go +++ b/cmd/codegen/main.go @@ -24,55 +24,58 @@ const defaultPollInterval = 250 * time.Millisecond const defaultInputPath = "/dev/stdin" const defaultOutputPath = "/dev/stdout" -func generate(data []byte, emitTypes bool) (string, error) { +func generate(data []byte, emitTypes bool) core.Result { var slots map[string]string if result := core.JSONUnmarshal(data, &slots); !result.OK { err, _ := result.Value.(error) - return "", log.E("codegen", "invalid JSON", err) + return core.Fail(log.E("codegen", "invalid JSON", err)) } if emitTypes { - return codegen.GenerateTypeScriptDefinitions(slots), nil + return core.Ok(codegen.GenerateTypeScriptDefinitions(slots)) } - out, err := codegen.GenerateBundle(slots) - if err != nil { - return "", log.E("codegen", "generate bundle", err) + outResult := codegen.GenerateBundle(slots) + if !outResult.OK { + return resultError("codegen", "generate bundle", outResult) } - return out, nil + return outResult } -func run(input, output any, emitTypes bool) error { - data, err := readInput(input) - if err != nil { - return log.E("codegen", "reading input", err) +func run(input, output any, emitTypes bool) core.Result { + dataResult := readInput(input) + if !dataResult.OK { + return resultError("codegen", "reading input", dataResult) } + data, _ := dataResult.Value.([]byte) - out, err := generate(data, emitTypes) - if err != nil { - return err + outResult := generate(data, emitTypes) + if !outResult.OK { + return outResult } + out, _ := outResult.Value.(string) - if err := writeOutput(output, out); err != nil { - return log.E("codegen", "writing output", err) + writeResult := writeOutput(output, out) + if !writeResult.OK { + return resultError("codegen", "writing output", writeResult) } - return nil + return core.Ok(nil) } -func readInput(input any) ([]byte, error) { +func readInput(input any) core.Result { if path, ok := input.(string); ok { return readLocalFile(path) } result := core.ReadAll(input) if !result.OK { - return nil, resultError("codegen", "reading input stream", result) + return resultError("codegen", "reading input stream", result) } content, _ := result.Value.(string) - return []byte(content), nil + return core.Ok([]byte(content)) } -func writeOutput(output any, content string) error { +func writeOutput(output any, content string) core.Result { if path, ok := output.(string); ok { return writeLocalFile(path, content) } @@ -81,15 +84,15 @@ func writeOutput(output any, content string) error { if !result.OK { return resultError("codegen", "writing output stream", result) } - return nil + return core.Ok(nil) } -func runDaemon(ctx context.Context, inputPath, outputPath string, emitTypes bool, pollInterval time.Duration) error { +func runDaemon(ctx context.Context, inputPath, outputPath string, emitTypes bool, pollInterval time.Duration) core.Result { if inputPath == "" { - return log.E("codegen", "watch mode requires -input", nil) + return core.Fail(log.E("codegen", "watch mode requires -input", nil)) } if outputPath == "" { - return log.E("codegen", "watch mode requires -output", nil) + return core.Fail(log.E("codegen", "watch mode requires -output", nil)) } if pollInterval <= 0 { pollInterval = defaultPollInterval @@ -97,18 +100,21 @@ func runDaemon(ctx context.Context, inputPath, outputPath string, emitTypes bool var lastInput []byte for { - input, err := readLocalFile(inputPath) - if err != nil { - return log.E("codegen", "reading input file", err) + inputResult := readLocalFile(inputPath) + if !inputResult.OK { + return resultError("codegen", "reading input file", inputResult) } + input, _ := inputResult.Value.([]byte) if !sameBytes(input, lastInput) { - out, err := generate(input, emitTypes) - if err != nil { - return err + outResult := generate(input, emitTypes) + if !outResult.OK { + return outResult } - if err := writeLocalFile(outputPath, out); err != nil { - return log.E("codegen", "writing output file", err) + out, _ := outResult.Value.(string) + writeResult := writeLocalFile(outputPath, out) + if !writeResult.OK { + return resultError("codegen", "writing output file", writeResult) } lastInput = append(lastInput[:0], input...) } @@ -116,15 +122,15 @@ func runDaemon(ctx context.Context, inputPath, outputPath string, emitTypes bool select { case <-ctx.Done(): if core.Is(ctx.Err(), context.Canceled) { - return nil + return core.Ok(nil) } - return ctx.Err() + return core.Fail(ctx.Err()) case <-time.After(pollInterval): } } } -func readLocalFile(path string) ([]byte, error) { +func readLocalFile(path string) core.Result { if path == "" { path = defaultInputPath } @@ -132,25 +138,25 @@ func readLocalFile(path string) ([]byte, error) { if path == defaultInputPath { f, err := coreio.Local.Open(path) if err != nil { - return nil, err + return core.Fail(err) } result := core.ReadAll(f) if !result.OK { - return nil, resultError("codegen", "reading stdin", result) + return resultError("codegen", "reading stdin", result) } content, _ := result.Value.(string) - return []byte(content), nil + return core.Ok([]byte(content)) } content, err := coreio.Local.Read(path) if err != nil { - return nil, err + return core.Fail(err) } - return []byte(content), nil + return core.Ok([]byte(content)) } -func writeLocalFile(path, content string) error { +func writeLocalFile(path, content string) core.Result { if path == "" { path = defaultOutputPath } @@ -161,7 +167,7 @@ func writeLocalFile(path, content string) error { f, err = coreio.Local.Append(path) if err != nil { core.Print(nil, "%s", content) - return nil + return core.Ok(nil) } } @@ -169,10 +175,10 @@ func writeLocalFile(path, content string) error { if !result.OK { return resultError("codegen", "writing stdout", result) } - return nil + return core.Ok(nil) } - return coreio.Local.Write(path, content) + return resultFromError(coreio.Local.Write(path, content)) } func sameBytes(a, b []byte) bool { @@ -187,14 +193,14 @@ func sameBytes(a, b []byte) bool { return true } -func runStdio(emitTypes bool) error { +func runStdio(emitTypes bool) core.Result { return run(defaultInputPath, defaultOutputPath, emitTypes) } func newCodegenApp() *core.Core { c := core.New(core.WithOption("name", "codegen")) if cli := c.Cli(); cli != nil { - cli.SetOutput(discardWriter{}) + cli.SetOutput(core.Discard) } registerCodegenCommands(c) @@ -206,49 +212,49 @@ func registerCodegenCommands(c *core.Core) { Description: "Generate JavaScript or TypeScript from a JSON slot map on stdin", Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runGenerateCommand(opts, opts.Bool("types"))) + return runGenerateCommand(opts, opts.Bool("types")) }, }) c.Command("types", core.Command{ Description: "Generate TypeScript declarations from a JSON slot map on stdin", Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runGenerateCommand(opts, true)) + return runGenerateCommand(opts, true) }, }) c.Command("-types", core.Command{ Hidden: true, Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runGenerateCommand(opts, true)) + return runGenerateCommand(opts, true) }, }) c.Command("--types", core.Command{ Hidden: true, Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runGenerateCommand(opts, true)) + return runGenerateCommand(opts, true) }, }) c.Command("watch", core.Command{ Description: "Poll an input JSON file and rewrite the generated output", Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runWatchCommand(c, opts, opts.Bool("types"))) + return runWatchCommand(c, opts, opts.Bool("types")) }, }) c.Command("-watch", core.Command{ Hidden: true, Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runWatchCommand(c, opts, opts.Bool("types"))) + return runWatchCommand(c, opts, opts.Bool("types")) }, }) c.Command("--watch", core.Command{ Hidden: true, Flags: codegenCommandFlags(), Action: func(opts core.Options) core.Result { - return resultFromError(runWatchCommand(c, opts, opts.Bool("types"))) + return runWatchCommand(c, opts, opts.Bool("types")) }, }) } @@ -262,7 +268,7 @@ func codegenCommandFlags() core.Options { ) } -func runGenerateCommand(opts core.Options, emitTypes bool) error { +func runGenerateCommand(opts core.Options, emitTypes bool) core.Result { return run(inputPathFromOptions(opts), outputPathFromOptions(opts), emitTypes) } @@ -280,11 +286,12 @@ func outputPathFromOptions(opts core.Options) string { return defaultOutputPath } -func runWatchCommand(c *core.Core, opts core.Options, emitTypes bool) error { - pollInterval, err := pollIntervalFromOptions(opts) - if err != nil { - return err +func runWatchCommand(c *core.Core, opts core.Options, emitTypes bool) core.Result { + pollResult := pollIntervalFromOptions(opts) + if !pollResult.OK { + return pollResult } + pollInterval, _ := pollResult.Value.(time.Duration) ctx := context.Background() if c != nil { @@ -293,22 +300,22 @@ func runWatchCommand(c *core.Core, opts core.Options, emitTypes bool) error { return runDaemon(ctx, opts.String("input"), opts.String("output"), emitTypes, pollInterval) } -func pollIntervalFromOptions(opts core.Options) (time.Duration, error) { +func pollIntervalFromOptions(opts core.Options) core.Result { raw := opts.String("poll") if raw == "" { - return defaultPollInterval, nil + return core.Ok(defaultPollInterval) } pollInterval, err := time.ParseDuration(raw) if err != nil { - return 0, log.E("codegen", "invalid poll interval", err) + return core.Fail(log.E("codegen", "invalid poll interval", err)) } - return pollInterval, nil + return core.Ok(pollInterval) } -func runCodegenApp(c *core.Core) error { +func runCodegenApp(c *core.Core) core.Result { if c == nil { - return log.E("codegen.main", "core app is required", nil) + return core.Fail(log.E("codegen.main", "core app is required", nil)) } defer func() { @@ -328,10 +335,10 @@ func runCodegenApp(c *core.Core) error { result := cli.Run() if result.OK { - return nil + return core.Ok(nil) } if err, ok := result.Value.(error); ok && err != nil { - return err + return core.Fail(err) } return runStdio(false) @@ -344,25 +351,19 @@ func resultFromError(err error) core.Result { return core.Ok(nil) } -func resultError(op, msg string, result core.Result) error { +func resultError(op, msg string, result core.Result) core.Result { if result.OK { - return nil + return core.Ok(nil) } if err, ok := result.Value.(error); ok && err != nil { - return err + return core.Fail(log.E(op, msg, err)) } - return log.E(op, msg, nil) -} - -type discardWriter struct{} - -func (discardWriter) Write(data []byte) (int, error) { - return len(data), nil + return core.Fail(log.E(op, msg, nil)) } func main() { c := newCodegenApp() - if err := runCodegenApp(c); err != nil { - log.Error("codegen failed", "scope", "codegen.main", "err", err) + if result := runCodegenApp(c); !result.OK { + log.Error("codegen failed", "scope", "codegen.main", "err", result.Error()) } } diff --git a/cmd/codegen/main_example_test.go b/cmd/codegen/main_example_test.go deleted file mode 100644 index 668e1f7..0000000 --- a/cmd/codegen/main_example_test.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !js - -// SPDX-Licence-Identifier: EUPL-1.2 - -package main - -import . "dappco.re/go" - -func ExampleWriter_Write() { - n, err := discardWriter{}.Write([]byte("agent")) - Println(n, err == nil) - // Output: 5 true -} diff --git a/cmd/codegen/main_test.go b/cmd/codegen/main_test.go index 721f270..fb03cd4 100644 --- a/cmd/codegen/main_test.go +++ b/cmd/codegen/main_test.go @@ -4,7 +4,6 @@ package main import ( "context" - . "dappco.re/go" goio "io" "testing" "time" @@ -17,8 +16,8 @@ func TestRun_WritesBundleGood(t *testing.T) { input := core.NewReader(`{"H":"nav-bar","C":"main-content"}`) output := core.NewBuilder() - if err := run(input, output, false); err != nil { - t.Fatalf("unexpected error: %v", err) + if result := run(input, output, false); !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } js := output.String() @@ -40,12 +39,12 @@ func TestRun_InvalidJSONBad(t *testing.T) { input := core.NewReader(`not json`) output := core.NewBuilder() - err := run(input, output, false) - if err == nil { - t.Fatal("expected error, got nil") + result := run(input, output, false) + if result.OK { + t.Fatal("expected error result, got OK") } - if !core.Contains(err.Error(), "invalid JSON") { - t.Fatalf("expected error to contain %q, got %v", "invalid JSON", err) + if !core.Contains(result.Error(), "invalid JSON") { + t.Fatalf("expected error to contain %q, got %v", "invalid JSON", result.Error()) } } @@ -53,12 +52,12 @@ func TestRun_InvalidTagBad(t *testing.T) { input := core.NewReader(`{"H":"notag"}`) output := core.NewBuilder() - err := run(input, output, false) - if err == nil { - t.Fatal("expected error, got nil") + result := run(input, output, false) + if result.OK { + t.Fatal("expected error result, got OK") } - if !core.Contains(err.Error(), "hyphen") { - t.Fatalf("expected error to contain %q, got %v", "hyphen", err) + if !core.Contains(result.Error(), "hyphen") { + t.Fatalf("expected error to contain %q, got %v", "hyphen", result.Error()) } } @@ -66,12 +65,12 @@ func TestRun_InvalidTagCharactersBad(t *testing.T) { input := core.NewReader(`{"H":"Nav-Bar","C":"nav bar"}`) output := core.NewBuilder() - err := run(input, output, false) - if err == nil { - t.Fatal("expected error, got nil") + result := run(input, output, false) + if result.OK { + t.Fatal("expected error result, got OK") } - if !core.Contains(err.Error(), "lowercase hyphenated name") { - t.Fatalf("expected error to contain %q, got %v", "lowercase hyphenated name", err) + if !core.Contains(result.Error(), "lowercase hyphenated name") { + t.Fatalf("expected error to contain %q, got %v", "lowercase hyphenated name", result.Error()) } } @@ -79,8 +78,8 @@ func TestRun_EmptySlotsGood(t *testing.T) { input := core.NewReader(`{}`) output := core.NewBuilder() - if err := run(input, output, false); err != nil { - t.Fatalf("unexpected error: %v", err) + if result := run(input, output, false); !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } if got := output.String(); got != "" { t.Fatalf("expected empty output, got %q", got) @@ -91,8 +90,8 @@ func TestRun_WritesTypeScriptDefinitionsGood(t *testing.T) { input := core.NewReader(`{"H":"nav-bar","C":"main-content"}`) output := core.NewBuilder() - if err := run(input, output, true); err != nil { - t.Fatalf("unexpected error: %v", err) + if result := run(input, output, true); !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } dts := output.String() @@ -121,7 +120,7 @@ func TestRunDaemon_WritesUpdatedBundleGood(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - done := make(chan error, 1) + done := make(chan core.Result, 1) go func() { done <- runDaemon(ctx, inputPath, outputPath, false, 5*time.Millisecond) }() @@ -141,18 +140,18 @@ func TestRunDaemon_WritesUpdatedBundleGood(t *testing.T) { } cancel() - if err := <-done; err != nil { - t.Fatalf("unexpected error: %v", err) + if result := <-done; !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } } func TestRunDaemon_MissingPathsBad(t *testing.T) { - err := runDaemon(context.Background(), "", "", false, time.Millisecond) - if err == nil { - t.Fatal("expected error, got nil") + result := runDaemon(context.Background(), "", "", false, time.Millisecond) + if result.OK { + t.Fatal("expected error result, got OK") } - if !core.Contains(err.Error(), "watch mode requires -input") { - t.Fatalf("expected error to contain %q, got %v", "watch mode requires -input", err) + if !core.Contains(result.Error(), "watch mode requires -input") { + t.Fatalf("expected error to contain %q, got %v", "watch mode requires -input", result.Error()) } } @@ -219,24 +218,3 @@ func readTextFile(path string) (string, error) { } return string(data), nil } - -func TestMain_Writer_Write_Good(t *T) { - writer := discardWriter{} - n, err := writer.Write([]byte("agent")) - AssertNoError(t, err) - AssertEqual(t, 5, n) -} - -func TestMain_Writer_Write_Bad(t *T) { - writer := discardWriter{} - n, err := writer.Write(nil) - AssertNoError(t, err) - AssertEqual(t, 0, n) -} - -func TestMain_Writer_Write_Ugly(t *T) { - writer := discardWriter{} - n, err := writer.Write([]byte{0, 'x'}) - AssertNoError(t, err) - AssertEqual(t, 2, n) -} diff --git a/cmd/wasm/register.go b/cmd/wasm/register.go index 3e95fae..95d1af9 100644 --- a/cmd/wasm/register.go +++ b/cmd/wasm/register.go @@ -9,15 +9,15 @@ import ( log "dappco.re/go/log" ) -// buildComponentJS takes a JSON slot map and returns the WC bundle JS string. +// buildComponentJS takes a JSON slot map and returns the WC bundle JS result. // This is the pure-Go part testable without WASM. // Excluded from WASM builds — encoding/json and text/template are too heavy. // Use cmd/codegen/ CLI instead for build-time generation. -func buildComponentJS(slotsJSON string) (string, error) { +func buildComponentJS(slotsJSON string) core.Result { var slots map[string]string if result := core.JSONUnmarshalString(slotsJSON, &slots); !result.OK { err, _ := result.Value.(error) - return "", log.E("buildComponentJS", "unmarshal JSON", err) + return core.Fail(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 ff097c8..5c2023a 100644 --- a/cmd/wasm/register_test.go +++ b/cmd/wasm/register_test.go @@ -10,10 +10,11 @@ import ( func TestBuildComponentJS_ValidJSONGood(t *testing.T) { slotsJSON := `{"H":"nav-bar","C":"main-content"}` - js, err := buildComponentJS(slotsJSON) - if err != nil { - t.Fatalf("unexpected error: %v", err) + result := buildComponentJS(slotsJSON) + if !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } + js, _ := result.Value.(string) if !core.Contains(js, "NavBar") { t.Fatal("expected js to contain NavBar") } @@ -26,8 +27,8 @@ func TestBuildComponentJS_ValidJSONGood(t *testing.T) { } func TestBuildComponentJS_InvalidJSONBad(t *testing.T) { - _, err := buildComponentJS("not json") - if err == nil { - t.Fatal("expected error, got nil") + result := buildComponentJS("not json") + if result.OK { + t.Fatal("expected error result, got OK") } } diff --git a/codegen/codegen.go b/codegen/codegen.go index bd4cf07..ccdcf54 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -138,10 +138,10 @@ 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) { +// Usage example: result := GenerateClass("nav-bar", "H") +func GenerateClass(tag, slot string) core.Result { if !isValidCustomElementTag(tag) { - return "", log.E("codegen.GenerateClass", "custom element tag must be a lowercase hyphenated name: "+tag, nil) + return core.Fail(log.E("codegen.GenerateClass", "custom element tag must be a lowercase hyphenated name: "+tag, nil)) } b := core.NewBuilder() tagLiteral := escapeJSStringLiteral(tag) @@ -154,9 +154,9 @@ func GenerateClass(tag, slot string) (string, error) { SlotLiteral: slotLiteral, }) if err != nil { - return "", log.E("codegen.GenerateClass", "template exec", err) + return core.Fail(log.E("codegen.GenerateClass", "template exec", err)) } - return b.String(), nil + return core.Ok(b.String()) } // GenerateRegistration produces the customElements.define() call. @@ -191,8 +191,8 @@ 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) { +// Usage example: result := GenerateBundle(map[string]string{"H": "nav-bar"}) +func GenerateBundle(slots map[string]string) core.Result { seen := make(map[string]bool) b := core.NewBuilder() keys := make([]string, 0, len(slots)) @@ -208,14 +208,19 @@ func GenerateBundle(slots map[string]string) (string, error) { } seen[tag] = true - cls, err := GenerateClass(tag, slot) - if err != nil { - return "", log.E("codegen.GenerateBundle", "generate class for tag "+tag, err) + clsResult := GenerateClass(tag, slot) + if !clsResult.OK { + var err error + if value, ok := clsResult.Value.(error); ok { + err = value + } + return core.Fail(log.E("codegen.GenerateBundle", "generate class for tag "+tag, err)) } + cls, _ := clsResult.Value.(string) b.WriteString(cls) b.WriteByte('\n') b.WriteString(GenerateRegistration(tag, TagToClassName(tag))) b.WriteByte('\n') } - return b.String(), nil + return core.Ok(b.String()) } diff --git a/codegen/codegen_example_test.go b/codegen/codegen_example_test.go index 46fb788..dd5ff1d 100644 --- a/codegen/codegen_example_test.go +++ b/codegen/codegen_example_test.go @@ -7,8 +7,9 @@ package codegen import . "dappco.re/go" func ExampleGenerateClass() { - js, err := GenerateClass("nav-bar", "H") - Println(err == nil, Contains(js, "class NavBar extends HTMLElement")) + result := GenerateClass("nav-bar", "H") + js, _ := result.Value.(string) + Println(result.OK, Contains(js, "class NavBar extends HTMLElement")) // Output: true true } @@ -23,7 +24,8 @@ func ExampleTagToClassName() { } func ExampleGenerateBundle() { - js, err := GenerateBundle(map[string]string{"H": "nav-bar"}) - Println(err == nil, Contains(js, "customElements.define")) + result := GenerateBundle(map[string]string{"H": "nav-bar"}) + js, _ := result.Value.(string) + Println(result.OK, Contains(js, "customElements.define")) // Output: true true } diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index d59f10b..7822a45 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -10,10 +10,11 @@ import ( ) func TestGenerateClass_ValidTagGood(t *testing.T) { - js, err := GenerateClass("photo-grid", "C") - if err != nil { - t.Fatalf("unexpected error: %v", err) + result := GenerateClass("photo-grid", "C") + if !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } + js, _ := result.Value.(string) for _, want := range []string{ "class PhotoGrid extends HTMLElement", "attachShadow", @@ -27,17 +28,17 @@ func TestGenerateClass_ValidTagGood(t *testing.T) { } func TestGenerateClass_InvalidTagBad(t *testing.T) { - if _, err := GenerateClass("invalid", "C"); err == nil { - t.Fatal("expected error: custom element names must contain a hyphen") + if result := GenerateClass("invalid", "C"); result.OK { + t.Fatal("expected error result: custom element names must contain a hyphen") } - if _, err := GenerateClass("Nav-Bar", "C"); err == nil { - t.Fatal("expected error: custom element names must be lowercase") + if result := GenerateClass("Nav-Bar", "C"); result.OK { + t.Fatal("expected error result: custom element names must be lowercase") } - if _, err := GenerateClass("nav bar", "C"); err == nil { - t.Fatal("expected error: custom element names must reject spaces") + if result := GenerateClass("nav bar", "C"); result.OK { + t.Fatal("expected error result: custom element names must reject spaces") } - if _, err := GenerateClass("annotation-xml", "C"); err == nil { - t.Fatal("expected error: reserved custom element names must be rejected") + if result := GenerateClass("annotation-xml", "C"); result.OK { + t.Fatal("expected error result: reserved custom element names must be rejected") } } @@ -65,10 +66,11 @@ func TestGenerateClass_ValidExtendedTagGood(t *testing.T) { for _, tt := range tests { t.Run(tt.tag, func(t *testing.T) { - js, err := GenerateClass(tt.tag, "C") - if err != nil { - t.Fatalf("unexpected error: %v", err) + result := GenerateClass(tt.tag, "C") + if !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } + js, _ := result.Value.(string) if want := "class " + tt.wantClass + " extends HTMLElement"; !core.Contains(js, want) { t.Fatalf("expected js to contain %q", want) } @@ -106,10 +108,11 @@ func TestGenerateBundle_DeduplicatesRegistrationsGood(t *testing.T) { "C": "main-content", "F": "nav-bar", } - js, err := GenerateBundle(slots) - if err != nil { - t.Fatalf("unexpected error: %v", err) + result := GenerateBundle(slots) + if !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } + js, _ := result.Value.(string) if !core.Contains(js, "NavBar") { t.Fatal("expected js to contain NavBar") } @@ -131,10 +134,11 @@ func TestGenerateBundle_DeterministicOrderingGood(t *testing.T) { "M": "main-content", } - js, err := GenerateBundle(slots) - if err != nil { - t.Fatalf("unexpected error: %v", err) + result := GenerateBundle(slots) + if !result.OK { + t.Fatalf("unexpected error: %v", result.Error()) } + js, _ := result.Value.(string) alpha := indexSubstr(js, "class AlphaPanel") main := indexSubstr(js, "class MainContent") @@ -272,20 +276,22 @@ func indexSubstr(s, substr string) int { } func TestCodegen_GenerateClass_Good(t *T) { - got, err := GenerateClass("nav-bar", "H") - AssertNoError(t, err) + result := GenerateClass("nav-bar", "H") + AssertTrue(t, result.OK) + got, _ := result.Value.(string) AssertContains(t, got, "class NavBar extends HTMLElement") } func TestCodegen_GenerateClass_Bad(t *T) { - got, err := GenerateClass("notag", "H") - AssertError(t, err) - AssertEqual(t, "", got) + result := GenerateClass("notag", "H") + AssertFalse(t, result.OK) + AssertContains(t, result.Error(), "hyphenated") } func TestCodegen_GenerateClass_Ugly(t *T) { - got, err := GenerateClass("nav-bar", `"&`) - AssertNoError(t, err) + result := GenerateClass("nav-bar", `"&`) + AssertTrue(t, result.OK) + got, _ := result.Value.(string) AssertContains(t, got, `\"&`) } @@ -326,19 +332,21 @@ func TestCodegen_TagToClassName_Ugly(t *T) { } func TestCodegen_GenerateBundle_Good(t *T) { - got, err := GenerateBundle(map[string]string{"H": "nav-bar"}) - AssertNoError(t, err) + result := GenerateBundle(map[string]string{"H": "nav-bar"}) + AssertTrue(t, result.OK) + got, _ := result.Value.(string) AssertContains(t, got, "customElements.define") } func TestCodegen_GenerateBundle_Bad(t *T) { - got, err := GenerateBundle(map[string]string{"H": "notag"}) - AssertError(t, err) - AssertEqual(t, "", got) + result := GenerateBundle(map[string]string{"H": "notag"}) + AssertFalse(t, result.OK) + AssertContains(t, result.Error(), "hyphenated") } func TestCodegen_GenerateBundle_Ugly(t *T) { - got, err := GenerateBundle(map[string]string{"H": "nav-bar", "C": "nav-bar"}) - AssertNoError(t, err) + result := GenerateBundle(map[string]string{"H": "nav-bar", "C": "nav-bar"}) + AssertTrue(t, result.OK) + got, _ := result.Value.(string) AssertEqual(t, 1, countSubstr(got, "customElements.define")) } diff --git a/codegen/doc.go b/codegen/doc.go index afc9b13..488675c 100644 --- a/codegen/doc.go +++ b/codegen/doc.go @@ -6,8 +6,9 @@ // // Use it at build time, or through the cmd/codegen CLI: // -// bundle, err := GenerateBundle(map[string]string{ +// result := GenerateBundle(map[string]string{ // "H": "site-header", // "C": "app-main", // }) +// bundle, _ := result.Value.(string) package codegen diff --git a/edge_test.go b/edge_test.go index e4fd7b4..5f33338 100644 --- a/edge_test.go +++ b/edge_test.go @@ -404,8 +404,8 @@ func TestLayout_VariantError_NoOp_Good(t *testing.T) { if tt.build != nil { tt.build(layout) } - if layout.VariantError() != nil { - t.Fatalf("VariantError() = %v, want nil", layout.VariantError()) + if result := layout.VariantError(); !result.OK || result.Value != nil { + t.Fatalf("VariantError() = %+v, want OK nil", result) } got := layout.Render(NewContext()) @@ -428,9 +428,9 @@ func TestValidateLayoutVariant_NoOpGood(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateLayoutVariant(tt.variant) - if err != nil { - t.Fatalf("ValidateLayoutVariant(%q) = %v, want nil", tt.variant, err) + result := ValidateLayoutVariant(tt.variant) + if !result.OK || result.Value != nil { + t.Fatalf("ValidateLayoutVariant(%q) = %+v, want OK nil", tt.variant, result) } }) } diff --git a/layout.go b/layout.go index fc88692..2b75f00 100644 --- a/layout.go +++ b/layout.go @@ -3,7 +3,8 @@ package html // Note: this file is WASM-linked. Per RFC §7 the WASM build must stay under the // 3.5 MB raw / 1 MB gzip size budget, so we deliberately avoid importing // dappco.re/go/core here — it transitively pulls in fmt/os/log (~500 KB+). -// The stdlib strconv primitive is safe for WASM. +// Result-shaped compatibility helpers are bridged through result_default.go +// and result_js.go so the WASM build avoids importing the full core package. import "strconv" @@ -75,9 +76,9 @@ func NewLayout(variant string) *Layout { // // Variant strings are permissive now: unknown characters are ignored during // rendering, so this helper always returns nil. -func ValidateLayoutVariant(variant string) error { +func ValidateLayoutVariant(variant string) result { _ = variant - return nil + return okResult(nil) } func (l *Layout) slotsForSlot(slot byte) []Node { @@ -158,11 +159,11 @@ func (l *Layout) blockID(slot byte, rendered int) string { // // Layouts no longer record variant validation errors, so this always returns // nil. Unknown characters are ignored at render time. -func (l *Layout) VariantError() error { +func (l *Layout) VariantError() result { if l == nil { - return nil + return okResult(nil) } - return nil + return okResult(nil) } // Render produces the semantic HTML for this layout. @@ -201,24 +202,24 @@ func (l *Layout) Render(ctx *Context) string { } bid := l.blockID(slot, count) - b.WriteByte('<') - b.WriteString(escapeHTML(meta.tag)) - b.WriteString(` role="`) - b.WriteString(escapeAttr(meta.role)) - b.WriteString(`" data-block="`) - b.WriteString(escapeAttr(bid)) - b.WriteString(`">`) + b.AppendByte('<') + b.AppendString(escapeHTML(meta.tag)) + b.AppendString(` role="`) + b.AppendString(escapeAttr(meta.role)) + b.AppendString(`" data-block="`) + b.AppendString(escapeAttr(bid)) + b.AppendString(`">`) for i, child := range children { if child == nil { continue } - b.WriteString(renderWithLayoutPath(child, ctx, bid+"."+strconv.Itoa(i))) + b.AppendString(renderWithLayoutPath(child, ctx, bid+"."+strconv.Itoa(i))) } - b.WriteString("') + b.AppendString("') } return b.String() @@ -232,10 +233,6 @@ func (e *layoutVariantError) Error() string { return "html: invalid layout variant " + e.variant } -func (e *layoutVariantError) Unwrap() error { - return ErrInvalidLayoutVariant -} - func (l *Layout) renderWithLayoutPath(ctx *Context, path string) string { if l == nil { return "" diff --git a/layout_example_test.go b/layout_example_test.go index cc9df51..2e9be89 100644 --- a/layout_example_test.go +++ b/layout_example_test.go @@ -18,8 +18,9 @@ func ExampleNewLayout() { } func ExampleValidateLayoutVariant() { - core.Println(ValidateLayoutVariant("???") == nil) - // Output: true + result := ValidateLayoutVariant("???") + core.Println(result.OK, result.Value == nil) + // Output: true true } func ExampleLayout_H() { @@ -48,8 +49,9 @@ func ExampleLayout_F() { } func ExampleLayout_VariantError() { - core.Println(NewLayout("C").VariantError() == nil) - // Output: true + result := NewLayout("C").VariantError() + core.Println(result.OK, result.Value == nil) + // Output: true true } func ExampleLayout_Render() { @@ -62,9 +64,3 @@ func ExampleVariantError_Error() { core.Println(err.Error()) // Output: html: invalid layout variant XYZ } - -func ExampleVariantError_Unwrap() { - err := &VariantError{variant: "XYZ"} - core.Println(err.Unwrap() == ErrInvalidLayoutVariant) - // Output: true -} diff --git a/layout_test.go b/layout_test.go index 60329bd..126e141 100644 --- a/layout_test.go +++ b/layout_test.go @@ -186,21 +186,21 @@ func TestLayout_NewLayout_Ugly(t *core.T) { } func TestLayout_ValidateLayoutVariant_Good(t *core.T) { - err := ValidateLayoutVariant("HLCRF") - core.AssertNil(t, err) - core.AssertTrue(t, err == nil) + result := ValidateLayoutVariant("HLCRF") + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) } func TestLayout_ValidateLayoutVariant_Bad(t *core.T) { - err := ValidateLayoutVariant("???") - core.AssertNil(t, err) - core.AssertTrue(t, err == nil) + result := ValidateLayoutVariant("???") + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) } func TestLayout_ValidateLayoutVariant_Ugly(t *core.T) { - err := ValidateLayoutVariant("") - core.AssertNil(t, err) - core.AssertTrue(t, err == nil) + result := ValidateLayoutVariant("") + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) } func TestLayout_Layout_H_Good(t *core.T) { @@ -295,20 +295,23 @@ func TestLayout_Layout_F_Ugly(t *core.T) { func TestLayout_Layout_VariantError_Good(t *core.T) { l := NewLayout("C") - err := l.VariantError() - core.AssertNil(t, err) + result := l.VariantError() + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) } func TestLayout_Layout_VariantError_Bad(t *core.T) { var l *Layout - err := l.VariantError() - core.AssertNil(t, err) + result := l.VariantError() + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) } func TestLayout_Layout_VariantError_Ugly(t *core.T) { l := NewLayout("???") - err := l.VariantError() - core.AssertNil(t, err) + result := l.VariantError() + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) } func TestLayout_Layout_Render_Good(t *core.T) { @@ -346,21 +349,3 @@ func TestLayout_VariantError_Error_Ugly(t *core.T) { got := err.Error() core.AssertContains(t, got, "\n") } - -func TestLayout_VariantError_Unwrap_Good(t *core.T) { - err := &layoutVariantError{variant: "XYZ"} - got := err.Unwrap() - core.AssertEqual(t, ErrInvalidLayoutVariant, got) -} - -func TestLayout_VariantError_Unwrap_Bad(t *core.T) { - err := &layoutVariantError{} - got := err.Unwrap() - core.AssertEqual(t, ErrInvalidLayoutVariant, got) -} - -func TestLayout_VariantError_Unwrap_Ugly(t *core.T) { - var err *layoutVariantError - got := err.Unwrap() - core.AssertEqual(t, ErrInvalidLayoutVariant, got) -} diff --git a/node.go b/node.go index 0a16e5e..86e3458 100644 --- a/node.go +++ b/node.go @@ -244,21 +244,21 @@ func (n *elNode) render(ctx *Context, path string) string { attrs["data-block"] = path } - b.WriteByte('<') - b.WriteString(escapeHTML(n.tag)) + b.AppendByte('<') + b.AppendString(escapeHTML(n.tag)) // Sort attribute keys for deterministic output. keys := slices.Collect(maps.Keys(attrs)) slices.Sort(keys) for _, key := range keys { - b.WriteByte(' ') - b.WriteString(escapeHTML(key)) - b.WriteString(`="`) - b.WriteString(escapeAttr(attrs[key])) - b.WriteByte('"') + b.AppendByte(' ') + b.AppendString(escapeHTML(key)) + b.AppendString(`="`) + b.AppendString(escapeAttr(attrs[key])) + b.AppendByte('"') } - b.WriteByte('>') + b.AppendByte('>') if voidElements[n.tag] { return b.String() @@ -270,15 +270,15 @@ func (n *elNode) render(ctx *Context, path string) string { continue } if path == "" { - b.WriteString(child.Render(ctx)) + b.AppendString(child.Render(ctx)) continue } - b.WriteString(renderWithLayoutPath(child, ctx, path+"."+strconv.Itoa(i))) + b.AppendString(renderWithLayoutPath(child, ctx, path+"."+strconv.Itoa(i))) } - b.WriteString("') + b.AppendString("') return b.String() } @@ -553,7 +553,7 @@ func (n *eachNode[T]) renderWithLayoutPath(ctx *Context, path string) string { if path != "" && (!nodePreservesLayoutPath(child, ctx) || total > 1) { childPath = path + "." + strconv.Itoa(idx) } - b.WriteString(renderWithLayoutPath(child, ctx, childPath)) + b.AppendString(renderWithLayoutPath(child, ctx, childPath)) } return b.String() } diff --git a/pipeline.go b/pipeline.go index 93b9f7d..b285380 100644 --- a/pipeline.go +++ b/pipeline.go @@ -41,7 +41,7 @@ func StripTags(html string) string { prevSpace = true } default: - _, _ = b.WriteString(html[i : i+size]) + b.WriteString(html[i : i+size]) prevSpace = false } diff --git a/responsive.go b/responsive.go index 2a42330..55caa3b 100644 --- a/responsive.go +++ b/responsive.go @@ -75,15 +75,15 @@ func (r *Responsive) renderWithLayoutPath(ctx *Context, path string) string { continue } - b.WriteString(`
`) - b.WriteString(renderWithLayoutPath(v.layout, ctx, path)) - b.WriteString(`
`) + b.AppendString(`">`) + b.AppendString(renderWithLayoutPath(v.layout, ctx, path)) + b.AppendString(``) } return b.String() } @@ -103,23 +103,23 @@ func escapeCSSString(s string) string { for _, r := range s { switch r { case '\\', '"': - b.WriteByte('\\') - b.WriteRune(r) + b.AppendByte('\\') + b.AppendRune(r) default: if r < 0x20 || r == 0x7f { - b.WriteByte('\\') + b.AppendByte('\\') esc := strconv.FormatInt(int64(r), 16) for i := 0; i < len(esc); i++ { c := esc[i] if c >= 'a' && c <= 'f' { c -= 'a' - 'A' } - b.WriteByte(c) + b.AppendByte(c) } - b.WriteByte(' ') + b.AppendByte(' ') continue } - b.WriteRune(r) + b.AppendRune(r) } } return b.String() diff --git a/result_default.go b/result_default.go new file mode 100644 index 0000000..b68e2ec --- /dev/null +++ b/result_default.go @@ -0,0 +1,13 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +type result = core.Result + +func okResult(value any) result { + return core.Ok(value) +} diff --git a/result_js.go b/result_js.go new file mode 100644 index 0000000..950edae --- /dev/null +++ b/result_js.go @@ -0,0 +1,14 @@ +//go:build js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +type result struct { + Value any + OK bool +} + +func okResult(value any) result { + return result{Value: value, OK: true} +} diff --git a/shadow.go b/shadow.go index 6b75dda..b14da38 100644 --- a/shadow.go +++ b/shadow.go @@ -28,19 +28,19 @@ func (sc *ShadowComponent) RenderClass() string { } b := newTextBuilder() - b.WriteString("class ") - b.WriteString(className) - b.WriteString(" extends HTMLElement {\n") - b.WriteString(" constructor() {\n") - b.WriteString(" super();\n") - b.WriteString(" const shadow = this.attachShadow({ mode: ") - b.WriteString(jsStringLiteral(shadowMode(sc.Mode))) - b.WriteString(" });\n") - b.WriteString(" shadow.innerHTML = ") - b.WriteString(jsStringLiteral(body)) - b.WriteString(";\n") - b.WriteString(" }\n") - b.WriteString("}") + b.AppendString("class ") + b.AppendString(className) + b.AppendString(" extends HTMLElement {\n") + b.AppendString(" constructor() {\n") + b.AppendString(" super();\n") + b.AppendString(" const shadow = this.attachShadow({ mode: ") + b.AppendString(jsStringLiteral(shadowMode(sc.Mode))) + b.AppendString(" });\n") + b.AppendString(" shadow.innerHTML = ") + b.AppendString(jsStringLiteral(body)) + b.AppendString(";\n") + b.AppendString(" }\n") + b.AppendString("}") return b.String() } @@ -88,7 +88,7 @@ func pascalCase(s string) string { if upperNext && unicode.IsLetter(r) { r = unicode.ToUpper(r) } - b.WriteRune(r) + b.AppendRune(r) upperNext = false } return b.String() @@ -114,7 +114,7 @@ func kebabCase(s string) string { kind := classifyKebabRune(r) if kind == kebabNone { if written && !lastWasDash { - b.WriteByte('-') + b.AppendByte('-') lastWasDash = true } previous = kebabNone @@ -122,9 +122,9 @@ func kebabCase(s string) string { } if written && !lastWasDash && shouldInsertKebabDash(previous, kind, runes, i) { - b.WriteByte('-') + b.AppendByte('-') } - b.WriteRune(unicode.ToLower(r)) + b.AppendRune(unicode.ToLower(r)) lastWasDash = false previous = kind written = true @@ -177,9 +177,9 @@ func nextKebabRuneKind(runes []rune, index int) kebabRuneKind { func jsStringLiteral(s string) string { b := newTextBuilder() - b.WriteByte('"') + b.AppendByte('"') appendJSStringLiteral(b, s) - b.WriteByte('"') + b.AppendByte('"') return b.String() } @@ -187,23 +187,23 @@ func appendJSStringLiteral(b *textBuilder, s string) { for _, r := range s { switch r { case '\\': - b.WriteString(`\\`) + b.AppendString(`\\`) case '"': - b.WriteString(`\"`) + b.AppendString(`\"`) case '\b': - b.WriteString(`\b`) + b.AppendString(`\b`) case '\f': - b.WriteString(`\f`) + b.AppendString(`\f`) case '\n': - b.WriteString(`\n`) + b.AppendString(`\n`) case '\r': - b.WriteString(`\r`) + b.AppendString(`\r`) case '\t': - b.WriteString(`\t`) + b.AppendString(`\t`) case 0x2028: - b.WriteString(`\u2028`) + b.AppendString(`\u2028`) case 0x2029: - b.WriteString(`\u2029`) + b.AppendString(`\u2029`) default: if r < 0x20 { appendUnicodeEscape(b, r) @@ -215,16 +215,16 @@ func appendJSStringLiteral(b *textBuilder, s string) { appendUnicodeEscape(b, rune(0xDC00+(rr&0x3FF))) continue } - b.WriteRune(r) + b.AppendRune(r) } } } func appendUnicodeEscape(b *textBuilder, r rune) { const hex = "0123456789ABCDEF" - b.WriteString(`\u`) - b.WriteByte(hex[(r>>12)&0xF]) - b.WriteByte(hex[(r>>8)&0xF]) - b.WriteByte(hex[(r>>4)&0xF]) - b.WriteByte(hex[r&0xF]) + b.AppendString(`\u`) + b.AppendByte(hex[(r>>12)&0xF]) + b.AppendByte(hex[(r>>8)&0xF]) + b.AppendByte(hex[(r>>4)&0xF]) + b.AppendByte(hex[r&0xF]) } diff --git a/text_builder_default.go b/text_builder_default.go index ceaedad..71bc80b 100644 --- a/text_builder_default.go +++ b/text_builder_default.go @@ -21,16 +21,18 @@ func newTextBuilder() *textBuilder { return &textBuilder{inner: core.NewBuilder()} } -func (b *textBuilder) WriteByte(c byte) error { - return b.inner.WriteByte(c) +func (b *textBuilder) AppendByte(c byte) { + b.inner.WriteByte(c) } -func (b *textBuilder) WriteRune(r rune) (int, error) { - return b.inner.WriteRune(r) +func (b *textBuilder) AppendRune(r rune) int { + n, _ := b.inner.WriteRune(r) + return n } -func (b *textBuilder) WriteString(s string) (int, error) { - return b.inner.WriteString(s) +func (b *textBuilder) AppendString(s string) int { + n, _ := b.inner.WriteString(s) + return n } func (b *textBuilder) String() string { diff --git a/text_builder_default_example_test.go b/text_builder_default_example_test.go index 35b323a..55cd9ea 100644 --- a/text_builder_default_example_test.go +++ b/text_builder_default_example_test.go @@ -8,30 +8,30 @@ import core "dappco.re/go" type Builder = textBuilder -func ExampleBuilder_WriteByte() { +func ExampleBuilder_AppendByte() { b := newTextBuilder() - b.WriteByte('A') + b.AppendByte('A') core.Println(b.String()) // Output: A } -func ExampleBuilder_WriteRune() { +func ExampleBuilder_AppendRune() { b := newTextBuilder() - b.WriteRune('R') + b.AppendRune('R') core.Println(b.String()) // Output: R } -func ExampleBuilder_WriteString() { +func ExampleBuilder_AppendString() { b := newTextBuilder() - n, _ := b.WriteString("go") + n := b.AppendString("go") core.Println(n, b.String()) // Output: 2 go } func ExampleBuilder_String() { b := newTextBuilder() - b.WriteString("ready") + b.AppendString("ready") core.Println(b.String()) // Output: ready } diff --git a/text_builder_default_test.go b/text_builder_default_test.go index 77b3834..dca0573 100644 --- a/text_builder_default_test.go +++ b/text_builder_default_test.go @@ -6,73 +6,63 @@ package html import core "dappco.re/go" -func TestTextBuilderDefault_Builder_WriteByte_Good(t *core.T) { +func TestTextBuilderDefault_Builder_AppendByte_Good(t *core.T) { b := newTextBuilder() - err := b.WriteByte('A') - core.AssertNoError(t, err) + b.AppendByte('A') core.AssertEqual(t, "A", b.String()) } -func TestTextBuilderDefault_Builder_WriteByte_Bad(t *core.T) { +func TestTextBuilderDefault_Builder_AppendByte_Bad(t *core.T) { b := newTextBuilder() - err := b.WriteByte(0) - core.AssertNoError(t, err) + b.AppendByte(0) core.AssertEqual(t, "\x00", b.String()) } -func TestTextBuilderDefault_Builder_WriteByte_Ugly(t *core.T) { +func TestTextBuilderDefault_Builder_AppendByte_Ugly(t *core.T) { b := newTextBuilder() - err := b.WriteByte('\n') - core.AssertNoError(t, err) + b.AppendByte('\n') core.AssertEqual(t, "\n", b.String()) } -func TestTextBuilderDefault_Builder_WriteRune_Good(t *core.T) { +func TestTextBuilderDefault_Builder_AppendRune_Good(t *core.T) { b := newTextBuilder() - n, err := b.WriteRune('A') - core.AssertNoError(t, err) + n := b.AppendRune('A') core.AssertEqual(t, 1, n) } -func TestTextBuilderDefault_Builder_WriteRune_Bad(t *core.T) { +func TestTextBuilderDefault_Builder_AppendRune_Bad(t *core.T) { b := newTextBuilder() - n, err := b.WriteRune(0) - core.AssertNoError(t, err) + n := b.AppendRune(0) core.AssertEqual(t, 1, n) } -func TestTextBuilderDefault_Builder_WriteRune_Ugly(t *core.T) { +func TestTextBuilderDefault_Builder_AppendRune_Ugly(t *core.T) { b := newTextBuilder() - n, err := b.WriteRune('λ') - core.AssertNoError(t, err) + n := b.AppendRune('λ') core.AssertEqual(t, len("λ"), n) } -func TestTextBuilderDefault_Builder_WriteString_Good(t *core.T) { +func TestTextBuilderDefault_Builder_AppendString_Good(t *core.T) { b := newTextBuilder() - n, err := b.WriteString("agent") - core.AssertNoError(t, err) + n := b.AppendString("agent") core.AssertEqual(t, 5, n) } -func TestTextBuilderDefault_Builder_WriteString_Bad(t *core.T) { +func TestTextBuilderDefault_Builder_AppendString_Bad(t *core.T) { b := newTextBuilder() - n, err := b.WriteString("") - core.AssertNoError(t, err) + n := b.AppendString("") core.AssertEqual(t, 0, n) } -func TestTextBuilderDefault_Builder_WriteString_Ugly(t *core.T) { +func TestTextBuilderDefault_Builder_AppendString_Ugly(t *core.T) { b := newTextBuilder() - n, err := b.WriteString("λ") - core.AssertNoError(t, err) + n := b.AppendString("λ") core.AssertEqual(t, len("λ"), n) } func TestTextBuilderDefault_Builder_String_Good(t *core.T) { b := newTextBuilder() - _, err := b.WriteString("agent") - core.AssertNoError(t, err) + b.AppendString("agent") core.AssertEqual(t, "agent", b.String()) } @@ -84,7 +74,6 @@ func TestTextBuilderDefault_Builder_String_Bad(t *core.T) { func TestTextBuilderDefault_Builder_String_Ugly(t *core.T) { b := newTextBuilder() - err := b.WriteByte(0) - core.AssertNoError(t, err) + b.AppendByte(0) core.AssertEqual(t, "\x00", b.String()) } diff --git a/text_builder_js.go b/text_builder_js.go index 4004ff2..0caabfe 100644 --- a/text_builder_js.go +++ b/text_builder_js.go @@ -12,20 +12,19 @@ func newTextBuilder() *textBuilder { return &textBuilder{buf: make([]byte, 0, 128)} } -func (b *textBuilder) WriteByte(c byte) error { +func (b *textBuilder) AppendByte(c byte) { b.buf = append(b.buf, c) - return nil } -func (b *textBuilder) WriteRune(r rune) (int, error) { +func (b *textBuilder) AppendRune(r rune) int { s := string(r) b.buf = append(b.buf, s...) - return len(s), nil + return len(s) } -func (b *textBuilder) WriteString(s string) (int, error) { +func (b *textBuilder) AppendString(s string) int { b.buf = append(b.buf, s...) - return len(s), nil + return len(s) } func (b *textBuilder) String() string { diff --git a/text_builder_js_example_test.go b/text_builder_js_example_test.go index 82a03c4..a661f4d 100644 --- a/text_builder_js_example_test.go +++ b/text_builder_js_example_test.go @@ -8,30 +8,30 @@ import core "dappco.re/go" type Builder = textBuilder -func ExampleBuilder_WriteByte() { +func ExampleBuilder_AppendByte() { b := newTextBuilder() - b.WriteByte('A') + b.AppendByte('A') core.Println(b.String()) // Output: A } -func ExampleBuilder_WriteRune() { +func ExampleBuilder_AppendRune() { b := newTextBuilder() - b.WriteRune('R') + b.AppendRune('R') core.Println(b.String()) // Output: R } -func ExampleBuilder_WriteString() { +func ExampleBuilder_AppendString() { b := newTextBuilder() - n, _ := b.WriteString("go") + n := b.AppendString("go") core.Println(n, b.String()) // Output: 2 go } func ExampleBuilder_String() { b := newTextBuilder() - b.WriteString("ready") + b.AppendString("ready") core.Println(b.String()) // Output: ready } diff --git a/text_builder_js_test.go b/text_builder_js_test.go index d821a74..5e3bb88 100644 --- a/text_builder_js_test.go +++ b/text_builder_js_test.go @@ -6,73 +6,63 @@ package html import core "dappco.re/go" -func TestTextBuilderJs_Builder_WriteByte_Good(t *core.T) { +func TestTextBuilderJs_Builder_AppendByte_Good(t *core.T) { b := newTextBuilder() - err := b.WriteByte('A') - core.AssertNoError(t, err) + b.AppendByte('A') core.AssertEqual(t, "A", b.String()) } -func TestTextBuilderJs_Builder_WriteByte_Bad(t *core.T) { +func TestTextBuilderJs_Builder_AppendByte_Bad(t *core.T) { b := newTextBuilder() - err := b.WriteByte(0) - core.AssertNoError(t, err) + b.AppendByte(0) core.AssertEqual(t, "\x00", b.String()) } -func TestTextBuilderJs_Builder_WriteByte_Ugly(t *core.T) { +func TestTextBuilderJs_Builder_AppendByte_Ugly(t *core.T) { b := newTextBuilder() - err := b.WriteByte('\n') - core.AssertNoError(t, err) + b.AppendByte('\n') core.AssertEqual(t, "\n", b.String()) } -func TestTextBuilderJs_Builder_WriteRune_Good(t *core.T) { +func TestTextBuilderJs_Builder_AppendRune_Good(t *core.T) { b := newTextBuilder() - n, err := b.WriteRune('A') - core.AssertNoError(t, err) + n := b.AppendRune('A') core.AssertEqual(t, 1, n) } -func TestTextBuilderJs_Builder_WriteRune_Bad(t *core.T) { +func TestTextBuilderJs_Builder_AppendRune_Bad(t *core.T) { b := newTextBuilder() - n, err := b.WriteRune(0) - core.AssertNoError(t, err) + n := b.AppendRune(0) core.AssertEqual(t, 1, n) } -func TestTextBuilderJs_Builder_WriteRune_Ugly(t *core.T) { +func TestTextBuilderJs_Builder_AppendRune_Ugly(t *core.T) { b := newTextBuilder() - n, err := b.WriteRune('λ') - core.AssertNoError(t, err) + n := b.AppendRune('λ') core.AssertEqual(t, len("λ"), n) } -func TestTextBuilderJs_Builder_WriteString_Good(t *core.T) { +func TestTextBuilderJs_Builder_AppendString_Good(t *core.T) { b := newTextBuilder() - n, err := b.WriteString("agent") - core.AssertNoError(t, err) + n := b.AppendString("agent") core.AssertEqual(t, 5, n) } -func TestTextBuilderJs_Builder_WriteString_Bad(t *core.T) { +func TestTextBuilderJs_Builder_AppendString_Bad(t *core.T) { b := newTextBuilder() - n, err := b.WriteString("") - core.AssertNoError(t, err) + n := b.AppendString("") core.AssertEqual(t, 0, n) } -func TestTextBuilderJs_Builder_WriteString_Ugly(t *core.T) { +func TestTextBuilderJs_Builder_AppendString_Ugly(t *core.T) { b := newTextBuilder() - n, err := b.WriteString("λ") - core.AssertNoError(t, err) + n := b.AppendString("λ") core.AssertEqual(t, len("λ"), n) } func TestTextBuilderJs_Builder_String_Good(t *core.T) { b := newTextBuilder() - _, err := b.WriteString("agent") - core.AssertNoError(t, err) + b.AppendString("agent") core.AssertEqual(t, "agent", b.String()) } @@ -84,7 +74,6 @@ func TestTextBuilderJs_Builder_String_Bad(t *core.T) { func TestTextBuilderJs_Builder_String_Ugly(t *core.T) { b := newTextBuilder() - err := b.WriteByte(0) - core.AssertNoError(t, err) + b.AppendByte(0) core.AssertEqual(t, "\x00", b.String()) } diff --git a/wasm.go b/wasm.go index df03a4f..10de5b5 100644 --- a/wasm.go +++ b/wasm.go @@ -29,7 +29,7 @@ func (n wasmFragmentNode) Render(ctx *Context) string { if child == nil { continue } - b.WriteString(child.Render(ctx)) + b.AppendString(child.Render(ctx)) } return b.String() } From ff57124c050765d561d5b46689df9dc89184eaed Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 30 Apr 2026 10:41:44 +0100 Subject: [PATCH 6/6] chore(deps): tidy after dappco.re vanity migration --- go.mod | 15 +-------------- go.sum | 54 ++++++++++++++++++------------------------------------ 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index f0e4c92..13a26f6 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( ) require ( + dappco.re/go/core v0.8.0-alpha.1 // indirect github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect @@ -46,17 +47,3 @@ require ( dappco.re/go/inference v0.8.0-alpha.1 // indirect golang.org/x/text v0.36.0 // indirect ) - -replace dappco.re/go => ../go - -replace dappco.re/go/core => ./.compat/core - -replace dappco.re/go/i18n => ../go-i18n - -replace dappco.re/go/inference => ../go-inference - -replace dappco.re/go/io => ../go-io - -replace dappco.re/go/log => ../go-log - -replace dappco.re/go/process => ../go-process diff --git a/go.sum b/go.sum index 7d21f6e..514efd3 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,29 @@ -forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= -forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= -forge.lthn.ai/Snider/Enchantrix v0.0.4 h1:biwpix/bdedfyc0iVeK15awhhJKH6TEMYOTXzHXx5TI= -forge.lthn.ai/Snider/Enchantrix v0.0.4/go.mod h1:OGCwuVeZPq3OPe2h6TX/ZbgEjHU6B7owpIBeXQGbSe0= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= -github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +dappco.re/go v0.9.0 h1:4ruZRNqKDDva8o6g65tYggjGVe42E6/lMZfVKXtr3p0= +dappco.re/go v0.9.0/go.mod h1:xapr7fLK4/9Pu2iSCr4qZuIuatmtx1j56zS/oPDbGyQ= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/i18n v0.8.0-alpha.1 h1:9LI/PrF41XeQu69eOaBTz3LMrXTJ08O2f1EEATq9k5A= +dappco.re/go/i18n v0.8.0-alpha.1/go.mod h1:aSfWSAW2EVh/aMbMplc27URnjl6DvRVvWfvRC2my7AY= +dappco.re/go/inference v0.8.0-alpha.1 h1:Cc3YZr04rNSqqHQBm7v53mzfn6e17sf7oDe+TqQnzwo= +dappco.re/go/inference v0.8.0-alpha.1/go.mod h1:rfNXLcfMilEI3nKpcdrC0PQKyUyaf6bDYseowgRwDP8= +dappco.re/go/io v0.8.0-alpha.1 h1:tIJ/Nd6lGr2DFEUj2HzGM8dPglS5bEAI4h2RAgzGCNE= +dappco.re/go/io v0.8.0-alpha.1/go.mod h1:491Lt0LOTK4/88EGWVWhrACuXAoxPXvXYu/iIwYc9C0= +dappco.re/go/log v0.8.0-alpha.1 h1:eXTdrt88Ovbdm0KJkJDaEpgLUHUZgJ2xYEu2uN3eV4I= +dappco.re/go/log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= +dappco.re/go/process v0.8.0-alpha.1 h1:mZaDrx5bXFUoI8xGpgd3G4Wntn/hRbRWP5oswTlLtXo= +dappco.re/go/process v0.8.0-alpha.1/go.mod h1:yRfVGKA8MWm5ZknRDEH2qxbLdatKw33GqA0ll8hBPH0= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -64,8 +49,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -77,10 +60,9 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= -github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=