docs: align html API comments with AX guidance
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 17:23:37 +00:00
parent a86c8ef770
commit c84bd21cf4
7 changed files with 86 additions and 45 deletions

View file

@ -1,11 +1,13 @@
package html
// Translator provides Text() lookups for a rendering context.
// context.go: Translator provides Text() lookups for a rendering context.
// Example: a locale-aware service can satisfy T(key, args...).
type Translator interface {
T(key string, args ...any) string
}
// Context carries rendering state through the node tree.
// context.go: Context carries rendering state through the node tree.
// Example: NewContext("en-GB") initialises locale-specific rendering state.
type Context struct {
Identity string
Locale string
@ -31,7 +33,8 @@ func applyLocaleToService(svc Translator, locale string) {
}
}
// NewContext creates a new rendering context with sensible defaults.
// context.go: NewContext creates a new rendering context with sensible defaults.
// Example: ctx := NewContext("en-GB").
// An optional locale may be provided as the first argument.
func NewContext(locale ...string) *Context {
ctx := &Context{
@ -43,7 +46,8 @@ func NewContext(locale ...string) *Context {
return ctx
}
// NewContextWithService creates a rendering context backed by a specific translator.
// context.go: NewContextWithService creates a rendering context backed by a specific translator.
// Example: ctx := NewContextWithService(svc, "fr-FR").
// An optional locale may be provided as the second argument.
func NewContextWithService(svc Translator, locale ...string) *Context {
ctx := NewContext(locale...)
@ -51,7 +55,8 @@ func NewContextWithService(svc Translator, locale ...string) *Context {
return ctx
}
// SetService swaps the translator used by the context.
// context.go: SetService swaps the translator used by the context.
// Example: ctx.SetService(svc).
func (ctx *Context) SetService(svc Translator) *Context {
if ctx == nil {
return nil
@ -62,7 +67,8 @@ func (ctx *Context) SetService(svc Translator) *Context {
return ctx
}
// SetLocale updates the context locale and reapplies it to the active translator.
// context.go: SetLocale updates the context locale and reapplies it to the active translator.
// Example: ctx.SetLocale("en-US").
func (ctx *Context) SetLocale(locale string) *Context {
if ctx == nil {
return nil

View file

@ -27,8 +27,9 @@ var slotRegistry = map[byte]slotMeta{
'F': {tag: "footer", role: "contentinfo"},
}
// Layout is an HLCRF compositor. Arranges nodes into semantic HTML regions
// layout.go: Layout is an HLCRF compositor. Arranges nodes into semantic HTML regions
// with deterministic path-based IDs.
// Example: NewLayout("HCF").H(Raw("head")).C(Raw("body")).F(Raw("foot")).
type Layout struct {
variant string // "HLCRF", "HCF", "C", etc.
path string // "" for root, "L-0-" for nested
@ -36,7 +37,8 @@ type Layout struct {
variantErr error
}
// NewLayout creates a new Layout with the given variant string.
// layout.go: NewLayout creates a new Layout with the given variant string.
// Example: page := NewLayout("HCF").
// The variant determines which slots are rendered (e.g., "HLCRF", "HCF", "C").
func NewLayout(variant string) *Layout {
l := &Layout{
@ -47,8 +49,9 @@ func NewLayout(variant string) *Layout {
return l
}
// ValidateLayoutVariant reports whether a layout variant string contains only
// layout.go: ValidateLayoutVariant reports whether a layout variant string contains only
// recognised slot characters.
// Example: ValidateLayoutVariant("HCF").
func ValidateLayoutVariant(variant string) error {
var invalid bool
for i := range len(variant) {
@ -99,8 +102,9 @@ func (l *Layout) blockID(slot byte) string {
return l.path + string(slot) + "-0"
}
// VariantError reports whether the layout variant string contained any invalid
// layout.go: VariantError reports whether the layout variant string contained any invalid
// slot characters when the layout was constructed.
// Example: NewLayout("HXC").VariantError().
func (l *Layout) VariantError() error {
if l == nil {
return nil
@ -108,7 +112,8 @@ func (l *Layout) VariantError() error {
return l.variantErr
}
// Render produces the semantic HTML for this layout.
// layout.go: Render produces the semantic HTML for this layout.
// Example: NewLayout("C").C(Raw("body")).Render(NewContext()).
// Only slots present in the variant string are rendered.
func (l *Layout) Render(ctx *Context) string {
if l == nil {

65
node.go
View file

@ -11,7 +11,8 @@ import (
i18n "dappco.re/go/core/i18n"
)
// Node is anything renderable.
// node.go: Node is anything renderable.
// Example: El("p", Text("page.body")) returns a Node that can be passed to Render().
type Node interface {
Render(ctx *Context) string
}
@ -119,7 +120,8 @@ type rawNode struct {
content string
}
// Raw creates a node that renders without escaping (escape hatch for trusted content).
// node.go: Raw creates a node that renders without escaping.
// Example: Raw("<strong>trusted</strong>") preserves the HTML verbatim.
func Raw(content string) Node {
return &rawNode{content: content}
}
@ -139,7 +141,8 @@ type elNode struct {
attrs map[string]string
}
// El creates an HTML element node with children.
// node.go: El creates an HTML element node with children.
// Example: El("nav", Text("nav.label")) renders a semantic element with nested nodes.
func El(tag string, children ...Node) Node {
return &elNode{
tag: tag,
@ -148,9 +151,11 @@ func El(tag string, children ...Node) Node {
}
}
// Attr sets an attribute on an El node. Returns the node for chaining.
// It recursively traverses through wrappers like If, Unless, Entitled,
// Switch, and Each/EachSeq so the attribute lands on the rendered element.
// node.go: Attr sets an attribute on an El node and returns the same node for chaining.
// Example: Attr(El("img"), "alt", "Logo") adds an escaped alt attribute.
//
// It recursively traverses wrappers like If, Unless, Entitled, Switch,
// and Each/EachSeq so the attribute lands on the rendered element.
func Attr(n Node, key, value string) Node {
switch t := n.(type) {
case *elNode:
@ -171,34 +176,40 @@ func Attr(n Node, key, value string) Node {
return n
}
// AriaLabel sets the aria-label attribute on an element node.
// node.go: AriaLabel sets the aria-label attribute on an element node.
// Example: AriaLabel(El("button"), "Open menu").
func AriaLabel(n Node, label string) Node {
return Attr(n, "aria-label", label)
}
// AriaDescribedBy sets the aria-describedby attribute on an element node.
// node.go: AriaDescribedBy sets the aria-describedby attribute on an element node.
// Example: AriaDescribedBy(El("input"), "help-text", "error-text").
// Multiple IDs are joined with spaces, matching the HTML attribute format.
func AriaDescribedBy(n Node, ids ...string) Node {
return Attr(n, "aria-describedby", strings.Join(ids, " "))
}
// AriaLabelledBy sets the aria-labelledby attribute on an element node.
// node.go: AriaLabelledBy sets the aria-labelledby attribute on an element node.
// Example: AriaLabelledBy(El("section"), "section-title").
// Multiple IDs are joined with spaces, matching the HTML attribute format.
func AriaLabelledBy(n Node, ids ...string) Node {
return Attr(n, "aria-labelledby", strings.Join(ids, " "))
}
// Role sets the role attribute on an element node.
// node.go: Role sets the role attribute on an element node.
// Example: Role(El("aside"), "complementary").
func Role(n Node, role string) Node {
return Attr(n, "role", role)
}
// Alt sets the alt attribute on an element node.
// node.go: Alt sets the alt attribute on an element node.
// Example: Alt(El("img"), "Product screenshot").
func Alt(n Node, text string) Node {
return Attr(n, "alt", text)
}
// AriaHidden sets the aria-hidden attribute on an element node.
// node.go: AriaHidden sets the aria-hidden attribute on an element node.
// Example: AriaHidden(El("svg"), true).
func AriaHidden(n Node, hidden bool) Node {
if hidden {
return Attr(n, "aria-hidden", "true")
@ -206,12 +217,14 @@ func AriaHidden(n Node, hidden bool) Node {
return Attr(n, "aria-hidden", "false")
}
// TabIndex sets the tabindex attribute on an element node.
// node.go: TabIndex sets the tabindex attribute on an element node.
// Example: TabIndex(El("button"), 0).
func TabIndex(n Node, index int) Node {
return Attr(n, "tabindex", strconv.Itoa(index))
}
// AutoFocus sets the autofocus attribute on an element node.
// node.go: AutoFocus sets the autofocus attribute on an element node.
// Example: AutoFocus(El("input")).
func AutoFocus(n Node) Node {
return Attr(n, "autofocus", "autofocus")
}
@ -268,7 +281,8 @@ type textNode struct {
args []any
}
// Text creates a node that renders through the go-i18n grammar pipeline.
// node.go: Text creates a node that renders through the go-i18n grammar pipeline.
// Example: Text("page.title") renders translated text and escapes it for HTML.
// Output is HTML-escaped by default. Safe-by-default path.
func Text(key string, args ...any) Node {
return &textNode{key: key, args: args}
@ -295,7 +309,8 @@ type ifNode struct {
node Node
}
// If renders child only when condition is true.
// node.go: If renders child only when condition is true.
// Example: If(func(*Context) bool { return true }, Raw("shown")).
func If(cond func(*Context) bool, node Node) Node {
return &ifNode{cond: cond, node: node}
}
@ -318,7 +333,8 @@ type unlessNode struct {
node Node
}
// Unless renders child only when condition is false.
// node.go: Unless renders child only when condition is false.
// Example: Unless(func(*Context) bool { return true }, Raw("hidden")).
func Unless(cond func(*Context) bool, node Node) Node {
return &unlessNode{cond: cond, node: node}
}
@ -341,8 +357,10 @@ type entitledNode struct {
node Node
}
// Entitled renders child only when entitlement is granted. Absent, not hidden.
// If no entitlement function is set on the context, access is denied by default.
// node.go: Entitled renders child only when entitlement is granted.
// Example: Entitled("premium", Raw("paid feature")).
// Content is absent, not hidden. If no entitlement function is set on the
// context, access is denied by default.
func Entitled(feature string, node Node) Node {
return &entitledNode{feature: feature, node: node}
}
@ -365,7 +383,8 @@ type switchNode struct {
cases map[string]Node
}
// Switch renders based on runtime selector value.
// node.go: Switch renders based on runtime selector value.
// Example: Switch(selector, map[string]Node{"desktop": Raw("wide")}).
func Switch(selector func(*Context) string, cases map[string]Node) Node {
return &switchNode{selector: selector, cases: cases}
}
@ -389,12 +408,14 @@ type eachNode[T any] struct {
fn func(T) Node
}
// Each iterates items and renders each via fn.
// node.go: Each iterates items and renders each via fn.
// Example: Each(items, func(item Item) Node { return El("li", Text(item.Name)) }).
func Each[T any](items []T, fn func(T) Node) Node {
return EachSeq(slices.Values(items), fn)
}
// EachSeq iterates an iter.Seq and renders each via fn.
// node.go: EachSeq iterates an iter.Seq and renders each via fn.
// Example: EachSeq(slices.Values(items), renderItem).
func EachSeq[T any](items iter.Seq[T], fn func(T) Node) Node {
return &eachNode[T]{items: items, fn: fn}
}

View file

@ -2,7 +2,7 @@ package html
import "strings"
// ParseBlockID extracts the slot sequence from a data-block ID.
// path.go: ParseBlockID extracts the slot sequence from a data-block ID.
// Example: ParseBlockID("L-0-C-0") returns []byte{'L', 'C'}.
func ParseBlockID(id string) []byte {
if id == "" {

View file

@ -8,7 +8,8 @@ import (
"dappco.re/go/core/i18n/reversal"
)
// StripTags removes HTML tags from rendered output, returning plain text.
// pipeline.go: StripTags removes HTML tags from rendered output, returning plain text.
// Example: StripTags("<p>Hello</p><p>world</p>") returns "Hello world".
// Tag boundaries are collapsed into single spaces; result is trimmed.
// Does not handle script/style element content (go-html does not generate these).
func StripTags(html string) string {
@ -43,8 +44,9 @@ func StripTags(html string) string {
return strings.TrimSpace(b.String())
}
// Imprint renders a node tree to HTML, strips tags, tokenises the text,
// pipeline.go: Imprint renders a node tree to HTML, strips tags, tokenises the text,
// and returns a GrammarImprint — the full render-reverse pipeline.
// Example: Imprint(NewLayout("C").C(Text("page.body")), NewContext()).
func Imprint(node Node, ctx *Context) reversal.GrammarImprint {
if ctx == nil {
ctx = NewContext()
@ -56,8 +58,9 @@ func Imprint(node Node, ctx *Context) reversal.GrammarImprint {
return reversal.NewImprint(tokens)
}
// CompareVariants runs the imprint pipeline on each responsive variant independently
// pipeline.go: CompareVariants runs the imprint pipeline on each responsive variant independently
// and returns pairwise similarity scores. Key format: "name1:name2".
// Example: CompareVariants(NewResponsive().Variant("desktop", NewLayout("C")), NewContext()).
func CompareVariants(r *Responsive, ctx *Context) map[string]float64 {
if ctx == nil {
ctx = NewContext()

View file

@ -1,6 +1,7 @@
package html
// Render is a convenience function that renders a node tree to HTML.
// render.go: Render is a convenience function that renders a node tree to HTML.
// Example: Render(NewLayout("C").C(Raw("body")), NewContext()).
func Render(node Node, ctx *Context) string {
if ctx == nil {
ctx = NewContext()

View file

@ -2,7 +2,8 @@ package html
import "strings"
// Responsive wraps multiple Layout variants for breakpoint-aware rendering.
// responsive.go: Responsive wraps multiple Layout variants for breakpoint-aware rendering.
// Example: NewResponsive().Variant("desktop", NewLayout("C").C(Raw("main"))).
// Each variant is rendered inside a container with data-variant for CSS targeting.
type Responsive struct {
variants []responsiveVariant
@ -13,7 +14,8 @@ type responsiveVariant struct {
layout *Layout
}
// NewResponsive creates a new multi-variant responsive compositor.
// responsive.go: NewResponsive creates a new multi-variant responsive compositor.
// Example: r := NewResponsive().
func NewResponsive() *Responsive {
return &Responsive{}
}
@ -40,14 +42,15 @@ func escapeCSSString(s string) string {
return b.String()
}
// VariantSelector returns a CSS attribute selector for a named responsive
// variant.
// responsive.go: VariantSelector returns a CSS attribute selector for a named responsive variant.
// Example: VariantSelector("desktop") returns [data-variant="desktop"].
func VariantSelector(name string) string {
return `[data-variant="` + escapeCSSString(name) + `"]`
}
// ScopeVariant prefixes a selector so it only matches elements inside the
// responsive.go: ScopeVariant prefixes a selector so it only matches elements inside the
// named responsive variant.
// Example: ScopeVariant("desktop", ".nav").
func ScopeVariant(name, selector string) string {
scope := VariantSelector(name)
if selector == "" {
@ -56,14 +59,16 @@ func ScopeVariant(name, selector string) string {
return scope + " " + selector
}
// Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
// responsive.go: Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
// Example: r.Variant("mobile", NewLayout("C").C(Raw("body"))).
// Variants render in insertion order.
func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
return r
}
// Render produces HTML with each variant in a data-variant container.
// responsive.go: Render produces HTML with each variant in a data-variant container.
// Example: NewResponsive().Variant("desktop", NewLayout("C")).Render(NewContext()).
func (r *Responsive) Render(ctx *Context) string {
if r == nil {
return ""