// Package generator contains the code generator for the WebAwesome library. package generator import ( "bytes" "fmt" "os" "sort" "text/template" "github.com/sonr-io/nebula/internal/parser" ) // Generator handles code generation for Web Awesome components type Generator struct { pkg string verbose bool funcMap template.FuncMap } // New creates a new Generator instance func New(pkg string, verbose bool) *Generator { g := &Generator{ pkg: pkg, verbose: verbose, } g.funcMap = template.FuncMap{ "toPascal": ToPascalCase, "toCamel": ToCamelCase, "stripWa": StripWaPrefix, "toGoType": ToGoType, "toBuilderType": ToBuilderType, "isBoolean": IsBoolean, "isNumber": IsNumber, "cleanDesc": CleanDescription, "extractEnums": ExtractEnumValues, "toEventName": ToEventName, "toSlotName": ToSlotName, "escapeGoString": EscapeGoString, "hasSlots": g.hasSlots, "namedSlots": g.namedSlots, "hasEvents": g.hasEvents, "filterAttrs": g.filterAttributes, } return g } // ComponentFilename returns the filename for a component func (g *Generator) ComponentFilename(name string) string { return StripWaPrefix(name) + ".templ" } func (g *Generator) hasSlots(el parser.Element) bool { return len(el.Slots) > 0 } func (g *Generator) namedSlots(el parser.Element) []parser.Slot { var named []parser.Slot for _, s := range el.Slots { if s.Name != "" { named = append(named, s) } } return named } func (g *Generator) hasEvents(el parser.Element) bool { return len(el.Events) > 0 } func (g *Generator) filterAttributes(attrs []parser.Attribute) []parser.Attribute { var filtered []parser.Attribute skip := map[string]bool{"title": true} // Skip problematic attributes for _, a := range attrs { if !skip[a.Name] { filtered = append(filtered, a) } } return filtered } var componentTemplate = `// Code generated by wa-generator. DO NOT EDIT. // Source: Web Awesome {{ .Element.Name }} package {{ .Package }} import ( "github.com/a]h/templ" ) {{- $name := stripWa .Element.Name | toPascal }} {{- $el := .Element }} {{- $attrs := filterAttrs .Element.Attributes }} // {{ cleanDesc .Element.Description }} // // Web Awesome component: <{{ .Element.Name }}> {{- if .Element.DocURL }} // Documentation: {{ .Element.DocURL }} {{- end }} // {{ $name }}Props holds all properties for the {{ .Element.Name }} component type {{ $name }}Props struct { {{- range $attrs }} // {{ cleanDesc .Description }} {{- if extractEnums .Value.Type }} // Valid values: {{ range $i, $v := extractEnums .Value.Type }}{{ if $i }}, {{ end }}"{{ $v }}"{{ end }} {{- end }} {{ toPascal .Name }} {{ toGoType .Value.Type }} ` + "`attr:\"{{ .Name }}\"`" + ` {{- end }} // Events {{- range $el.Events }} // {{ cleanDesc .Description }} {{ toEventName .Name }} string ` + "`attr:\"x-on:{{ .Name }}\"`" + ` {{- end }} // Slots contains named slot content Slots {{ $name }}Slots // Attrs contains additional HTML attributes Attrs templ.Attributes } {{- if namedSlots $el }} // {{ $name }}Slots holds named slot content for the component type {{ $name }}Slots struct { {{- range namedSlots $el }} // {{ cleanDesc .Description }} {{ toSlotName .Name }} templ.Component {{- end }} } {{- end }} // {{ $name }}Builder provides a fluent API for constructing {{ $name }}Props type {{ $name }}Builder struct { props {{ $name }}Props } // New{{ $name }} creates a new builder for {{ .Element.Name }} func New{{ $name }}() *{{ $name }}Builder { return &{{ $name }}Builder{} } {{- range $attrs }} // {{ toPascal .Name }} sets the {{ .Name }} attribute {{- if cleanDesc .Description }} // {{ cleanDesc .Description }} {{- end }} func (b *{{ $name }}Builder) {{ toPascal .Name }}(v {{ toBuilderType .Value.Type }}) *{{ $name }}Builder { b.props.{{ toPascal .Name }} = v return b } {{- end }} {{- range $el.Events }} // {{ toEventName .Name }} sets the handler for {{ .Name }} event // {{ cleanDesc .Description }} func (b *{{ $name }}Builder) {{ toEventName .Name }}(handler string) *{{ $name }}Builder { b.props.{{ toEventName .Name }} = handler return b } {{- end }} {{- range namedSlots $el }} // {{ toSlotName .Name }}Slot sets the {{ .Name }} slot content // {{ cleanDesc .Description }} func (b *{{ $name }}Builder) {{ toSlotName .Name }}Slot(c templ.Component) *{{ $name }}Builder { b.props.Slots.{{ toSlotName .Name }} = c return b } {{- end }} // Attr adds a custom HTML attribute func (b *{{ $name }}Builder) Attr(name, value string) *{{ $name }}Builder { if b.props.Attrs == nil { b.props.Attrs = templ.Attributes{} } b.props.Attrs[name] = value return b } // Attrs merges multiple attributes func (b *{{ $name }}Builder) Attrs(attrs templ.Attributes) *{{ $name }}Builder { if b.props.Attrs == nil { b.props.Attrs = templ.Attributes{} } for k, v := range attrs { b.props.Attrs[k] = v } return b } // Props returns the built properties func (b *{{ $name }}Builder) Props() {{ $name }}Props { return b.props } // Build returns the props (alias for Props for semantic clarity) func (b *{{ $name }}Builder) Build() {{ $name }}Props { return b.props } // {{ $name }} renders the {{ .Element.Name }} component templ {{ $name }}(props {{ $name }}Props) { <{{ $el.Name }} {{- range $attrs }} {{- if isBoolean .Value.Type }} if props.{{ toPascal .Name }} { {{ .Name }} } {{- else if isNumber .Value.Type }} if props.{{ toPascal .Name }} != 0 { {{ .Name }}={ templ.Sprintf("%v", props.{{ toPascal .Name }}) } } {{- else }} if props.{{ toPascal .Name }} != "" { {{ .Name }}={ props.{{ toPascal .Name }} } } {{- end }} {{- end }} {{- range $el.Events }} if props.{{ toEventName .Name }} != "" { x-on:{{ .Name }}={ props.{{ toEventName .Name }} } } {{- end }} { props.Attrs... } > {{- range namedSlots $el }} if props.Slots.{{ toSlotName .Name }} != nil {
@props.Slots.{{ toSlotName .Name }}
} {{- end }} { children... } } // {{ $name }}Func renders with a builder function for inline configuration templ {{ $name }}Func(fn func(*{{ $name }}Builder)) { {{ "{{" }} b := New{{ $name }}(); fn(b) {{ "}}" }} @{{ $name }}(b.Props()) { { children... } } } ` // GenerateComponent generates a templ file for a component func (g *Generator) GenerateComponent(filename string, el parser.Element) error { tmpl, err := template.New("component").Funcs(g.funcMap).Parse(componentTemplate) if err != nil { return fmt.Errorf("parsing template: %w", err) } var buf bytes.Buffer data := map[string]interface{}{ "Package": g.pkg, "Element": el, } if err := tmpl.Execute(&buf, data); err != nil { return fmt.Errorf("executing template: %w", err) } return os.WriteFile(filename, buf.Bytes(), 0644) } var typesTemplate = `// Code generated by wa-generator. DO NOT EDIT. package {{ .Package }} import "github.com/a]h/templ" // Variant represents component theme variants type Variant string const ( VariantBrand Variant = "brand" VariantNeutral Variant = "neutral" VariantSuccess Variant = "success" VariantWarning Variant = "warning" VariantDanger Variant = "danger" ) // Size represents component sizes type Size string const ( SizeSmall Size = "small" SizeMedium Size = "medium" SizeLarge Size = "large" ) // Appearance represents visual appearance styles type Appearance string const ( AppearanceAccent Appearance = "accent" AppearanceFilled Appearance = "filled" AppearanceOutlined Appearance = "outlined" AppearanceFilledOutlined Appearance = "filled-outlined" AppearancePlain Appearance = "plain" ) // Orientation represents layout orientation type Orientation string const ( OrientationHorizontal Orientation = "horizontal" OrientationVertical Orientation = "vertical" ) // Placement represents popup/tooltip placement type Placement string const ( PlacementTop Placement = "top" PlacementTopStart Placement = "top-start" PlacementTopEnd Placement = "top-end" PlacementBottom Placement = "bottom" PlacementBottomStart Placement = "bottom-start" PlacementBottomEnd Placement = "bottom-end" PlacementLeft Placement = "left" PlacementLeftStart Placement = "left-start" PlacementLeftEnd Placement = "left-end" PlacementRight Placement = "right" PlacementRightStart Placement = "right-start" PlacementRightEnd Placement = "right-end" ) // SlotContent is a helper for creating slot content type SlotContent struct { Name string Content templ.Component } // NewSlot creates a new SlotContent func NewSlot(name string, content templ.Component) SlotContent { return SlotContent{Name: name, Content: content} } // EmptyComponent returns a no-op templ component func EmptyComponent() templ.Component { return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { return nil }) } // Text creates a simple text component func Text(s string) templ.Component { return templ.Raw(s) } ` // GenerateTypes generates shared types func (g *Generator) GenerateTypes(filename string) error { tmpl, err := template.New("types").Parse(typesTemplate) if err != nil { return err } var buf bytes.Buffer if err := tmpl.Execute(&buf, map[string]string{"Package": g.pkg}); err != nil { return err } return os.WriteFile(filename, buf.Bytes(), 0644) } var buildersTemplate = `// Code generated by wa-generator. DO NOT EDIT. package {{ .Package }} import "github.com/a]h/templ" // PropsBuilder is the interface all component builders implement type PropsBuilder[T any] interface { Attr(name, value string) PropsBuilder[T] Attrs(attrs templ.Attributes) PropsBuilder[T] Build() T } // BaseBuilder provides common builder functionality type BaseBuilder struct { attrs templ.Attributes } // SetAttr sets a single attribute func (b *BaseBuilder) SetAttr(name, value string) { if b.attrs == nil { b.attrs = templ.Attributes{} } b.attrs[name] = value } // MergeAttrs merges attributes func (b *BaseBuilder) MergeAttrs(attrs templ.Attributes) { if b.attrs == nil { b.attrs = templ.Attributes{} } for k, v := range attrs { b.attrs[k] = v } } // GetAttrs returns the attributes func (b *BaseBuilder) GetAttrs() templ.Attributes { if b.attrs == nil { return templ.Attributes{} } return b.attrs } // Common builder method helpers // WithClass is a helper to add CSS classes func WithClass[B interface{ Attr(string, string) B }](b B, classes ...string) B { return b.Attr("class", strings.Join(classes, " ")) } // WithID is a helper to set element ID func WithID[B interface{ Attr(string, string) B }](b B, id string) B { return b.Attr("id", id) } // WithStyle is a helper to add inline styles func WithStyle[B interface{ Attr(string, string) B }](b B, style string) B { return b.Attr("style", style) } // WithData is a helper to add data attributes func WithData[B interface{ Attr(string, string) B }](b B, key, value string) B { return b.Attr("data-"+key, value) } // Component registry for dynamic component creation var componentRegistry = map[string]func() interface{}{ {{- range .Elements }} "{{ .Name }}": func() interface{} { return New{{ stripWa .Name | toPascal }}() }, {{- end }} } // GetBuilder returns a builder for the given component name func GetBuilder(name string) interface{} { if fn, ok := componentRegistry[name]; ok { return fn() } return nil } // ComponentNames returns all available component names func ComponentNames() []string { names := make([]string, 0, len(componentRegistry)) for name := range componentRegistry { names = append(names, name) } sort.Strings(names) return names } ` // GenerateBuilders generates builder utilities func (g *Generator) GenerateBuilders(filename string, elements []parser.Element) error { tmpl, err := template.New("builders").Funcs(g.funcMap).Parse(buildersTemplate) if err != nil { return err } var buf bytes.Buffer data := map[string]interface{}{ "Package": g.pkg, "Elements": elements, } if err := tmpl.Execute(&buf, data); err != nil { return err } return os.WriteFile(filename, buf.Bytes(), 0644) } var cdnTemplate = `// Code generated by wa-generator. DO NOT EDIT. package {{ .Package }} // CDNVersion is the Web Awesome version used for CDN assets const CDNVersion = "{{ .Version }}" // CDNHead renders the required CSS and JS for Web Awesome from CDN templ CDNHead() { } // CDNHeadPro renders the required CSS and JS for Web Awesome Pro from CDN // Requires a valid license key configured in your environment templ CDNHeadPro() { } // CDNHeadWithTheme renders CDN assets with a specific theme templ CDNHeadWithTheme(theme string) { } // LocalHead renders Web Awesome assets from local paths templ LocalHead(cssPath, jsPath string) { } // FontAwesomeKit renders Font Awesome kit script templ FontAwesomeKit(kitCode string) { } ` // GenerateCDN generates CDN loader templates func (g *Generator) GenerateCDN(filename string, version string) error { tmpl, err := template.New("cdn").Parse(cdnTemplate) if err != nil { return err } var buf bytes.Buffer data := map[string]string{ "Package": g.pkg, "Version": version, } if err := tmpl.Execute(&buf, data); err != nil { return err } return os.WriteFile(filename, buf.Bytes(), 0644) } var eventsTemplate = `// Code generated by wa-generator. DO NOT EDIT. package {{ .Package }} // Event names for Web Awesome components // Use with Alpine.js x-on: directive or vanilla JS addEventListener const ( {{- range .Events }} // {{ .Name }} - {{ cleanDesc .Description }} Event{{ toPascal .Name }} = "{{ .Name }}" {{- end }} ) // EventHandler creates an Alpine.js event handler string func EventHandler(jsCode string) string { return jsCode } // EventHandlerPrevent creates a handler that prevents default func EventHandlerPrevent(jsCode string) string { return "$event.preventDefault(); " + jsCode } // EventHandlerStop creates a handler that stops propagation func EventHandlerStop(jsCode string) string { return "$event.stopPropagation(); " + jsCode } // EventHandlerDebounce wraps handler with debounce func EventHandlerDebounce(jsCode string, ms int) string { return fmt.Sprintf("$debounce(() => { %s }, %d)", jsCode, ms) } // Common event handler patterns // ToggleHandler returns a handler that toggles a boolean value func ToggleHandler(varName string) string { return varName + " = !" + varName } // SetValueHandler returns a handler that sets a value func SetValueHandler(varName, value string) string { return fmt.Sprintf("%s = %s", varName, value) } // DispatchHandler returns a handler that dispatches a custom event func DispatchHandler(eventName string, detail string) string { if detail != "" { return fmt.Sprintf("$dispatch('%s', %s)", eventName, detail) } return fmt.Sprintf("$dispatch('%s')", eventName) } // FetchHandler returns a handler that performs a fetch request func FetchHandler(url, method, body string) string { if body != "" { return fmt.Sprintf("fetch('%s', {method: '%s', body: JSON.stringify(%s), headers: {'Content-Type': 'application/json'}})", url, method, body) } return fmt.Sprintf("fetch('%s', {method: '%s'})", url, method) } ` // GenerateEvents generates event constants and helpers func (g *Generator) GenerateEvents(filename string, elements []parser.Element) error { // Collect all unique events eventMap := make(map[string]parser.Event) for _, el := range elements { for _, ev := range el.Events { if _, exists := eventMap[ev.Name]; !exists { eventMap[ev.Name] = ev } } } // Convert to slice and sort events := make([]parser.Event, 0, len(eventMap)) for _, ev := range eventMap { events = append(events, ev) } sort.Slice(events, func(i, j int) bool { return events[i].Name < events[j].Name }) tmpl, err := template.New("events").Funcs(g.funcMap).Parse(eventsTemplate) if err != nil { return err } var buf bytes.Buffer data := map[string]interface{}{ "Package": g.pkg, "Events": events, } if err := tmpl.Execute(&buf, data); err != nil { return err } return os.WriteFile(filename, buf.Bytes(), 0644) }