fusee-framework 2.7.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +56 -2
  2. package/bin/create-fusee-linux +0 -0
  3. package/bin/create-fusee-mac-arm64 +0 -0
  4. package/bin/create-fusee-mac-intel +0 -0
  5. package/bin/create-fusee-win.exe +0 -0
  6. package/framework/auto-import-preset.js +20 -12
  7. package/framework/auto-imports.d.ts +68 -39
  8. package/framework/core/compiler.js +310 -310
  9. package/framework/core/component-core.js +445 -0
  10. package/framework/core/component.js +11 -440
  11. package/framework/core/composable.js +29 -29
  12. package/framework/core/h.js +588 -557
  13. package/framework/core/jsx-helpers.js +11 -2
  14. package/framework/core/jsx-runtime.js +73 -73
  15. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm +0 -0
  16. package/framework/core/store.js +333 -333
  17. package/framework/globals.js +42 -0
  18. package/framework/index.js +22 -55
  19. package/framework/router/file-router.js +184 -184
  20. package/framework/server/actions.js +183 -183
  21. package/framework/server/generate-manifest.js +3 -3
  22. package/framework/types/component.d.ts +97 -86
  23. package/framework/types/index.d.ts +118 -117
  24. package/package.json +83 -80
  25. package/framework/core/rust-compiler/Cargo.toml +0 -31
  26. package/framework/core/rust-compiler/README.md +0 -69
  27. package/framework/core/rust-compiler/pkg/README.md +0 -69
  28. package/framework/core/rust-compiler/pkg/fusee_compiler.d.ts +0 -6
  29. package/framework/core/rust-compiler/pkg/fusee_compiler.js +0 -9
  30. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.js +0 -137
  31. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm.d.ts +0 -11
  32. package/framework/core/rust-compiler/pkg/package.json +0 -18
  33. package/framework/core/rust-compiler/src/ast.rs +0 -274
  34. package/framework/core/rust-compiler/src/errors.rs +0 -204
  35. package/framework/core/rust-compiler/src/generator.rs +0 -583
  36. package/framework/core/rust-compiler/src/lexer.rs +0 -414
  37. package/framework/core/rust-compiler/src/lib.rs +0 -87
  38. package/framework/core/rust-compiler/src/main_compiler.rs +0 -165
  39. package/framework/core/rust-compiler/src/parser.rs +0 -516
  40. package/framework/core/rust-compiler/src/ssr_generator.rs +0 -688
  41. package/framework/core/rust-compiler/src/transformer.rs +0 -562
  42. package/framework/core/rust-compiler/src/utils.rs +0 -10
  43. package/framework/engine-go/actions.go +0 -112
  44. package/framework/engine-go/cmd/server/main.go +0 -233
  45. package/framework/engine-go/comet/comet.go +0 -41
  46. package/framework/engine-go/comet/fragment.go +0 -51
  47. package/framework/engine-go/dispatcher.go +0 -120
  48. package/framework/engine-go/elements.go +0 -248
  49. package/framework/engine-go/evalexpr/evalexpr.go +0 -733
  50. package/framework/engine-go/go.mod +0 -3
  51. package/framework/engine-go/hydration.go +0 -43
  52. package/framework/engine-go/load-helpers.go +0 -29
  53. package/framework/engine-go/manifest.go +0 -41
  54. package/framework/engine-go/reactive.go +0 -123
  55. package/framework/engine-go/render.go +0 -635
  56. package/framework/engine-go/reqcontext.go +0 -48
  57. package/framework/engine-go/router.go +0 -85
  58. package/framework/engine-go/rustast/rustast.go +0 -317
  59. package/framework/engine-go/ssr.go +0 -103
  60. package/framework/engine-go/static-assets.go +0 -139
  61. package/framework/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -1,233 +0,0 @@
