test(html): add helper coverage and refresh docs
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
ebbd050df4
commit
0020f26582
4 changed files with 66 additions and 12 deletions
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
# go-html
|
||||
|
||||
HLCRF DOM compositor with grammar pipeline integration for server-side HTML generation and optional WASM client rendering. Provides a type-safe node tree (El, Text, Raw, If, Each, Switch, Entitled, AriaLabel, AltText), a five-slot Header/Left/Content/Right/Footer layout compositor with deterministic `data-block` path IDs and ARIA roles, a responsive multi-variant wrapper, a server-side grammar pipeline (StripTags, GrammarImprint via go-i18n reversal, CompareVariants), a build-time Web Component codegen CLI, and a WASM module (2.90 MB raw, 842 KB gzip) exposing `renderToString()`.
|
||||
HLCRF DOM compositor with grammar pipeline integration for server-side HTML generation and optional WASM client rendering. Provides a type-safe node tree (El, Text, Raw, If, Each, Switch, Entitled, AriaLabel, AltText, TabIndex, AutoFocus, Role), a five-slot Header/Left/Content/Right/Footer layout compositor with deterministic `data-block` path IDs and ARIA roles, a responsive multi-variant wrapper, a server-side grammar pipeline (StripTags, GrammarImprint via go-i18n reversal, CompareVariants), a build-time Web Component codegen CLI with optional TypeScript declarations, and a WASM module (2.90 MB raw, 842 KB gzip) exposing `renderToString()`.
|
||||
|
||||
**Module**: `dappco.re/go/core/html`
|
||||
**Licence**: EUPL-1.2
|
||||
|
|
|
|||
|
|
@ -93,19 +93,19 @@ Slot letters not present in the variant string are ignored, even if nodes have b
|
|||
|
||||
### Deterministic Block IDs
|
||||
|
||||
Each rendered slot receives a `data-block` attribute encoding its position in the layout tree. At the root level, IDs follow the pattern `{slot}-0`:
|
||||
Each rendered slot receives a `data-block` attribute encoding its position in the layout tree. At the root level, IDs use the slot letter itself:
|
||||
|
||||
```html
|
||||
<header role="banner" data-block="H-0">...</header>
|
||||
<main role="main" data-block="C-0">...</main>
|
||||
<footer role="contentinfo" data-block="F-0">...</footer>
|
||||
<header role="banner" data-block="H">...</header>
|
||||
<main role="main" data-block="C">...</main>
|
||||
<footer role="contentinfo" data-block="F">...</footer>
|
||||
```
|
||||
|
||||
Block IDs are constructed by simple string concatenation (no `fmt.Sprintf`) to keep the `fmt` package out of the WASM import graph.
|
||||
|
||||
### Nested Layouts
|
||||
|
||||
`Layout` implements `Node`, so a layout can be placed inside any slot of another layout. At render time, nested layouts are cloned and their internal `path` field is set to the parent's block ID as a prefix. This produces hierarchical paths:
|
||||
`Layout` implements `Node`, so a layout can be placed inside any slot of another layout. At render time, nested layouts retain the parent's block ID as a prefix. This produces hierarchical paths:
|
||||
|
||||
```go
|
||||
inner := html.NewLayout("HCF").
|
||||
|
|
@ -120,7 +120,7 @@ outer := html.NewLayout("HLCRF").
|
|||
F(html.Raw("foot"))
|
||||
```
|
||||
|
||||
The inner layout's slots render with prefixed block IDs: `L-0-H-0`, `L-0-C-0`, `L-0-F-0`. At 10 levels of nesting, the deepest block ID becomes `C-0-C-0-C-0-C-0-C-0-C-0-C-0-C-0-C-0-C-0` (tested in `edge_test.go`).
|
||||
The inner layout's slots render with prefixed block IDs: `L.0`, `L.0.1`, `L.0.2`. At 10 levels of nesting, the deepest block ID becomes `C.0.0.0.0.0.0.0.0.0` (tested in `edge_test.go`).
|
||||
|
||||
The clone-on-render approach means the original layout is never mutated. This is safe for concurrent use.
|
||||
|
||||
|
|
@ -141,9 +141,9 @@ html.NewLayout("HCF").
|
|||
`ParseBlockID()` in `path.go` extracts the slot letter sequence from a `data-block` attribute value:
|
||||
|
||||
```go
|
||||
ParseBlockID("L-0-C-0") // returns ['L', 'C']
|
||||
ParseBlockID("C-0-C-0-C-0") // returns ['C', 'C', 'C']
|
||||
ParseBlockID("H-0") // returns ['H']
|
||||
ParseBlockID("L.0.C.0") // returns ['L', 'C']
|
||||
ParseBlockID("C.0.C.0.C.0") // returns ['C', 'C', 'C']
|
||||
ParseBlockID("H") // returns ['H']
|
||||
ParseBlockID("") // returns nil
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The test is skipped under `go test -short` and is guarded with `//go:build !js`.
|
|||
|
||||
These are not regressions; they are design choices or deferred work recorded for future consideration.
|
||||
|
||||
1. **Invalid layout variants are silent.** `NewLayout("XYZ")` produces empty output. No error, no warning. Adding validation would require changing the return type of `NewLayout` from `*Layout` to `(*Layout, error)`, which is a breaking API change.
|
||||
1. **Invalid layout variants are still non-fatal at render time.** `NewLayout("XYZ")` produces empty output, but `VariantError()` exposes the validation result without changing the `NewLayout` API.
|
||||
|
||||
2. **No WASM integration test.** `cmd/wasm/size_test.go` tests binary size only. The `renderToString` behaviour is tested by building and running the WASM binary in a browser, not by an automated test. A `syscall/js`-compatible test harness would be needed.
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ These are not regressions; they are design choices or deferred work recorded for
|
|||
|
||||
4. **Context.service is private.** The i18n service is still unexported, but callers can now swap it explicitly with `Context.SetService()`. This keeps the field encapsulated while allowing controlled mutation.
|
||||
|
||||
5. **TypeScript definitions not generated.** `codegen.GenerateBundle()` produces JS only. A `.d.ts` companion would benefit TypeScript consumers of the generated Web Components.
|
||||
5. **TypeScript definitions are generated.** `codegen.GenerateTypeScriptDefinitions()` and the `cmd/codegen -types` flag emit `.d.ts` companions for generated Web Components.
|
||||
|
||||
6. **CSS scoping helper added.** `VariantSelector(name)` returns a reusable `data-variant` attribute selector for stylesheet targeting. The `Responsive` rendering model remains unchanged.
|
||||
|
||||
|
|
|
|||
54
node_test.go
54
node_test.go
|
|
@ -259,6 +259,60 @@ func TestElNode_Attr_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAccessibilityHelpers_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
|
||||
button := Role(
|
||||
AriaLabel(
|
||||
TabIndex(
|
||||
AutoFocus(El("button", Raw("save"))),
|
||||
3,
|
||||
),
|
||||
"Save changes",
|
||||
),
|
||||
"button",
|
||||
)
|
||||
|
||||
got := button.Render(ctx)
|
||||
for _, want := range []string{
|
||||
`aria-label="Save changes"`,
|
||||
`autofocus="autofocus"`,
|
||||
`role="button"`,
|
||||
`tabindex="3"`,
|
||||
">save</button>",
|
||||
} {
|
||||
if !containsText(got, want) {
|
||||
t.Fatalf("accessibility helpers missing %q in:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
img := AltText(El("img"), "Profile photo")
|
||||
if got := img.Render(ctx); got != `<img alt="Profile photo">` {
|
||||
t.Fatalf("AltText() = %q, want %q", got, `<img alt="Profile photo">`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchNode_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
ctx.Locale = "en-GB"
|
||||
|
||||
node := Switch(
|
||||
func(ctx *Context) string { return ctx.Locale },
|
||||
map[string]Node{
|
||||
"en-GB": Raw("hello"),
|
||||
"fr-FR": Raw("bonjour"),
|
||||
},
|
||||
)
|
||||
|
||||
if got := node.Render(ctx); got != "hello" {
|
||||
t.Fatalf("Switch matched case = %q, want %q", got, "hello")
|
||||
}
|
||||
|
||||
if got := Switch(func(*Context) string { return "de-DE" }, map[string]Node{"en-GB": Raw("hello")}).Render(ctx); got != "" {
|
||||
t.Fatalf("Switch missing case = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_AttrEscaping_Good(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Attr(El("img"), "alt", `he said "hello"`)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue