Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8a3f28a
fix(conventions): isolate banned imports and clarify tests
Mar 26, 2026
0e976b3
fix(wasm): keep server i18n out of js builds
Mar 26, 2026
b8d0646
refactor(core): upgrade to v0.8.0-alpha.1
Mar 26, 2026
2a5bd5c
Merge pull request '[agent/codex] Upgrade this package to dappco.re/g…
Mar 26, 2026
3616ad3
chore: polish ax v0.8.0 conventions
Mar 26, 2026
df19b84
Merge pull request '[agent/codex] AX v0.8.0 polish pass. Fix ALL viol…
Mar 26, 2026
df5035c
chore: verification pass
Mar 27, 2026
1c61fde
Merge pull request '[agent/codex] VERIFICATION PASS — report findings…
Mar 27, 2026
11f18a2
fix(tests): complete ax naming compliance
Mar 27, 2026
44e3478
Merge pull request '[agent/codex] Full AX v0.8.0 compliance review. R…
Mar 27, 2026
adcb98e
chore: bump i18n v0.2.0 → v0.2.1
Mar 27, 2026
f21562c
docs: add generated package specs
Mar 27, 2026
adc9403
Merge pull request '[agent/codex] A specs/ folder has been injected i…
Mar 27, 2026
33d9e0c
docs(specs): add codegen RFC
Mar 27, 2026
8c7a9de
Merge pull request '[agent/codex] Update specs/codegen/RFC.md from co…
Mar 27, 2026
0318d73
fix(core): harden nil-safe rendering paths
Mar 29, 2026
cae46f9
chore(codegen): remove panic exits from cli path
Mar 29, 2026
c6fd135
fix(core): harden remaining nil-safe rendering paths
Mar 30, 2026
911071d
fix(core): harden layout and responsive nil chains
Mar 30, 2026
714d7ad
test(responsive): align nil variant output with semantic roles
Mar 30, 2026
f9f0aa1
fix(codegen): make bundle generation deterministic
Mar 30, 2026
65c0dd3
fix(html): default nil render contexts
Mar 31, 2026
4ae93ce
feat(html): add accessibility attribute helpers
Mar 31, 2026
c1852f8
feat(html): add focus management helpers
Mar 31, 2026
c63f0a2
feat(html): add responsive variant selector helper
Mar 31, 2026
12a7d24
feat(codegen): add TypeScript definitions generator
Mar 31, 2026
a928d01
feat(codegen): add TypeScript CLI output
Mar 31, 2026
5d13a40
fix(html): validate block ID parsing
Mar 31, 2026
8386c7e
fix(html): preserve block paths through conditional wrappers
Mar 31, 2026
1f98026
feat(html): add layout variant validation sentinel
Mar 31, 2026
14c16b5
feat(codegen): add watch mode for bundle generation
Mar 31, 2026
4a3a69e
fix(html): preserve switch wrapper paths
Mar 31, 2026
cb901db
feat(html): allow locale in context constructors
Mar 31, 2026
8e9ca00
feat(html): apply locale to context translators
Mar 31, 2026
60d8225
feat(html): preserve layout paths in iterators
Mar 31, 2026
c2ff591
feat(html): apply attrs through iterator wrappers
Mar 31, 2026
b9e2630
feat(html): allow swapping context translators
Mar 31, 2026
2e2af31
feat(html): add locale setter for context translators
Mar 31, 2026
1d11472
feat(html): add role accessibility helper
Mar 31, 2026
c088e5a
feat(codegen): emit module boundary in TypeScript definitions
Mar 31, 2026
8abd428
fix(codegen): validate custom element tags
Apr 1, 2026
70a3096
chore: improve CSS selector escaping for control chars
Apr 3, 2026
5784b76
fix(wasm): harden renderToString arg handling
Apr 3, 2026
8402485
fix(html): use locale setter in render path
Apr 3, 2026
f543f02
feat(html): add layout variant validation helper
Apr 3, 2026
4a924b0
fix: migrate module paths from forge.lthn.ai to dappco.re
Snider Apr 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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), 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), 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()`.

**Module**: `forge.lthn.ai/core/go-html`
**Licence**: EUPL-1.2
Expand Down
7 changes: 3 additions & 4 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package html

import (
"fmt"
"testing"

i18n "dappco.re/go/core/i18n"
Expand Down Expand Up @@ -100,7 +99,7 @@ func BenchmarkImprint_Small(b *testing.B) {
func BenchmarkImprint_Large(b *testing.B) {
items := make([]string, 20)
for i := range items {
items[i] = fmt.Sprintf("Item %d was created successfully", i)
items[i] = "Item " + itoaText(i) + " was created successfully"
}
page := NewLayout("HLCRF").
H(El("h1", Text("Building project"))).
Expand Down Expand Up @@ -207,7 +206,7 @@ func BenchmarkLayout_Nested(b *testing.B) {
func BenchmarkLayout_ManySlotChildren(b *testing.B) {
nodes := make([]Node, 50)
for i := range nodes {
nodes[i] = El("p", Raw(fmt.Sprintf("paragraph %d", i)))
nodes[i] = El("p", Raw("paragraph "+itoaText(i)))
}
layout := NewLayout("HLCRF").
H(Raw("header")).
Expand Down Expand Up @@ -242,7 +241,7 @@ func benchEach(b *testing.B, n int) {
items[i] = i
}
node := Each(items, func(i int) Node {
return El("li", Raw(fmt.Sprintf("item-%d", i)))
return El("li", Raw("item-"+itoaText(i)))
})
ctx := NewContext()

Expand Down
160 changes: 149 additions & 11 deletions cmd/codegen/main.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,181 @@
// Package main provides a build-time CLI for generating Web Component JS bundles.
// Reads a JSON slot map from stdin, writes the generated JS to stdout.
//go:build !js

// Package main provides a build-time CLI for generating Web Component bundles.
// Reads a JSON slot map from stdin, writes the generated JS or TypeScript to stdout.
//
// Usage:
//
// echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ > components.js
// echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ -types > components.d.ts
// echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ -watch -input slots.json -output components.js
package main

import (
"encoding/json"
"context"
"errors"
"flag"
goio "io"
"os"
"os/signal"
"time"

core "dappco.re/go/core"
"dappco.re/go/core/html/codegen"
coreio "dappco.re/go/core/io"
log "dappco.re/go/core/log"
)

func run(r goio.Reader, w goio.Writer) error {
func generate(data []byte, emitTypes bool) (string, error) {
var slots map[string]string
if result := core.JSONUnmarshal(data, &slots); !result.OK {
err, _ := result.Value.(error)
return "", log.E("codegen", "invalid JSON", err)
}

if emitTypes {
return codegen.GenerateTypeScriptDefinitions(slots), nil
}

out, err := codegen.GenerateBundle(slots)
if err != nil {
return "", log.E("codegen", "generate bundle", err)
}
return out, nil
}

func run(r goio.Reader, w goio.Writer, emitTypes bool) error {
data, err := goio.ReadAll(r)
if err != nil {
return log.E("codegen", "reading stdin", err)
}

var slots map[string]string
if err := json.Unmarshal(data, &slots); err != nil {
return log.E("codegen", "invalid JSON", err)
out, err := generate(data, emitTypes)
if err != nil {
return err
}

js, err := codegen.GenerateBundle(slots)
_, err = goio.WriteString(w, out)
if err != nil {
return log.E("codegen", "writing output", err)
}
return nil
}

func runDaemon(ctx context.Context, inputPath, outputPath string, emitTypes bool, pollInterval time.Duration) error {
if inputPath == "" {
return log.E("codegen", "watch mode requires -input", nil)
}
if outputPath == "" {
return log.E("codegen", "watch mode requires -output", nil)
}
if pollInterval <= 0 {
pollInterval = 250 * time.Millisecond
}

var lastInput []byte
for {
input, err := readLocalFile(inputPath)
if err != nil {
return log.E("codegen", "reading input file", err)
}

if !sameBytes(input, lastInput) {
out, err := generate(input, emitTypes)
if err != nil {
return err
}
if err := writeLocalFile(outputPath, out); err != nil {
return log.E("codegen", "writing output file", err)
}
lastInput = append(lastInput[:0], input...)
}

select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
case <-time.After(pollInterval):
}
}
}

func readLocalFile(path string) ([]byte, error) {
f, err := coreio.Local.Open(path)
if err != nil {
return nil, err
}
defer func() {
_ = f.Close()
}()

return goio.ReadAll(f)
}

func writeLocalFile(path, content string) error {
f, err := coreio.Local.Create(path)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()

_, err = goio.WriteString(w, js)
_, err = goio.WriteString(f, content)
return err
}

func sameBytes(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range len(a) {
if a[i] != b[i] {
return false
}
}
return true
}

func main() {
if err := run(os.Stdin, os.Stdout); err != nil {
log.Error("codegen failed", "err", err)
emitWatch := flag.Bool("watch", false, "poll input and rewrite output when the JSON changes")
inputPath := flag.String("input", "", "path to the JSON slot map used by -watch")
outputPath := flag.String("output", "", "path to the generated bundle written by -watch")
emitTypes := flag.Bool("types", false, "emit TypeScript declarations instead of JavaScript")
pollInterval := flag.Duration("poll", 250*time.Millisecond, "poll interval used by -watch")
flag.Parse()

if *emitWatch {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

if err := runDaemon(ctx, *inputPath, *outputPath, *emitTypes, *pollInterval); err != nil {
log.Error("codegen failed", "scope", "codegen.main", "err", err)
os.Exit(1)
}
return
}

stdin, err := coreio.Local.Open("/dev/stdin")
if err != nil {
log.Error("failed to open stdin", "scope", "codegen.main", "err", log.E("codegen.main", "open stdin", err))
os.Exit(1)
}

stdout, err := coreio.Local.Create("/dev/stdout")
if err != nil {
_ = stdin.Close()
log.Error("failed to open stdout", "scope", "codegen.main", "err", log.E("codegen.main", "open stdout", err))
os.Exit(1)
}
defer func() {
_ = stdin.Close()
_ = stdout.Close()
}()

if err := run(stdin, stdout, *emitTypes); err != nil {
log.Error("codegen failed", "scope", "codegen.main", "err", err)
os.Exit(1)
}
}
Loading
Loading