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, `
`, 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)
+}