1
- package main
2
-
3
- import (
4
- "flag"
5
- "fmt"
6
- "log"
7
- "net/http"
8
- "os"
9
- "path/filepath"
10
-
11
- engine "fusee"
12
- "fusee/evalexpr"
13
- "fusee/rustast"
14
- )
15
-
16
- type PageHandler struct {
17
- engine.Module
18
- AST *rustast.Node
19
- Title string
20
- }
21
-
22
- func main() {
23
- port := flag.Int("port", 3000, "Port to listen on")
24
- distDir := flag.String("dist", "./dist", "Path to dist directory for static assets")
25
- flag.Parse()
26
-
27
- // 1. Locate project root (containing package.json)
28
- rootDir := "."
29
- for _, candidate := range []string{".", "..", "../..", "../../.."} {
30
- if _, err := os.Stat(filepath.Join(candidate, "package.json")); err == nil {
31
- rootDir = candidate
32
- break
33
- }
34
- }
35
- absRoot, _ := filepath.Abs(rootDir)
36
-
37
- // 2. Initialize Server Actions Registry
38
- actionReg := engine.NewActionRegistry()
39
- actionReg.DefineAction("submitContact", func(args []any) (any, error) {
40
- var name, email string
41
- if len(args) > 0 {
42
- if m, ok := args[0].(map[string]any); ok {
43
- name, _ = m["name"].(string)
44
- email, _ = m["email"].(string)
45
- }
46
- }
47
- log.Printf("[Action] submitContact received from %s <%s>", name, email)
48
- return map[string]any{
49
- "success": true,
50
- "message": fmt.Sprintf("Thank you, %s! We received your message.", name),
51
- }, nil
52
- })
53
-
54
- actionReg.DefineAction("submitFeedback", func(args []any) (any, error) {
55
- var message string
56
- if len(args) > 0 {
57
- if m, ok := args[0].(map[string]any); ok {
58
- message, _ = m["message"].(string)
59
- }
60
- }
61
- log.Printf("[Action] submitFeedback received: %q", message)
62
- return map[string]any{
63
- "ok": true,
64
- "received": message,
65
- }, nil
66
- })
67
-
68
- // 3. Dynamically Load Routes from .fusee/manifest.json
69
- var routes []*engine.Route
70
- var elements engine.ElementRegistry
71
- manifestPath := filepath.Join(absRoot, ".fusee", "manifest.json")
72
-
73
- if manifest, err := engine.LoadManifest(manifestPath); err == nil && len(manifest.Routes) > 0 {
74
- elements = manifest.ElementRegistry()
75
- if len(elements) > 0 {
76
- log.Printf("✨ [Fusee SSR] Loaded %d custom elements for declarative shadow DOM", len(elements))
77
- }
78
- log.Printf("✨ [Fusee SSR] Loaded %d dynamic routes from %s", len(manifest.Routes), manifestPath)
79
- for _, mr := range manifest.Routes {
80
- if mr.AST == nil {
81
- continue
82
- }
83
- routeAST := mr.AST
84
- routeTitle := mr.Title
85
- handler := &PageHandler{
86
- Module: engine.Module{
87
- Load: func(params map[string]string, query map[string][]string, r *http.Request) (any, error) {
88
- return map[string]any{
89
- "title": routeTitle,
90
- "status": "Pre-rendered dynamically by Go SSR Engine",
91
- "count": 42,
92
- "items": []any{"Dynamic Manifest", "Zero Go Code", "Rust AST Parser", "Selective Hydration"},
93
- }, nil
94
- },
95
- },
96
- AST: routeAST,
97
- Title: routeTitle,
98
- }
99
- routes = append(routes, engine.Compile(mr.Pattern, handler))
100
- }
101
- } else {
102
- log.Printf("ℹ️ [Fusee SSR] No manifest found at %s. Using default fallback routes.", manifestPath)
103
- routes = append(routes, engine.Compile("/", &engine.Module{
104
- Load: func(params map[string]string, query map[string][]string, r *http.Request) (any, error) {
105
- return map[string]any{
106
- "title": "Welcome to Fusee SSR",
107
- "status": "SSR Active",
108
- "count": 10,
109
- "items": []any{"High Performance Signals", "Go SSR Engine", "Rust AST Parser", "Selective Hydration"},
110
- "version": "2.0.0",
111
- }, nil
112
- },
113
- }))
114
- }
115
-
116
- // 4. Configure Dynamic Render Function
117
- renderFunc := func(w http.ResponseWriter, r *http.Request, route *engine.Route, params map[string]string, data any) {
118
- scope := evalexpr.Scope{}
119
- if m, ok := data.(map[string]any); ok {
120
- for k, v := range m {
121
- scope[k] = v
122
- }
123
- }
124
- for k, v := range params {
125
- scope[k] = v
126
- }
127
-
128
- var bodyHTML string
129
- var err error
130
-
131
- if pageH, ok := route.Handler.(*PageHandler); ok && pageH.AST != nil {
132
- bodyHTML, err = engine.RenderWithElements(pageH.AST, scope, nil, elements)
133
- } else {
134
- bodyHTML = fmt.Sprintf(`<div class="page"><h1>%s</h1><p>%s</p></div>`, scope["title"], scope["status"])
135
- }
136
-
137
- if err != nil {
138
- http.Error(w, fmt.Sprintf("Render Error: %v", err), http.StatusInternalServerError)
139
- return
140
- }
141
-
142
- signals := map[string][]any{}
143
- if c, ok := scope["count"]; ok {
144
- signals["count"] = []any{c}
145
- }
146
- if s, ok := scope["status"]; ok {
147
- signals["status"] = []any{s}
148
- }
149
-
150
- hydrationScript, _ := engine.RenderDehydrationScript(nil, signals)
151
-
152
- pageTitle := "Fusee SSR App"
153
- if t, ok := scope["title"].(string); ok {
154
- pageTitle = t
155
- }
156
-
157
- fullHTML := fmt.Sprintf(`<!DOCTYPE html>
158
- <html lang="en">
159
- <head>
160
- <meta charset="UTF-8" />
161
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
162
- <title>%s</title>
163
- <link rel="icon" href="/fusee-logo.svg" type="image/svg+xml" />
164
- <style>
165
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
166
- body { font-family: system-ui, sans-serif; background: #0f0f13; color: #e8e8f0; min-height: 100vh; }
167
- nav { display: flex; flex-wrap: wrap; gap: 1rem; padding: 1rem 2rem; background: #1a1a24; border-bottom: 1px solid #2a2a3a; }
168
- nav a { color: #ff3333; text-decoration: none; font-weight: 500; }
169
- .content-wrap { padding: 2rem; max-width: 800px; margin: 0 auto; width: 100%%; }
170
- .demo-card { background: #13131d; border: 1px solid #2a2a3a; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
171
- .primary-btn { padding: 0.6rem 1.25rem; border: none; background: #3b82f6; color: #fff; font-weight: 600; border-radius: 6px; cursor: pointer; }
172
- .cmp-btn { padding: 0.45rem 1.1rem; border-radius: 6px; border: 1px solid #2a2a3a; background: #0f0f1a; color: #e8e8f0; cursor: pointer; }
173
- .cmp-input { width: 100%%; padding: 0.5rem 0.75rem; background: #0f0f1a; border: 1px solid #2a2a3a; border-radius: 6px; color: #e8e8f0; }
174
- .badge { display: inline-block; padding: 0.2rem 0.65rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
175
- .badge-blue { background: rgba(96, 165, 250, 0.15); color: #60a5fa; }
176
- .badge-red { background: rgba(255, 51, 51, 0.12); color: #ff3333; }
177
- .info-section { background: #0f0f1a; border: 1px solid #2a2a3a; border-radius: 12px; padding: 1.5rem; margin-top: 2rem; }
178
- .info-section h2 { color: #34d399; margin-bottom: 1rem; font-size: 1.1rem; }
179
- .info-section ul { margin-left: 1.5rem; }
180
- .info-section li { color: #aaa; margin-bottom: 0.5rem; }
181
- </style>
182
- </head>
183
- <body>
184
- <div id="app">
185
- <div class="root-layout">
186
- <nav>
187
- <a href="/" f-link>Home</a>
188
- <a href="/about" f-link>About</a>
189
- <a href="/ssr-demo" f-link style="color: #60a5fa;">🚀 SSR Demo</a>
190
- <a href="/users" f-link style="color: #f59e0b;">👥 Users</a>
191
- </nav>
192
- <main class="content-wrap">
193
- <div data-router-view>%s</div>
194
- </main>
195
- </div>
196
- </div>
197
- %s
198
- <script type="module" src="/app/main.js"></script>
199
- </body>
200
- </html>`, pageTitle, bodyHTML, hydrationScript)
201
-
202
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
203
- w.WriteHeader(http.StatusOK)
204
- w.Write([]byte(fullHTML))
205
- }
206
-
207
- // 5. Configure Static Assets Directory
208
- absDist := *distDir
209
- if absDist == "./dist" || absDist == "dist" {
210
- absDist = filepath.Join(absRoot, "dist")
211
- if _, err := os.Stat(absDist); os.IsNotExist(err) {
212
- absDist = absRoot
213
- }
214
- } else {
215
- absDist, _ = filepath.Abs(absDist)
216
- }
217
- log.Printf("[Static Assets] Serving files from: %s", absDist)
218
-
219
- // 6. Create Dispatcher & Listen
220
- dispatcher := engine.NewDispatcher(engine.Config{
221
- DistDir: absDist,
222
- Routes: routes,
223
- Actions: actionReg,
224
- Render: renderFunc,
225
- ActionsBasePath: "/__fusee/actions",
226
- })
227
-
228
- addr := fmt.Sprintf(":%d", *port)
229
- log.Printf("Fusee Go Server listening on http://localhost:%d", *port)
230
- if err := http.ListenAndServe(addr, dispatcher); err != nil {
231
- log.Fatalf("Server error: %v", err)
232
- }
233
- }
@@ -1,41 +0,0 @@
1
- package comet
2
-
3
- import "net/http"
4
-
5
- const (
6
- headerRequest = "Comet-Request"
7
- headerTrigger = "Comet-Trigger"
8
- headerTarget = "Comet-Target"
9
- headerRetarget = "Comet-Retarget"
10
- headerReswap = "Comet-Reswap"
11
- headerPushURL = "Comet-Push-Url"
12
- headerRedirect = "Comet-Redirect"
13
- )
14
-
15
- func IsComet(r *http.Request) bool {
16
- return r.Header.Get(headerRequest) == "true"
17
- }
18
-
19
- func GetCometTarget(r *http.Request) string {
20
- return r.Header.Get(headerTarget)
21
- }
22
-
23
- func GetCometTrigger(r *http.Request) string {
24
- return r.Header.Get(headerTrigger)
25
- }
26
-
27
- func Retarget(w http.ResponseWriter, selector string) {
28
- w.Header().Set(headerRetarget, selector)
29
- }
30
-
31
- func Reswap(w http.ResponseWriter, mode string) {
32
- w.Header().Set(headerReswap, mode)
33
- }
34
-
35
- func PushURL(w http.ResponseWriter, url string) {
36
- w.Header().Set(headerPushURL, url)
37
- }
38
-
39
- func Redirect(w http.ResponseWriter, url string) {
40
- w.Header().Set(headerRedirect, url)
41
- }
@@ -1,51 +0,0 @@
1
- package comet
2
-
3
- import (
4
- "context"
5
- "net/http"
6
-
7
- engine "fusee"
8
- )
9
-
10
- // FragmentAware wraps a fragment renderer and a full page renderer, dispatching to
11
- // the fragment renderer when Comet-Request header is detected, or fullPage otherwise.
12
- func FragmentAware(fragment, fullPage engine.RenderFunc) engine.RenderFunc {
13
- return func(w http.ResponseWriter, r *http.Request, route *engine.Route, params map[string]string, data any) {
14
- if IsComet(r) {
15
- fragment(w, r, route, params, data)
16
- return
17
- }
18
- fullPage(w, r, route, params, data)
19
- }
20
- }
21
-
22
- // RenderFragment writes a standalone HTML fragment response with text/html content type.
23
- func RenderFragment(w http.ResponseWriter, html string) {
24
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
25
- w.WriteHeader(http.StatusOK)
26
- _, _ = w.Write([]byte(html))
27
- }
28
-
29
- // RenderFragmentWithState writes an HTML fragment accompanied by an embedded Fusée hydration state script.
30
- func RenderFragmentWithState(w http.ResponseWriter, html string, resources map[string]map[string]any, signals map[string][]any) error {
31
- script, err := engine.RenderDehydrationScript(resources, signals)
32
- if err != nil {
33
- return err
34
- }
35
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
36
- w.WriteHeader(http.StatusOK)
37
- _, err = w.Write([]byte(html + "\n" + script))
38
- return err
39
- }
40
-
41
- // RenderFragmentWithContext writes an HTML fragment with hydration state extracted from the request Context.
42
- func RenderFragmentWithContext(w http.ResponseWriter, ctx context.Context, html string) error {
43
- script, err := engine.BuildFromContext(ctx)
44
- if err != nil {
45
- return err
46
- }
47
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
48
- w.WriteHeader(http.StatusOK)
49
- _, err = w.Write([]byte(html + "\n" + script))
50
- return err
51
- }
@@ -1,120 +0,0 @@
1
- package engine
2
-
3
- import (
4
- "encoding/json"
5
- "errors"
6
- "log"
7
- "net/http"
8
- "strings"
9
- )
10
-
11
- const defaultActionsBasePath = "/__fusee/actions"
12
-
13
- type LoadFunc func(params map[string]string, query map[string][]string, r *http.Request) (any, error)
14
-
15
- type RenderFunc func(w http.ResponseWriter, r *http.Request, route *Route, params map[string]string, data any)
16
-
17
- type Module struct {
18
- Load LoadFunc
19
- }
20
-
21
- // AsModule returns the module itself. Route handlers that embed Module (for example a
22
- // page handler that also carries its AST) inherit it, so the dispatcher still finds Load.
23
- func (m *Module) AsModule() *Module { return m }
24
-
25
- type moduleHandler interface {
26
- AsModule() *Module
27
- }
28
-
29
- type Config struct {
30
- DistDir string
31
- Routes []*Route
32
- Actions *ActionRegistry
33
- Render RenderFunc
34
- ActionsBasePath string
35
- }
36
-
37
- type Dispatcher struct {
38
- cfg Config
39
- }
40
-
41
- func NewDispatcher(cfg Config) *Dispatcher {
42
- if cfg.ActionsBasePath == "" {
43
- cfg.ActionsBasePath = defaultActionsBasePath
44
- }
45
- return &Dispatcher{cfg: cfg}
46
- }
47
-
48
- func (d *Dispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
49
- // 1. Static asset bypass
50
- if d.cfg.DistDir != "" && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
51
- if path, ok := Resolve(d.cfg.DistDir, r.URL.Path); ok {
52
- log.Printf("[HTTP 200] %s %s -> static: %s", r.Method, r.URL.Path, path)
53
- if err := Serve(w, r, path); err != nil {
54
- log.Printf("[HTTP 500] %s %s error serving static: %v", r.Method, r.URL.Path, err)
55
- http.Error(w, "Internal Server Error", http.StatusInternalServerError)
56
- }
57
- return
58
- }
59
- }
60
-
61
- // 2. Server Actions interceptor
62
- if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, d.cfg.ActionsBasePath) {
63
- if d.cfg.Actions == nil {
64
- log.Printf("[HTTP 404] %s %s (No ActionRegistry)", r.Method, r.URL.Path)
65
- http.Error(w, "Not Found", http.StatusNotFound)
66
- return
67
- }
68
- name := strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, d.cfg.ActionsBasePath), "/")
69
- log.Printf("[HTTP Action] %s %s -> action: %s", r.Method, r.URL.Path, name)
70
- d.cfg.Actions.HandleRequest(w, r, name)
71
- return
72
- }
73
-
74
- match := MatchAll(d.cfg.Routes, r.URL.Path)
75
- if match == nil {
76
- log.Printf("[HTTP 404] %s %s (No route or static file matched)", r.Method, r.URL.Path)
77
- http.Error(w, "Not Found", http.StatusNotFound)
78
- return
79
- }
80
-
81
- log.Printf("[HTTP SSR] %s %s -> route: %s params: %+v", r.Method, r.URL.Path, match.Route.Pattern, match.Params)
82
-
83
- var data any
84
- if h, ok := match.Route.Handler.(moduleHandler); ok && h.AsModule() != nil && h.AsModule().Load != nil {
85
- module := h.AsModule()
86
- var err error
87
- data, err = module.Load(match.Params, r.URL.Query(), r)
88
- if err != nil {
89
- log.Printf("[HTTP LoadError] %s %s: %v", r.Method, r.URL.Path, err)
90
- d.handleLoadError(w, r, match.Route, err)
91
- return
92
- }
93
- }
94
-
95
- if d.cfg.Render == nil {
96
- w.Header().Set("Content-Type", "application/json")
97
- w.WriteHeader(http.StatusOK)
98
- json.NewEncoder(w).Encode(map[string]any{"params": match.Params, "data": data})
99
- return
100
- }
101
-
102
- d.cfg.Render(w, r, match.Route, match.Params, data)
103
- }
104
-
105
- func (d *Dispatcher) handleLoadError(w http.ResponseWriter, r *http.Request, route *Route, err error) {
106
- var redirect *RouteRedirect
107
- if errors.As(err, &redirect) {
108
- w.Header().Set("Location", redirect.Location)
109
- w.WriteHeader(redirect.Status)
110
- return
111
- }
112
-
113
- var httpErr *RouteHTTPError
114
- if errors.As(err, &httpErr) {
115
- http.Error(w, httpErr.Message, httpErr.Status)
116
- return
117
- }
118
-
119
- http.Error(w, "Internal Server Error", http.StatusInternalServerError)
120
- }
@@ -1,248 +0,0 @@
1
- package engine
2
-
3
- import (
4
- "encoding/json"
5
- "fmt"
6
- "strconv"
7
- "strings"
8
-
9
- "fusee/evalexpr"
10
- "fusee/rustast"
11
- )
12
-
13
- // maxElementDepth bounds element-in-element nesting so a self-rendering element fails
14
- // instead of recursing forever.
15
- const maxElementDepth = 50
16
-
17
- // ElementProp mirrors one prop declared with defineElement().
18
- type ElementProp struct {
19
- Name string `json:"name"`
20
- Attribute string `json:"attribute"` // "" for property-only props (attribute: false)
21
- Type string `json:"type"` // "String", "Number", "Boolean", "Array", "Object" or ""
22
- Default any `json:"default"`
23
- Reflect bool `json:"reflect"`
24
- }
25
-
26
- // ElementShadow mirrors the shadow root options of an element.
27
- type ElementShadow struct {
28
- Mode string `json:"mode"`
29
- DelegatesFocus bool `json:"delegatesFocus"`
30
- }
31
-
32
- // Element is a custom element registered with defineElement(), rendered server-side as
33
- // declarative shadow DOM. Setup is optional and plays the role of the JS setup():
34
- // it receives the resolved props and returns the template scope. Without it, the
35
- // template sees each prop by name and the whole set as `props`.
36
- type Element struct {
37
- Tag string `json:"tag"`
38
- Props []ElementProp `json:"props"`
39
- Styles string `json:"styles"`
40
- Shadow ElementShadow `json:"shadow"`
41
- AST *rustast.Node `json:"ast"`
42
- Setup func(props evalexpr.Scope) (evalexpr.Scope, error) `json:"-"`
43
- }
44
-
45
- // ElementRegistry maps lowercase tag names to element definitions.
46
- type ElementRegistry map[string]*Element
47
-
48
- // NewElementRegistry indexes elements by lowercase tag name, skipping entries without an AST.
49
- func NewElementRegistry(elements []*Element) ElementRegistry {
50
- reg := ElementRegistry{}
51
- for _, el := range elements {
52
- if el == nil || el.AST == nil || el.Tag == "" {
53
- continue
54
- }
55
- reg[strings.ToLower(el.Tag)] = el
56
- }
57
- return reg
58
- }
59
-
60
- func (reg ElementRegistry) lookup(tag string) *Element {
61
- if reg == nil || !strings.Contains(tag, "-") {
62
- return nil
63
- }
64
- return reg[strings.ToLower(tag)]
65
- }
66
-
67
- func (p ElementProp) matches(name string) bool {
68
- return strings.EqualFold(name, p.Name) || (p.Attribute != "" && strings.EqualFold(name, p.Attribute))
69
- }
70
-
71
- // propFromAttribute converts a static attribute value (nil when present without a value)
72
- // to the prop's type, mirroring the client converters. ok is false when the value cannot
73
- // be converted, in which case the default applies.
74
- func (p ElementProp) propFromAttribute(value *string) (any, bool) {
75
- if p.Type == "Boolean" {
76
- return true, true
77
- }
78
- if value == nil {
79
- return nil, false
80
- }
81
- switch p.Type {
82
- case "Number":
83
- if *value == "" {
84
- return nil, false
85
- }
86
- f, err := strconv.ParseFloat(*value, 64)
87
- if err != nil {
88
- return nil, false
89
- }
90
- return f, true
91
- case "Array", "Object":
92
- var v any
93
- if err := json.Unmarshal([]byte(*value), &v); err != nil {
94
- return nil, false
95
- }
96
- return v, true
97
- default:
98
- return *value, true
99
- }
100
- }
101
-
102
- // propToAttribute converts a prop value to its attribute form; ok is false when the
103
- // attribute should be absent.
104
- func (p ElementProp) propToAttribute(value any) (string, bool) {
105
- if p.Type == "Boolean" {
106
- return "", truthyForClass(value)
107
- }
108
- if value == nil {
109
- return "", false
110
- }
111
- if p.Type == "Array" || p.Type == "Object" {
112
- raw, err := json.Marshal(value)
113
- if err != nil {
114
- return "", false
115
- }
116
- return string(raw), true
117
- }
118
- return displayString(value), true
119
- }
120
-
121
- // resolveElementProps builds the props scope from the element's static attributes and
122
- // bindings. It also returns the reflected attributes that must be added to the tag
123
- // because nothing in the template set them.
124
- func resolveElementProps(el *Element, props []*rustast.Prop, scope evalexpr.Scope) (evalexpr.Scope, [][2]string, error) {
125
- resolved := evalexpr.Scope{}
126
- var reflected [][2]string
127
-
128
- for _, def := range el.Props {
129
- value, found := def.Default, false
130
-
131
- for _, p := range props {
132
- switch p.Type {
133
- case rustast.PropAttribute:
134
- if def.Attribute == "" || !strings.EqualFold(p.Attribute.Name, def.Attribute) {
135
- continue
136
- }
137
- found = true
138
- if v, ok := def.propFromAttribute(p.Attribute.Value); ok {
139
- value = v
140
- }
141
- case rustast.PropBinding:
142
- if !def.matches(p.Binding.Name) {
143
- continue
144
- }
145
- found = true
146
- v, err := evalexpr.Eval(p.Binding.Expression.Content, scope)
147
- if err != nil {
148
- return nil, nil, err
149
- }
150
- if v != nil {
151
- value = v
152
- }
153
- }
154
- }
155
-
156
- resolved[def.Name] = value
157
- if def.Reflect && !found {
158
- if attr, ok := def.propToAttribute(value); ok {
159
- reflected = append(reflected, [2]string{def.Attribute, attr})
160
- }
161
- }
162
- }
163
-
164
- return resolved, reflected, nil
165
- }
166
-
167
- // elementScope runs the element's Go Setup, or exposes the props directly to the template.
168
- func elementScope(el *Element, props evalexpr.Scope) (evalexpr.Scope, error) {
169
- if el.Setup != nil {
170
- return el.Setup(props)
171
- }
172
- scope := cloneScope(props)
173
- scope["props"] = map[string]any(props)
174
- return scope, nil
175
- }
176
-
177
- // slotOutlets makes every slot outlet in a shadow template render as a native <slot>.
178
- func slotOutlets(ast *rustast.Node) map[string][]*rustast.Node {
179
- outlets := map[string][]*rustast.Node{}
180
- var walk func(nodes []*rustast.Node)
181
- walk = func(nodes []*rustast.Node) {
182
- for _, n := range nodes {
183
- switch n.Type {
184
- case rustast.NodeSlotOutlet:
185
- name := n.SlotOutlet.SlotName
186
- tag := "<slot></slot>"
187
- if name != "default" {
188
- tag = fmt.Sprintf(`<slot name="%s"></slot>`, escapeAttr(name))
189
- }
190
- outlets[name] = []*rustast.Node{{Type: rustast.NodeText, Text: &rustast.TextNode{Content: tag}}}
191
- walk(n.SlotOutlet.Fallback)
192
- case rustast.NodeElement:
193
- walk(n.Element.Children)
194
- case rustast.NodeComponent:
195
- for _, s := range n.Component.Slots {
196
- walk(s.Children)
197
- }
198
- case rustast.NodeIf:
199
- for _, b := range n.If.Branches {
200
- walk([]*rustast.Node{b.Node})
201
- }
202
- }
203
- }
204
- }
205
- walk(ast.Root.Children)
206
- return outlets
207
- }
208
-
209
- // renderShadowRoot writes `<template shadowrootmode>` with the element's styles and its
210
- // template rendered against the resolved props.
211
- func (r *Renderer) renderShadowRoot(el *Element, props evalexpr.Scope, sb *strings.Builder) error {
212
- if r.depth >= maxElementDepth {
213
- return fmt.Errorf("render: elements nested deeper than %d levels at <%s> (recursive element?)", maxElementDepth, el.Tag)
214
- }
215
-
216
- scope, err := elementScope(el, props)
217
- if err != nil {
218
- return fmt.Errorf("render: setup of <%s> failed: %w", el.Tag, err)
219
- }
220
- scope = cloneScope(scope)
221
- scope["_slots"] = slotOutlets(el.AST)
222
-
223
- child := NewRenderer(r.Components)
224
- child.Elements = r.Elements
225
- child.depth = r.depth + 1
226
- html, err := child.renderRoot(el.AST, scope)
227
- if err != nil {
228
- return err
229
- }
230
-
231
- mode := "open"
232
- if el.Shadow.Mode == "closed" {
233
- mode = "closed"
234
- }
235
- fmt.Fprintf(sb, `<template shadowrootmode="%s"`, mode)
236
- if el.Shadow.DelegatesFocus {
237
- sb.WriteString(" shadowrootdelegatesfocus")
238
- }
239
- sb.WriteByte('>')
240
- if el.Styles != "" {
241
- sb.WriteString("<style>")
242
- sb.WriteString(strings.ReplaceAll(el.Styles, "</style", `<\/style`))
243
- sb.WriteString("</style>")
244
- }
245
- sb.WriteString(html)
246
- sb.WriteString("</template>")
247
- return nil
248
- }