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/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..107f0e6 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,37 @@ +# 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: 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: [go-test] + environment: + SONAR_HOST_URL: https://sonar.lthn.sh + SONAR_TOKEN: + from_secret: sonar_token + commands: + - sonar-scanner 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/cmd/codegen/main.go b/cmd/codegen/main.go index 6e9a56f..73107f2 100644 --- a/cmd/codegen/main.go +++ b/cmd/codegen/main.go @@ -14,7 +14,7 @@ import ( "context" "time" - core "dappco.re/go/core" + core "dappco.re/go" "dappco.re/go/html/codegen" coreio "dappco.re/go/io" log "dappco.re/go/log" @@ -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,26 +300,28 @@ 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() { - _ = c.ServiceShutdown(context.Background()) + if result := c.ServiceShutdown(context.Background()); !result.OK { + log.Warn("codegen shutdown failed", "scope", "codegen.main", "err", result.Error()) + } }() if result := c.ServiceStartup(c.Context(), nil); !result.OK { @@ -326,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) @@ -337,30 +346,24 @@ func runCodegenApp(c *core.Core) error { func resultFromError(err error) core.Result { if err != nil { - return core.Result{Value: err, OK: false} + return core.Fail(err) } - return core.Result{OK: true} + 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_test.go b/cmd/codegen/main_test.go index 812b7d0..fb03cd4 100644 --- a/cmd/codegen/main_test.go +++ b/cmd/codegen/main_test.go @@ -5,31 +5,29 @@ package main import ( "context" goio "io" - "path/filepath" - "strings" "testing" "time" - core "dappco.re/go/core" + core "dappco.re/go" coreio "dappco.re/go/io" ) -func TestRun_WritesBundle_Good(t *testing.T) { +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() - if !strings.Contains(js, "NavBar") { + if !core.Contains(js, "NavBar") { t.Fatal("expected js to contain NavBar") } - if !strings.Contains(js, "MainContent") { + if !core.Contains(js, "MainContent") { t.Fatal("expected js to contain MainContent") } - if !strings.Contains(js, "customElements.define") { + if !core.Contains(js, "customElements.define") { t.Fatal("expected js to contain customElements.define") } if got := countSubstr(js, "extends HTMLElement"); got != 2 { @@ -37,63 +35,63 @@ func TestRun_WritesBundle_Good(t *testing.T) { } } -func TestRun_InvalidJSON_Bad(t *testing.T) { +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 !strings.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()) } } -func TestRun_InvalidTag_Bad(t *testing.T) { +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 !strings.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()) } } -func TestRun_InvalidTagCharacters_Bad(t *testing.T) { +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 !strings.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()) } } -func TestRun_EmptySlots_Good(t *testing.T) { +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) } } -func TestRun_WritesTypeScriptDefinitions_Good(t *testing.T) { +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() @@ -104,16 +102,16 @@ func TestRun_WritesTypeScriptDefinitions_Good(t *testing.T) { "export declare class NavBar extends HTMLElement", "export declare class MainContent extends HTMLElement", } { - if !strings.Contains(dts, want) { + if !core.Contains(dts, want) { t.Fatalf("expected dts to contain %q", want) } } } -func TestRunDaemon_WritesUpdatedBundle_Good(t *testing.T) { +func TestRunDaemon_WritesUpdatedBundleGood(t *testing.T) { dir := t.TempDir() - inputPath := filepath.Join(dir, "slots.json") - outputPath := filepath.Join(dir, "bundle.js") + inputPath := core.Path(dir, "slots.json") + outputPath := core.Path(dir, "bundle.js") if err := writeTextFile(inputPath, `{"H":"nav-bar","C":"main-content"}`); err != nil { t.Fatalf("unexpected error: %v", err) @@ -122,7 +120,7 @@ func TestRunDaemon_WritesUpdatedBundle_Good(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) }() @@ -131,7 +129,7 @@ func TestRunDaemon_WritesUpdatedBundle_Good(t *testing.T) { ok := false for time.Now().Before(deadline) { got, err := readTextFile(outputPath) - if err == nil && strings.Contains(got, "NavBar") && strings.Contains(got, "MainContent") { + if err == nil && core.Contains(got, "NavBar") && core.Contains(got, "MainContent") { ok = true break } @@ -142,18 +140,18 @@ func TestRunDaemon_WritesUpdatedBundle_Good(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_MissingPaths_Bad(t *testing.T) { - err := runDaemon(context.Background(), "", "", false, time.Millisecond) - if err == nil { - t.Fatal("expected error, got nil") +func TestRunDaemon_MissingPathsBad(t *testing.T) { + result := runDaemon(context.Background(), "", "", false, time.Millisecond) + if result.OK { + t.Fatal("expected error result, got OK") } - if !strings.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()) } } diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go index 63a97e2..1a11194 100644 --- a/cmd/wasm/main.go +++ b/cmd/wasm/main.go @@ -3,7 +3,6 @@ package main import ( - "strings" "syscall/js" html "dappco.re/go/html" @@ -69,12 +68,43 @@ func renderTemplateString(template string, data js.Value) string { continue } rendered := html.Render(html.Text(scalarString(value)), ctx) - out = strings.ReplaceAll(out, "{{"+key+"}}", rendered) - out = strings.ReplaceAll(out, "{{ "+key+" }}", rendered) + out = replaceTemplateToken(out, "{{"+key+"}}", rendered) + out = replaceTemplateToken(out, "{{ "+key+" }}", rendered) } return out } +func replaceTemplateToken(s, old, new string) string { + if old == "" { + return s + } + + out := "" + for { + i := indexTemplateToken(s, old) + if i < 0 { + return out + s + } + out += s[:i] + new + s = s[i+len(old):] + } +} + +func indexTemplateToken(s, token string) int { + if token == "" { + return 0 + } + if len(token) > len(s) { + return -1 + } + for i := 0; i <= len(s)-len(token); i++ { + if s[i:i+len(token)] == token { + return i + } + } + return -1 +} + func scalarString(value js.Value) string { if value.Type() == js.TypeString { return value.String() diff --git a/cmd/wasm/main_test.go b/cmd/wasm/main_test.go index e1f0739..1ea5675 100644 --- a/cmd/wasm/main_test.go +++ b/cmd/wasm/main_test.go @@ -26,7 +26,7 @@ func TestRenderToString_Good(t *testing.T) { } } -func TestRenderToString_EmptySlot_Good(t *testing.T) { +func TestRenderToString_EmptySlotGood(t *testing.T) { gotAny := renderToString(js.Value{}, []js.Value{ js.ValueOf("C"), js.ValueOf("en-GB"), diff --git a/cmd/wasm/register.go b/cmd/wasm/register.go index dc86943..95d1af9 100644 --- a/cmd/wasm/register.go +++ b/cmd/wasm/register.go @@ -3,21 +3,21 @@ package main import ( - core "dappco.re/go/core" + core "dappco.re/go" "dappco.re/go/html/codegen" 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 d3632c3..5c2023a 100644 --- a/cmd/wasm/register_test.go +++ b/cmd/wasm/register_test.go @@ -3,30 +3,32 @@ package main import ( - "strings" "testing" + + core "dappco.re/go" ) -func TestBuildComponentJS_ValidJSON_Good(t *testing.T) { +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()) } - if !strings.Contains(js, "NavBar") { + js, _ := result.Value.(string) + if !core.Contains(js, "NavBar") { t.Fatal("expected js to contain NavBar") } - if !strings.Contains(js, "MainContent") { + if !core.Contains(js, "MainContent") { t.Fatal("expected js to contain MainContent") } - if !strings.Contains(js, "customElements.define") { + if !core.Contains(js, "customElements.define") { t.Fatal("expected js to contain customElements.define") } } -func TestBuildComponentJS_InvalidJSON_Bad(t *testing.T) { - _, err := buildComponentJS("not json") - if err == nil { - t.Fatal("expected error, got nil") +func TestBuildComponentJS_InvalidJSONBad(t *testing.T) { + result := buildComponentJS("not json") + if result.OK { + t.Fatal("expected error result, got OK") } } diff --git a/cmd/wasm/render_layout_test.go b/cmd/wasm/render_layout_test.go index 899b56c..f2e0bd6 100644 --- a/cmd/wasm/render_layout_test.go +++ b/cmd/wasm/render_layout_test.go @@ -6,7 +6,7 @@ package main import "testing" -func TestRenderLayout_EmptyStringSlot_Good(t *testing.T) { +func TestRenderLayout_EmptyStringSlotGood(t *testing.T) { got := renderLayout("C", "en-GB", map[string]string{"C": ""}) want := `
` if got != want { diff --git a/cmd/wasm/size_test.go b/cmd/wasm/size_test.go index 94b93b9..388b61f 100644 --- a/cmd/wasm/size_test.go +++ b/cmd/wasm/size_test.go @@ -8,7 +8,7 @@ import ( "context" "testing" - core "dappco.re/go/core" + core "dappco.re/go" coreio "dappco.re/go/io" process "dappco.re/go/process" ) @@ -18,7 +18,7 @@ const ( wasmRawLimit = 3_670_016 // 3.5 MB raw size limit ) -func TestCmdWasm_WASMBinarySize_Good(t *testing.T) { +func TestCmdWasm_WASMBinarySizeGood(t *testing.T) { if testing.Short() { t.Skip("skipping WASM build test in short mode") } diff --git a/codegen/codegen.go b/codegen/codegen.go index b210714..ccdcf54 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -8,7 +8,7 @@ import ( "unicode" "unicode/utf8" - core "dappco.re/go/core" + core "dappco.re/go" log "dappco.re/go/log" ) @@ -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 new file mode 100644 index 0000000..dd5ff1d --- /dev/null +++ b/codegen/codegen_example_test.go @@ -0,0 +1,31 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package codegen + +import . "dappco.re/go" + +func ExampleGenerateClass() { + result := GenerateClass("nav-bar", "H") + js, _ := result.Value.(string) + Println(result.OK, Contains(js, "class NavBar extends HTMLElement")) + // Output: true true +} + +func ExampleGenerateRegistration() { + Println(GenerateRegistration("nav-bar", "NavBar")) + // Output: customElements.define("nav-bar", NavBar); +} + +func ExampleTagToClassName() { + Println(TagToClassName("nav-bar")) + // Output: NavBar +} + +func ExampleGenerateBundle() { + 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 36ef4da..7822a45 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -3,56 +3,59 @@ package codegen import ( - "strings" + . "dappco.re/go" "testing" + + core "dappco.re/go" ) -func TestGenerateClass_ValidTag_Good(t *testing.T) { - js, err := GenerateClass("photo-grid", "C") - if err != nil { - t.Fatalf("unexpected error: %v", err) +func TestGenerateClass_ValidTagGood(t *testing.T) { + 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", `mode: "closed"`, "photo-grid", } { - if !strings.Contains(js, want) { + if !core.Contains(js, want) { t.Fatalf("expected js to contain %q", want) } } } -func TestGenerateClass_InvalidTag_Bad(t *testing.T) { - if _, err := GenerateClass("invalid", "C"); err == nil { - t.Fatal("expected error: custom element names must contain a hyphen") +func TestGenerateClass_InvalidTagBad(t *testing.T) { + 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") } } -func TestGenerateRegistration_DefinesCustomElement_Good(t *testing.T) { +func TestGenerateRegistration_DefinesCustomElementGood(t *testing.T) { js := GenerateRegistration("photo-grid", "PhotoGrid") for _, want := range []string{ "customElements.define", `"photo-grid"`, "PhotoGrid", } { - if !strings.Contains(js, want) { + if !core.Contains(js, want) { t.Fatalf("expected js to contain %q", want) } } } -func TestGenerateClass_ValidExtendedTag_Good(t *testing.T) { +func TestGenerateClass_ValidExtendedTagGood(t *testing.T) { tests := []struct { tag string wantClass string @@ -63,24 +66,25 @@ func TestGenerateClass_ValidExtendedTag_Good(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()) } - if want := "class " + tt.wantClass + " extends HTMLElement"; !strings.Contains(js, want) { + js, _ := result.Value.(string) + if want := "class " + tt.wantClass + " extends HTMLElement"; !core.Contains(js, want) { t.Fatalf("expected js to contain %q", want) } - if want := `tag: "` + tt.tag + `"`; !strings.Contains(js, want) { + if want := `tag: "` + tt.tag + `"`; !core.Contains(js, want) { t.Fatalf("expected js to contain %q", want) } - if want := `slot = this.getAttribute("data-slot") || "C";`; !strings.Contains(js, want) { + if want := `slot = this.getAttribute("data-slot") || "C";`; !core.Contains(js, want) { t.Fatalf("expected js to contain %q", want) } }) } } -func TestTagToClassName_KebabCase_Good(t *testing.T) { +func TestTagToClassName_KebabCaseGood(t *testing.T) { tests := []struct{ tag, want string }{ {"photo-grid", "PhotoGrid"}, {"nav-breadcrumb", "NavBreadcrumb"}, @@ -98,20 +102,21 @@ func TestTagToClassName_KebabCase_Good(t *testing.T) { } } -func TestGenerateBundle_DeduplicatesRegistrations_Good(t *testing.T) { +func TestGenerateBundle_DeduplicatesRegistrationsGood(t *testing.T) { slots := map[string]string{ "H": "nav-bar", "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()) } - if !strings.Contains(js, "NavBar") { + js, _ := result.Value.(string) + if !core.Contains(js, "NavBar") { t.Fatal("expected js to contain NavBar") } - if !strings.Contains(js, "MainContent") { + if !core.Contains(js, "MainContent") { t.Fatal("expected js to contain MainContent") } if got := countSubstr(js, "extends HTMLElement"); got != 2 { @@ -122,21 +127,22 @@ func TestGenerateBundle_DeduplicatesRegistrations_Good(t *testing.T) { } } -func TestGenerateBundle_DeterministicOrdering_Good(t *testing.T) { +func TestGenerateBundle_DeterministicOrderingGood(t *testing.T) { slots := map[string]string{ "Z": "zed-panel", "A": "alpha-panel", "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 := strings.Index(js, "class AlphaPanel") - main := strings.Index(js, "class MainContent") - zed := strings.Index(js, "class ZedPanel") + alpha := indexSubstr(js, "class AlphaPanel") + main := indexSubstr(js, "class MainContent") + zed := indexSubstr(js, "class ZedPanel") if alpha == -1 { t.Fatal("expected AlphaPanel class in js") @@ -161,7 +167,7 @@ func TestGenerateBundle_DeterministicOrdering_Good(t *testing.T) { } } -func TestGenerateTypeScriptDefinitions_DeduplicatesAndOrders_Good(t *testing.T) { +func TestGenerateTypeScriptDefinitions_DeduplicatesAndOrdersGood(t *testing.T) { slots := map[string]string{ "Z": "zed-panel", "A": "alpha-panel", @@ -176,7 +182,7 @@ func TestGenerateTypeScriptDefinitions_DeduplicatesAndOrders_Good(t *testing.T) `"zed-panel": ZedPanel;`, "export {};", } { - if !strings.Contains(dts, want) { + if !core.Contains(dts, want) { t.Fatalf("expected dts to contain %q", want) } } @@ -189,14 +195,14 @@ func TestGenerateTypeScriptDefinitions_DeduplicatesAndOrders_Good(t *testing.T) if got := countSubstr(dts, `export declare class ZedPanel extends HTMLElement`); got != 1 { t.Fatalf("want 1 ZedPanel declaration, got %d", got) } - alphaIdx := strings.Index(dts, `"alpha-panel": AlphaPanel;`) - zedIdx := strings.Index(dts, `"zed-panel": ZedPanel;`) + alphaIdx := indexSubstr(dts, `"alpha-panel": AlphaPanel;`) + zedIdx := indexSubstr(dts, `"zed-panel": ZedPanel;`) if !(alphaIdx < zedIdx) { t.Fatalf("expected alpha-panel (%d) before zed-panel (%d)", alphaIdx, zedIdx) } } -func TestGenerateTypeScriptDefinitions_SkipsInvalidTags_Good(t *testing.T) { +func TestGenerateTypeScriptDefinitions_SkipsInvalidTagsGood(t *testing.T) { slots := map[string]string{ "H": "nav-bar", "C": "Nav-Bar", @@ -205,13 +211,13 @@ func TestGenerateTypeScriptDefinitions_SkipsInvalidTags_Good(t *testing.T) { dts := GenerateTypeScriptDefinitions(slots) - if !strings.Contains(dts, `"nav-bar": NavBar;`) { + if !core.Contains(dts, `"nav-bar": NavBar;`) { t.Fatal(`expected dts to contain "nav-bar": NavBar;`) } - if strings.Contains(dts, "Nav-Bar") { + if core.Contains(dts, "Nav-Bar") { t.Fatal("expected dts NOT to contain Nav-Bar") } - if strings.Contains(dts, "nav bar") { + if core.Contains(dts, "nav bar") { t.Fatal("expected dts NOT to contain nav bar") } if got := countSubstr(dts, `export declare class NavBar extends HTMLElement`); got != 1 { @@ -219,17 +225,17 @@ func TestGenerateTypeScriptDefinitions_SkipsInvalidTags_Good(t *testing.T) { } } -func TestGenerateTypeScriptDefinitions_ValidExtendedTag_Good(t *testing.T) { +func TestGenerateTypeScriptDefinitions_ValidExtendedTagGood(t *testing.T) { slots := map[string]string{ "H": "foo.bar-baz", } dts := GenerateTypeScriptDefinitions(slots) - if !strings.Contains(dts, `"foo.bar-baz": FooBarBaz;`) { + if !core.Contains(dts, `"foo.bar-baz": FooBarBaz;`) { t.Fatal(`expected dts to contain "foo.bar-baz": FooBarBaz;`) } - if !strings.Contains(dts, `export declare class FooBarBaz extends HTMLElement`) { + if !core.Contains(dts, `export declare class FooBarBaz extends HTMLElement`) { t.Fatal("expected dts to contain FooBarBaz class declaration") } } @@ -268,3 +274,79 @@ func indexSubstr(s, substr string) int { return -1 } + +func TestCodegen_GenerateClass_Good(t *T) { + 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) { + result := GenerateClass("notag", "H") + AssertFalse(t, result.OK) + AssertContains(t, result.Error(), "hyphenated") +} + +func TestCodegen_GenerateClass_Ugly(t *T) { + result := GenerateClass("nav-bar", `"&`) + AssertTrue(t, result.OK) + got, _ := result.Value.(string) + AssertContains(t, got, `\"&`) +} + +func TestCodegen_GenerateRegistration_Good(t *T) { + got := GenerateRegistration("nav-bar", "NavBar") + AssertContains(t, got, `customElements.define("nav-bar", NavBar)`) + AssertContains(t, got, ");") +} + +func TestCodegen_GenerateRegistration_Bad(t *T) { + got := GenerateRegistration("", "") + AssertContains(t, got, `customElements.define("", )`) + AssertContains(t, got, ");") +} + +func TestCodegen_GenerateRegistration_Ugly(t *T) { + got := GenerateRegistration(`nav-"bar`, "NavBar") + AssertContains(t, got, `nav-\"bar`) + AssertContains(t, got, "NavBar") +} + +func TestCodegen_TagToClassName_Good(t *T) { + got := TagToClassName("nav-bar") + want := "NavBar" + AssertEqual(t, want, got) +} + +func TestCodegen_TagToClassName_Bad(t *T) { + got := TagToClassName("") + want := "" + AssertEqual(t, want, got) +} + +func TestCodegen_TagToClassName_Ugly(t *T) { + got := TagToClassName("nav-2-item") + want := "Nav2Item" + AssertEqual(t, want, got) +} + +func TestCodegen_GenerateBundle_Good(t *T) { + 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) { + result := GenerateBundle(map[string]string{"H": "notag"}) + AssertFalse(t, result.OK) + AssertContains(t, result.Error(), "hyphenated") +} + +func TestCodegen_GenerateBundle_Ugly(t *T) { + 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/codegen/typescript.go b/codegen/typescript.go index 38aad3f..20956c7 100644 --- a/codegen/typescript.go +++ b/codegen/typescript.go @@ -7,7 +7,7 @@ package codegen import ( "sort" - core "dappco.re/go/core" + core "dappco.re/go" ) // GenerateTypeScriptDefinitions produces ambient TypeScript declarations for diff --git a/codegen/typescript_example_test.go b/codegen/typescript_example_test.go new file mode 100644 index 0000000..793c731 --- /dev/null +++ b/codegen/typescript_example_test.go @@ -0,0 +1,13 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package codegen + +import . "dappco.re/go" + +func ExampleGenerateTypeScriptDefinitions() { + dts := GenerateTypeScriptDefinitions(map[string]string{"H": "nav-bar"}) + Println(Contains(dts, `"nav-bar": NavBar;`)) + // Output: true +} diff --git a/codegen/typescript_test.go b/codegen/typescript_test.go new file mode 100644 index 0000000..477970e --- /dev/null +++ b/codegen/typescript_test.go @@ -0,0 +1,25 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package codegen + +import . "dappco.re/go" + +func TestTypescript_GenerateTypeScriptDefinitions_Good(t *T) { + got := GenerateTypeScriptDefinitions(map[string]string{"H": "nav-bar"}) + AssertContains(t, got, `interface HTMLElementTagNameMap`) + AssertContains(t, got, `export declare class NavBar`) +} + +func TestTypescript_GenerateTypeScriptDefinitions_Bad(t *T) { + got := GenerateTypeScriptDefinitions(map[string]string{"H": "notag"}) + AssertContains(t, got, "declare global") + AssertFalse(t, Contains(got, "Notag")) +} + +func TestTypescript_GenerateTypeScriptDefinitions_Ugly(t *T) { + got := GenerateTypeScriptDefinitions(map[string]string{"H": "nav-bar", "C": "nav-bar"}) + AssertEqual(t, 1, countSubstr(got, `NavBar extends HTMLElement`)) + AssertEqual(t, 1, countSubstr(got, `"nav-bar": NavBar`)) +} diff --git a/context.go b/context.go index 160d552..f0a0230 100644 --- a/context.go +++ b/context.go @@ -33,7 +33,9 @@ func applyLocaleToService(svc Translator, locale string) { } if setter, ok := svc.(interface{ SetLanguage(string) error }); ok { - _ = setter.SetLanguage(serviceLocale(svc, locale)) + if err := setter.SetLanguage(serviceLocale(svc, locale)); err != nil { + return + } } } diff --git a/context_example_test.go b/context_example_test.go new file mode 100644 index 0000000..e74d3c3 --- /dev/null +++ b/context_example_test.go @@ -0,0 +1,47 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +type contextExampleTranslator struct { + lang string +} + +func (tr *contextExampleTranslator) T(key string, _ ...any) string { + return key +} + +func (tr *contextExampleTranslator) SetLanguage(lang string) error { + tr.lang = lang + return nil +} + +func ExampleNewContext() { + ctx := NewContext("en-GB") + core.Println(ctx.Locale, ctx.Data != nil) + // Output: en-GB true +} + +func ExampleNewContextWithService() { + tr := &contextExampleTranslator{} + ctx := NewContextWithService(tr, "en") + core.Println(ctx.Locale, tr.lang) + // Output: en en +} + +func ExampleContext_SetService() { + tr := &contextExampleTranslator{} + ctx := NewContext("cy") + ctx.SetService(tr) + core.Println(tr.lang) + // Output: cy +} + +func ExampleContext_SetLocale() { + tr := &contextExampleTranslator{} + ctx := NewContextWithService(tr) + ctx.SetLocale("fr-FR") + core.Println(ctx.Locale, tr.lang) + // Output: fr-FR fr-FR +} diff --git a/context_test.go b/context_test.go index ea7c6d3..d7e0596 100644 --- a/context_test.go +++ b/context_test.go @@ -3,6 +3,7 @@ package html import ( + core "dappco.re/go" "reflect" "testing" @@ -26,7 +27,7 @@ func (r *recordingTranslator) T(key string, args ...any) string { return "translated" } -func TestNewContext_OptionalLocale_Good(t *testing.T) { +func TestNewContext_OptionalLocaleGood(t *testing.T) { ctx := NewContext("en-GB") if ctx == nil { @@ -40,7 +41,7 @@ func TestNewContext_OptionalLocale_Good(t *testing.T) { } } -func TestNewContextWithService_OptionalLocale_Good(t *testing.T) { +func TestNewContextWithService_OptionalLocaleGood(t *testing.T) { svc, _ := i18n.New() ctx := NewContextWithService(svc, "fr-FR") @@ -55,7 +56,7 @@ func TestNewContextWithService_OptionalLocale_Good(t *testing.T) { } } -func TestNewContextWithService_AppliesLocaleToService_Good(t *testing.T) { +func TestNewContextWithService_AppliesLocaleToServiceGood(t *testing.T) { svc, _ := i18n.New() ctx := NewContextWithService(svc, "fr-FR") @@ -65,7 +66,7 @@ func TestNewContextWithService_AppliesLocaleToService_Good(t *testing.T) { } } -func TestNewContextWithService_ForwardsFullLocaleToTranslator_Good(t *testing.T) { +func TestNewContextWithService_ForwardsFullLocaleToTranslatorGood(t *testing.T) { svc := &recordingTranslator{} ctx := NewContextWithService(svc, "en-GB") @@ -77,7 +78,7 @@ func TestNewContextWithService_ForwardsFullLocaleToTranslator_Good(t *testing.T) } } -func TestTextNode_UsesMetadataAliasWhenDataNil_Good(t *testing.T) { +func TestTextNode_UsesMetadataAliasWhenDataNilGood(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) @@ -91,7 +92,7 @@ func TestTextNode_UsesMetadataAliasWhenDataNil_Good(t *testing.T) { } } -func TestTextNode_CustomTranslatorReceivesCountArgs_Good(t *testing.T) { +func TestTextNode_CustomTranslatorReceivesCountArgsGood(t *testing.T) { ctx := NewContextWithService(&recordingTranslator{}) ctx.Metadata["count"] = 3 @@ -111,7 +112,7 @@ func TestTextNode_CustomTranslatorReceivesCountArgs_Good(t *testing.T) { } } -func TestTextNode_NonCountKey_DoesNotInjectCount_Good(t *testing.T) { +func TestTextNode_NonCountKey_DoesNotInjectCountGood(t *testing.T) { ctx := NewContextWithService(&recordingTranslator{}) ctx.Metadata["count"] = 3 @@ -181,7 +182,7 @@ func TestContext_SetLocale_NilContext_Ugly(t *testing.T) { } } -func TestCloneContext_PreservesMetadataAlias_Good(t *testing.T) { +func TestCloneContext_PreservesMetadataAliasGood(t *testing.T) { ctx := NewContext() ctx.Data["count"] = 3 @@ -202,3 +203,79 @@ func TestCloneContext_PreservesMetadataAlias_Good(t *testing.T) { t.Fatalf("cloneContext should copy map contents, got Data=%v Metadata=%v", clone.Data, clone.Metadata) } } + +func TestContext_NewContext_Good(t *core.T) { + ctx := NewContext("cy") + core.AssertEqual(t, "cy", ctx.Locale) + core.AssertEqual(t, ctx.Data, ctx.Metadata) +} + +func TestContext_NewContext_Bad(t *core.T) { + ctx := NewContext() + got := ctx.Locale + core.AssertEqual(t, "", got) +} + +func TestContext_NewContext_Ugly(t *core.T) { + ctx := NewContext("") + got := ctx.Metadata + core.AssertNotNil(t, got) +} + +func TestContext_NewContextWithService_Good(t *core.T) { + tr := &complianceTranslator{} + ctx := NewContextWithService(tr, "cy") + core.AssertEqual(t, "cy", ctx.Locale) +} + +func TestContext_NewContextWithService_Bad(t *core.T) { + ctx := NewContextWithService(nil, "cy") + got := ctx.Locale + core.AssertEqual(t, "cy", got) +} + +func TestContext_NewContextWithService_Ugly(t *core.T) { + tr := &complianceTranslator{available: []string{"en"}} + NewContextWithService(tr, "en-GB") + core.AssertEqual(t, "en", tr.lang) +} + +func TestContext_Context_SetService_Good(t *core.T) { + tr := &complianceTranslator{} + ctx := NewContext("cy") + got := ctx.SetService(tr) + core.AssertEqual(t, ctx, got) +} + +func TestContext_Context_SetService_Bad(t *core.T) { + var ctx *Context + got := ctx.SetService(&complianceTranslator{}) + core.AssertNil(t, got) +} + +func TestContext_Context_SetService_Ugly(t *core.T) { + tr := &complianceTranslator{} + ctx := NewContext("cy") + ctx.SetService(tr) + core.AssertEqual(t, "cy", tr.lang) +} + +func TestContext_Context_SetLocale_Good(t *core.T) { + tr := &complianceTranslator{available: []string{"en"}} + ctx := NewContextWithService(tr) + got := ctx.SetLocale("en-GB") + core.AssertEqual(t, ctx, got) +} + +func TestContext_Context_SetLocale_Bad(t *core.T) { + var ctx *Context + got := ctx.SetLocale("en") + core.AssertNil(t, got) +} + +func TestContext_Context_SetLocale_Ugly(t *core.T) { + tr := &complianceTranslator{available: []string{"en"}} + ctx := NewContextWithService(tr) + ctx.SetLocale("en-GB") + core.AssertEqual(t, "en", tr.lang) +} diff --git a/edge_test.go b/edge_test.go index b0823f9..5f33338 100644 --- a/edge_test.go +++ b/edge_test.go @@ -39,7 +39,7 @@ func TestText_Emoji_Ugly(t *testing.T) { } } -func TestEl_Emoji_Ugly(t *testing.T) { +func TestEl_EmojiUgly(t *testing.T) { ctx := NewContext() node := El("span", Raw("\U0001F680 Launch")) got := node.Render(ctx) @@ -86,7 +86,7 @@ func TestEl_RTL_Ugly(t *testing.T) { } } -func TestText_ZeroWidth_Ugly(t *testing.T) { +func TestText_ZeroWidthUgly(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) ctx := NewContext() @@ -112,7 +112,7 @@ func TestText_ZeroWidth_Ugly(t *testing.T) { } } -func TestText_MixedScripts_Ugly(t *testing.T) { +func TestText_MixedScriptsUgly(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) ctx := NewContext() @@ -139,7 +139,7 @@ func TestText_MixedScripts_Ugly(t *testing.T) { } } -func TestStripTags_Unicode_Ugly(t *testing.T) { +func TestStripTags_UnicodeUgly(t *testing.T) { tests := []struct { name string input string @@ -161,7 +161,7 @@ func TestStripTags_Unicode_Ugly(t *testing.T) { } } -func TestAttr_UnicodeValue_Ugly(t *testing.T) { +func TestAttr_UnicodeValueUgly(t *testing.T) { ctx := NewContext() node := Attr(El("div"), "title", "\U0001F680 Rocket Launch") got := node.Render(ctx) @@ -173,7 +173,7 @@ func TestAttr_UnicodeValue_Ugly(t *testing.T) { // --- Deep nesting stress tests --- -func TestLayout_DeepNesting10Levels_Ugly(t *testing.T) { +func TestLayout_DeepNesting10LevelsUgly(t *testing.T) { ctx := NewContext() // Build 10 levels of nested layouts @@ -204,7 +204,7 @@ func TestLayout_DeepNesting10Levels_Ugly(t *testing.T) { } } -func TestLayout_DeepNesting20Levels_Ugly(t *testing.T) { +func TestLayout_DeepNesting20LevelsUgly(t *testing.T) { ctx := NewContext() current := NewLayout("C").C(Raw("bottom")) @@ -222,7 +222,7 @@ func TestLayout_DeepNesting20Levels_Ugly(t *testing.T) { } } -func TestLayout_DeepNestingMixedSlots_Ugly(t *testing.T) { +func TestLayout_DeepNestingMixedSlotsUgly(t *testing.T) { ctx := NewContext() // Alternate slot types at each level: C -> L -> C -> L -> ... @@ -241,7 +241,7 @@ func TestLayout_DeepNestingMixedSlots_Ugly(t *testing.T) { } } -func TestEach_LargeIteration1000_Ugly(t *testing.T) { +func TestEach_LargeIteration1000Ugly(t *testing.T) { ctx := NewContext() items := make([]int, 1000) for i := range items { @@ -265,7 +265,7 @@ func TestEach_LargeIteration1000_Ugly(t *testing.T) { } } -func TestEach_LargeIteration5000_Ugly(t *testing.T) { +func TestEach_LargeIteration5000Ugly(t *testing.T) { ctx := NewContext() items := make([]int, 5000) for i := range items { @@ -283,7 +283,7 @@ func TestEach_LargeIteration5000_Ugly(t *testing.T) { } } -func TestEach_NestedEach_Ugly(t *testing.T) { +func TestEach_NestedEachUgly(t *testing.T) { ctx := NewContext() rows := []int{0, 1, 2} cols := []string{"a", "b", "c"} @@ -307,7 +307,7 @@ func TestEach_NestedEach_Ugly(t *testing.T) { } } -func TestEach_WrappedElement_PreservesItemPaths_Good(t *testing.T) { +func TestEach_WrappedElement_PreservesItemPathsGood(t *testing.T) { ctx := NewContext() node := Each([]string{"a", "b"}, func(item string) Node { @@ -324,7 +324,7 @@ func TestEach_WrappedElement_PreservesItemPaths_Good(t *testing.T) { } } -func TestEach_WrappedLayout_PreservesBlockPath_Good(t *testing.T) { +func TestEach_WrappedLayout_PreservesBlockPathGood(t *testing.T) { ctx := NewContext() inner := NewLayout("C").C(Raw("item")) @@ -341,7 +341,7 @@ func TestEach_WrappedLayout_PreservesBlockPath_Good(t *testing.T) { // --- Layout variant validation --- -func TestLayout_InvalidVariantChars_Bad(t *testing.T) { +func TestLayout_InvalidVariantCharsBad(t *testing.T) { ctx := NewContext() tests := []struct { @@ -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()) @@ -416,7 +416,7 @@ func TestLayout_VariantError_NoOp_Good(t *testing.T) { } } -func TestValidateLayoutVariant_NoOp_Good(t *testing.T) { +func TestValidateLayoutVariant_NoOpGood(t *testing.T) { tests := []struct { name string variant string @@ -428,15 +428,15 @@ func TestValidateLayoutVariant_NoOp_Good(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) } }) } } -func TestLayout_InvalidVariantMixedValidInvalid_Bad(t *testing.T) { +func TestLayout_InvalidVariantMixedValidInvalidBad(t *testing.T) { ctx := NewContext() // "HXC" — H and C are valid, X is not. Only H and C should render. @@ -456,7 +456,7 @@ func TestLayout_InvalidVariantMixedValidInvalid_Bad(t *testing.T) { } } -func TestLayout_DuplicateVariantChars_Ugly(t *testing.T) { +func TestLayout_DuplicateVariantCharsUgly(t *testing.T) { ctx := NewContext() // "CCC" — C appears three times. Should render C slot content three times. @@ -469,7 +469,7 @@ func TestLayout_DuplicateVariantChars_Ugly(t *testing.T) { } } -func TestLayout_DuplicateVariantChars_UniqueBlockIDs_Good(t *testing.T) { +func TestLayout_DuplicateVariantChars_UniqueBlockIDsGood(t *testing.T) { ctx := NewContext() layout := NewLayout("CCC").C(Raw("content")) @@ -482,7 +482,7 @@ func TestLayout_DuplicateVariantChars_UniqueBlockIDs_Good(t *testing.T) { } } -func TestLayout_EmptySlots_Ugly(t *testing.T) { +func TestLayout_EmptySlotsUgly(t *testing.T) { ctx := NewContext() // Variant includes all slots but none are populated — should produce empty output. @@ -494,7 +494,7 @@ func TestLayout_EmptySlots_Ugly(t *testing.T) { } } -func TestLayout_NestedThroughIf_Ugly(t *testing.T) { +func TestLayout_NestedThroughIfUgly(t *testing.T) { ctx := NewContext() inner := NewLayout("C").C(Raw("wrapped")) @@ -507,7 +507,7 @@ func TestLayout_NestedThroughIf_Ugly(t *testing.T) { } } -func TestLayout_NestedThroughSwitch_Ugly(t *testing.T) { +func TestLayout_NestedThroughSwitchUgly(t *testing.T) { ctx := NewContext() inner := NewLayout("C").C(Raw("wrapped")) @@ -525,7 +525,7 @@ func TestLayout_NestedThroughSwitch_Ugly(t *testing.T) { // --- Render convenience function edge cases --- -func TestRender_NilContext_Ugly(t *testing.T) { +func TestRender_NilContextUgly(t *testing.T) { node := Raw("test") got := Render(node, nil) if got != "test" { @@ -533,7 +533,7 @@ func TestRender_NilContext_Ugly(t *testing.T) { } } -func TestImprint_NilContext_Ugly(t *testing.T) { +func TestImprint_NilContextUgly(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) @@ -545,7 +545,7 @@ func TestImprint_NilContext_Ugly(t *testing.T) { } } -func TestCompareVariants_NilContext_Ugly(t *testing.T) { +func TestCompareVariants_NilContextUgly(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) @@ -559,7 +559,7 @@ func TestCompareVariants_NilContext_Ugly(t *testing.T) { } } -func TestCompareVariants_SingleVariant_Ugly(t *testing.T) { +func TestCompareVariants_SingleVariantUgly(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) @@ -574,7 +574,7 @@ func TestCompareVariants_SingleVariant_Ugly(t *testing.T) { // --- escapeHTML / escapeAttr edge cases --- -func TestEscapeAttr_AllSpecialChars_Ugly(t *testing.T) { +func TestEscapeAttr_AllSpecialCharsUgly(t *testing.T) { ctx := NewContext() node := Attr(El("div"), "data-val", `&<>"'`) got := node.Render(ctx) @@ -587,7 +587,7 @@ func TestEscapeAttr_AllSpecialChars_Ugly(t *testing.T) { } } -func TestElNode_EmptyTag_Ugly(t *testing.T) { +func TestElNode_EmptyTagUgly(t *testing.T) { ctx := NewContext() node := El("", Raw("content")) got := node.Render(ctx) @@ -598,7 +598,7 @@ func TestElNode_EmptyTag_Ugly(t *testing.T) { } } -func TestSwitchNode_NoMatch_Ugly(t *testing.T) { +func TestSwitchNode_NoMatchUgly(t *testing.T) { ctx := NewContext() cases := map[string]Node{ "a": Raw("alpha"), @@ -611,7 +611,7 @@ func TestSwitchNode_NoMatch_Ugly(t *testing.T) { } } -func TestEntitled_NilContext_Ugly(t *testing.T) { +func TestEntitled_NilContextUgly(t *testing.T) { node := Entitled("premium", Raw("content")) got := node.Render(nil) if got != "" { diff --git a/entitled_example_test.go b/entitled_example_test.go new file mode 100644 index 0000000..e932ab4 --- /dev/null +++ b/entitled_example_test.go @@ -0,0 +1,26 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +type AllChecker = denyAllChecker + +func ExampleAllChecker_Check() { + var checker EntitlementChecker = AllChecker{} + core.Println(checker.Check("premium")) + // Output: false +} + +func ExampleNode_Render_empty() { + var node Node = emptyNode{} + core.Println(node.Render(NewContext()) == "") + // Output: true +} + +func ExampleEntitled() { + ctx := NewContext() + ctx.Entitlements = func(feature string) bool { return feature == "premium" } + core.Println(Entitled("premium", Text("allowed")).Render(ctx)) + // Output: allowed +} diff --git a/entitled_test.go b/entitled_test.go index 83afe73..0b87d46 100644 --- a/entitled_test.go +++ b/entitled_test.go @@ -2,7 +2,10 @@ package html -import "testing" +import ( + core "dappco.re/go" + "testing" +) type stubEntitlementChecker map[string]bool @@ -55,3 +58,58 @@ func TestEntitledChecker_GoodBadUgly(t *testing.T) { }) } } + +func TestEntitled_AllChecker_Check_Good(t *core.T) { + checker := denyAllChecker{} + got := checker.Check("premium") + core.AssertFalse(t, got) +} + +func TestEntitled_AllChecker_Check_Bad(t *core.T) { + checker := denyAllChecker{} + got := checker.Check("") + core.AssertFalse(t, got) +} + +func TestEntitled_AllChecker_Check_Ugly(t *core.T) { + var checker EntitlementChecker = denyAllChecker{} + got := checker.Check("anything") + core.AssertFalse(t, got) +} + +func TestEntitled_Entitled_Good(t *core.T) { + node := Entitled(stubEntitlementChecker{"premium": true}, "premium", Text("granted")) + got := node.Render(NewContext()) + core.AssertEqual(t, "granted", got) +} + +func TestEntitled_Entitled_Bad(t *core.T) { + node := Entitled(stubEntitlementChecker{"premium": false}, "premium", Text("denied")) + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestEntitled_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 TestEntitled_Node_Render_Good(t *core.T) { + var node Node = emptyNode{} + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestEntitled_Node_Render_Bad(t *core.T) { + node := emptySentinel() + got := node.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestEntitled_Node_Render_Ugly(t *core.T) { + var node Node = (*entitledNode)(nil) + got := Render(node, NewContext()) + core.AssertEqual(t, "", got) +} diff --git a/go.mod b/go.mod index ed4e490..13a26f6 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ module dappco.re/go/html -go 1.26.0 +go 1.26.2 require ( - dappco.re/go/core v0.8.0-alpha.1 dappco.re/go/i18n v0.8.0-alpha.1 dappco.re/go/io v0.8.0-alpha.1 dappco.re/go/log v0.8.0-alpha.1 @@ -12,15 +11,39 @@ require ( ) require ( - dappco.re/go/inference v0.8.0-alpha.1 // indirect - golang.org/x/text v0.36.0 // indirect + 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 + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) -replace ( - dappco.re/go/core => ../go - dappco.re/go/i18n => ../go-i18n - dappco.re/go/inference => ../go-inference - dappco.re/go/io => ../go-io - dappco.re/go/log => ../go-log - dappco.re/go/process => ../go-process +require ( + dappco.re/go v0.9.0 + dappco.re/go/inference v0.8.0-alpha.1 // indirect + golang.org/x/text v0.36.0 // indirect ) diff --git a/go.sum b/go.sum index 6a755ae..514efd3 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,105 @@ +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/core/i18n v0.2.1 h1:BeEThqNmQxFoGHY95jSlawq8+RmJBEz4fZ7D7eRQSJo= -dappco.re/go/core/i18n v0.2.1/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok= -dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= -dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= -dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= -dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= -dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM= -dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as= -forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0= -forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= -forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= +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/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/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= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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/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= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +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/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= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work.sum b/go.work.sum index 141fcc6..da30244 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,5 @@ +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/ws v0.2.4 h1:aQjFzI7VsZVUYeuArnKsAnrl2Oq7L1VtTcVk9RF6W1o= dappco.re/go/core/ws v0.2.4/go.mod h1:C3riJyLLcV6QhLvYlq3P/XkGTsN598qQeGBoLdoHBU4= forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= @@ -208,17 +210,22 @@ golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/grammar_example_test.go b/grammar_example_test.go new file mode 100644 index 0000000..c549095 --- /dev/null +++ b/grammar_example_test.go @@ -0,0 +1,11 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +func ExampleGrammarImprint_Imprint() { + stamp := (&GrammarImprint{}).Imprint(El("section", Text("body")), *NewContext()) + core.Println(stamp.Path, stamp.Tags[0]) + // Output: 0 branch +} diff --git a/grammar_test.go b/grammar_test.go index ed3ee01..fc3c425 100644 --- a/grammar_test.go +++ b/grammar_test.go @@ -1,11 +1,12 @@ package html import ( + core "dappco.re/go" "slices" "testing" ) -func TestGrammarImprint_KnownTreePathDeterministic_Good(t *testing.T) { +func TestGrammarImprint_KnownTreePathDeterministicGood(t *testing.T) { ctx := Context{} page := NewLayout("HCF"). H(El("h1", Raw("title"))). @@ -39,7 +40,7 @@ func TestGrammarImprint_KnownTreePathDeterministic_Good(t *testing.T) { } } -func TestGrammarImprint_UnsetNode_Bad(t *testing.T) { +func TestGrammarImprint_UnsetNodeBad(t *testing.T) { var node Node got := (&GrammarImprint{}).Imprint(node, Context{}) @@ -49,7 +50,7 @@ func TestGrammarImprint_UnsetNode_Bad(t *testing.T) { } } -func TestGrammarImprint_DoesNotRenderContent_Good(t *testing.T) { +func TestGrammarImprint_DoesNotRenderContentGood(t *testing.T) { got := (&GrammarImprint{}).Imprint(grammarPanicNode{}, Context{}) if got.Path != "0" { @@ -63,7 +64,7 @@ func TestGrammarImprint_DoesNotRenderContent_Good(t *testing.T) { } } -func TestGrammarImprint_DeepNestedPathBudget_Ugly(t *testing.T) { +func TestGrammarImprint_DeepNestedPathBudgetUgly(t *testing.T) { var node Node = Raw("leaf") for range defaultGrammarImprintMaxDepth * 3 { node = NewLayout("C").C(node) @@ -87,3 +88,21 @@ type grammarPanicNode struct{} func (grammarPanicNode) Render(*Context) string { panic("GrammarImprint must not render nodes") } + +func TestGrammar_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 TestGrammar_GrammarImprint_Imprint_Bad(t *core.T) { + stamp := (&GrammarImprint{}).Imprint(nil, *NewContext()) + core.AssertEqual(t, Stamp{}, stamp) + core.AssertEqual(t, uint64(0), stamp.Hash) +} + +func TestGrammar_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) +} diff --git a/integration_test.go b/integration_test.go index ff841dd..63b56d8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -6,7 +6,7 @@ import ( i18n "dappco.re/go/i18n" ) -func TestIntegration_RenderThenReverse_Good(t *testing.T) { +func TestIntegration_RenderThenReverseGood(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) ctx := NewContext() @@ -26,7 +26,7 @@ func TestIntegration_RenderThenReverse_Good(t *testing.T) { } } -func TestIntegration_ResponsiveImprint_Good(t *testing.T) { +func TestIntegration_ResponsiveImprintGood(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) ctx := NewContext() 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 new file mode 100644 index 0000000..2e9be89 --- /dev/null +++ b/layout_example_test.go @@ -0,0 +1,66 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +type InvalidVariantSentinel = layoutInvalidVariantSentinel +type VariantError = layoutVariantError + +func ExampleInvalidVariantSentinel_Error() { + core.Println(InvalidVariantSentinel{}.Error()) + // Output: html: invalid layout variant +} + +func ExampleNewLayout() { + core.Println(NewLayout("C").C(Text("body")).Render(NewContext())) + // Output:
body
+} + +func ExampleValidateLayoutVariant() { + result := ValidateLayoutVariant("???") + core.Println(result.OK, result.Value == nil) + // Output: true true +} + +func ExampleLayout_H() { + core.Println(NewLayout("H").H(Text("head")).Render(NewContext())) + // Output:
head
+} + +func ExampleLayout_L() { + core.Println(NewLayout("L").L(Text("nav")).Render(NewContext())) + // Output: +} + +func ExampleLayout_C() { + core.Println(NewLayout("C").C(Text("body")).Render(NewContext())) + // Output:
body
+} + +func ExampleLayout_R() { + core.Println(NewLayout("R").R(Text("side")).Render(NewContext())) + // Output: +} + +func ExampleLayout_F() { + core.Println(NewLayout("F").F(Text("foot")).Render(NewContext())) + // Output:
foot
+} + +func ExampleLayout_VariantError() { + result := NewLayout("C").VariantError() + core.Println(result.OK, result.Value == nil) + // Output: true true +} + +func ExampleLayout_Render() { + core.Println(NewLayout("C").C(Text("content")).Render(NewContext())) + // Output:
content
+} + +func ExampleVariantError_Error() { + err := &VariantError{variant: "XYZ"} + core.Println(err.Error()) + // Output: html: invalid layout variant XYZ +} diff --git a/layout_test.go b/layout_test.go index 1460a1d..126e141 100644 --- a/layout_test.go +++ b/layout_test.go @@ -1,6 +1,7 @@ package html import ( + core "dappco.re/go" "testing" ) @@ -60,7 +61,7 @@ func TestLayout_HCF_Good(t *testing.T) { } } -func TestLayout_ContentOnly_Good(t *testing.T) { +func TestLayout_ContentOnlyGood(t *testing.T) { ctx := NewContext() layout := NewLayout("C"). H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer")) @@ -82,7 +83,7 @@ func TestLayout_ContentOnly_Good(t *testing.T) { } } -func TestLayout_FluentAPI_Good(t *testing.T) { +func TestLayout_FluentAPIGood(t *testing.T) { layout := NewLayout("HLCRF") // Fluent methods should return the same layout for chaining @@ -97,7 +98,7 @@ func TestLayout_FluentAPI_Good(t *testing.T) { } } -func TestLayout_IgnoresInvalidSlots_Good(t *testing.T) { +func TestLayout_IgnoresInvalidSlotsGood(t *testing.T) { ctx := NewContext() // "C" variant: populating L and R should have no effect layout := NewLayout("C").L(Raw("left")).C(Raw("main")).R(Raw("right")) @@ -114,7 +115,7 @@ func TestLayout_IgnoresInvalidSlots_Good(t *testing.T) { } } -func TestLayout_Methods_NilLayout_Ugly(t *testing.T) { +func TestLayout_Methods_NilLayoutUgly(t *testing.T) { var layout *Layout if layout.H(Raw("h")) != nil { @@ -147,3 +148,204 @@ func TestLayout_Render_NilContext_Good(t *testing.T) { t.Fatalf("layout.Render(nil) = %q, want %q", got, want) } } + +func TestLayout_InvalidVariantSentinel_Error_Good(t *core.T) { + err := layoutInvalidVariantSentinel{} + got := err.Error() + core.AssertEqual(t, "html: invalid layout variant", got) +} + +func TestLayout_InvalidVariantSentinel_Error_Bad(t *core.T) { + err := layoutInvalidVariantSentinel{} + got := err.Error() + core.AssertNotEqual(t, "", got) +} + +func TestLayout_InvalidVariantSentinel_Error_Ugly(t *core.T) { + got := ErrInvalidLayoutVariant.Error() + want := "html: invalid layout variant" + core.AssertEqual(t, want, got) +} + +func TestLayout_NewLayout_Good(t *core.T) { + l := NewLayout("C") + core.AssertNotNil(t, l) + core.AssertEqual(t, "", l.Render(NewContext())) +} + +func TestLayout_NewLayout_Bad(t *core.T) { + l := NewLayout("") + got := l.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestLayout_NewLayout_Ugly(t *core.T) { + l := NewLayout("XC").C(Text("content")) + got := l.Render(NewContext()) + core.AssertContains(t, got, "content") +} + +func TestLayout_ValidateLayoutVariant_Good(t *core.T) { + result := ValidateLayoutVariant("HLCRF") + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) +} + +func TestLayout_ValidateLayoutVariant_Bad(t *core.T) { + result := ValidateLayoutVariant("???") + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) +} + +func TestLayout_ValidateLayoutVariant_Ugly(t *core.T) { + result := ValidateLayoutVariant("") + core.AssertTrue(t, result.OK) + core.AssertNil(t, result.Value) +} + +func TestLayout_Layout_H_Good(t *core.T) { + l := NewLayout("H").H(Text("head")) + got := l.Render(NewContext()) + core.AssertContains(t, got, "content`, got) +} + +func TestLayout_Layout_Render_Bad(t *core.T) { + var l *Layout + got := l.Render(NewContext()) + core.AssertEqual(t, "", got) +} + +func TestLayout_Layout_Render_Ugly(t *core.T) { + l := NewLayout("XC").C(Text("content")) + got := l.Render(nil) + core.AssertContains(t, got, "content") +} + +func TestLayout_VariantError_Error_Good(t *core.T) { + err := &layoutVariantError{variant: "XYZ"} + got := err.Error() + core.AssertEqual(t, "html: invalid layout variant XYZ", got) +} + +func TestLayout_VariantError_Error_Bad(t *core.T) { + err := &layoutVariantError{} + got := err.Error() + core.AssertEqual(t, "html: invalid layout variant ", got) +} + +func TestLayout_VariantError_Error_Ugly(t *core.T) { + err := &layoutVariantError{variant: "\n"} + got := err.Error() + core.AssertContains(t, got, "\n") +} 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/node_example_test.go b/node_example_test.go new file mode 100644 index 0000000..819482e --- /dev/null +++ b/node_example_test.go @@ -0,0 +1,89 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import core "dappco.re/go" + +func ExampleRaw() { + core.Println(Raw("trusted").Render(NewContext())) + // Output: trusted +} + +func ExampleNode_Render() { + var node Node = Text("hello") + core.Println(node.Render(NewContext())) + // Output: hello +} + +func ExampleEl() { + core.Println(El("span", Text("ok")).Render(NewContext())) + // Output: ok +} + +func ExampleAttr() { + core.Println(Attr(El("a", Text("Docs")), "href", "/docs").Render(NewContext())) + // Output: Docs +} + +func ExampleAriaLabel() { + core.Println(AriaLabel(El("button", Text("Save")), "Save changes").Render(NewContext())) + // Output: +} + +func ExampleAltText() { + core.Println(AltText(El("img"), "Profile photo").Render(NewContext())) + // Output: Profile photo +} + +func ExampleTabIndex() { + core.Println(TabIndex(El("button", Text("Save")), 0).Render(NewContext())) + // Output: +} + +func ExampleAutoFocus() { + core.Println(AutoFocus(El("input")).Render(NewContext())) + // Output: +} + +func ExampleRole() { + core.Println(Role(El("nav", Text("Links")), "navigation").Render(NewContext())) + // Output: +} + +func ExampleText() { + core.Println(Text("safe").Render(NewContext())) + // Output: <b>safe</b> +} + +func ExampleIf() { + node := If(func(*Context) bool { return true }, Text("yes")) + core.Println(node.Render(NewContext())) + // Output: yes +} + +func ExampleUnless() { + node := Unless(func(*Context) bool { return false }, Text("yes")) + core.Println(node.Render(NewContext())) + // Output: yes +} + +func ExampleSwitch() { + node := Switch(func(*Context) string { return "en" }, map[string]Node{"en": Text("hello")}) + core.Println(node.Render(NewContext())) + // Output: hello +} + +func ExampleEach() { + node := Each([]string{"a", "b"}, func(v string) Node { return Text(v) }) + core.Println(node.Render(NewContext())) + // Output: ab +} + +func ExampleEachSeq() { + node := EachSeq[string](func(yield func(string) bool) { + yield("a") + yield("b") + }, func(v string) Node { return Text(v) }) + core.Println(node.Render(NewContext())) + // Output: ab +} diff --git a/node_test.go b/node_test.go index 2d9e3f9..213434c 100644 --- a/node_test.go +++ b/node_test.go @@ -1,6 +1,7 @@ package html import ( + core "dappco.re/go" "testing" i18n "dappco.re/go/i18n" @@ -26,7 +27,7 @@ func TestElNode_Render_Good(t *testing.T) { } } -func TestElNode_Nested_Good(t *testing.T) { +func TestElNode_NestedGood(t *testing.T) { ctx := NewContext() node := El("div", El("span", Raw("inner"))) got := node.Render(ctx) @@ -36,7 +37,7 @@ func TestElNode_Nested_Good(t *testing.T) { } } -func TestLayout_DirectElementBlockPath_Good(t *testing.T) { +func TestLayout_DirectElementBlockPathGood(t *testing.T) { ctx := NewContext() got := NewLayout("C").C(El("div", Raw("content"))).Render(ctx) @@ -45,7 +46,7 @@ func TestLayout_DirectElementBlockPath_Good(t *testing.T) { } } -func TestLayout_EachElementBlockPaths_Good(t *testing.T) { +func TestLayout_EachElementBlockPathsGood(t *testing.T) { ctx := NewContext() got := NewLayout("C").C( Each([]string{"a", "b"}, func(item string) Node { @@ -61,7 +62,7 @@ func TestLayout_EachElementBlockPaths_Good(t *testing.T) { } } -func TestElNode_MultipleChildren_Good(t *testing.T) { +func TestElNode_MultipleChildrenGood(t *testing.T) { ctx := NewContext() node := El("div", Raw("a"), Raw("b")) got := node.Render(ctx) @@ -71,7 +72,7 @@ func TestElNode_MultipleChildren_Good(t *testing.T) { } } -func TestElNode_VoidElement_Good(t *testing.T) { +func TestElNode_VoidElementGood(t *testing.T) { ctx := NewContext() node := El("br") got := node.Render(ctx) @@ -90,7 +91,7 @@ func TestTextNode_Render_Good(t *testing.T) { } } -func TestTextNode_UsesContextDataForCount_Good(t *testing.T) { +func TestTextNode_UsesContextDataForCountGood(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) @@ -129,7 +130,7 @@ func TestTextNode_UsesContextDataForCount_Good(t *testing.T) { } } -func TestTextNode_Escapes_Good(t *testing.T) { +func TestTextNode_EscapesGood(t *testing.T) { ctx := NewContext() node := Text("") 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) +}