Files
nebula/internal/generator/generator.go

645 lines
17 KiB
Go

// 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 {
<div slot="{{ .Name }}">
@props.Slots.{{ toSlotName .Name }}
</div>
}
{{- end }}
{ children... }
</{{ $el.Name }}>
}
// {{ $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() {
<link rel="stylesheet" href={ "https://cdn.jsdelivr.net/npm/@aspect/web-awesome@" + CDNVersion + "/dist/themes/default.css" }/>
<script type="module" src={ "https://cdn.jsdelivr.net/npm/@aspect/web-awesome@" + CDNVersion + "/dist/web-awesome.loader.js" }></script>
}
// CDNHeadPro renders the required CSS and JS for Web Awesome Pro from CDN
// Requires a valid license key configured in your environment
templ CDNHeadPro() {
<link rel="stylesheet" href={ "https://cdn.jsdelivr.net/npm/@awesome.me/webawesome-pro@" + CDNVersion + "/dist/themes/default.css" }/>
<script type="module" src={ "https://cdn.jsdelivr.net/npm/@awesome.me/webawesome-pro@" + CDNVersion + "/dist/webawesome.loader.js" }></script>
}
// CDNHeadWithTheme renders CDN assets with a specific theme
templ CDNHeadWithTheme(theme string) {
<link rel="stylesheet" href={ "https://cdn.jsdelivr.net/npm/@aspect/web-awesome@" + CDNVersion + "/dist/themes/" + theme + ".css" }/>
<script type="module" src={ "https://cdn.jsdelivr.net/npm/@aspect/web-awesome@" + CDNVersion + "/dist/web-awesome.loader.js" }></script>
}
// LocalHead renders Web Awesome assets from local paths
templ LocalHead(cssPath, jsPath string) {
<link rel="stylesheet" href={ cssPath }/>
<script type="module" src={ jsPath }></script>
}
// FontAwesomeKit renders Font Awesome kit script
templ FontAwesomeKit(kitCode string) {
<script src={ "https://kit.fontawesome.com/" + kitCode + ".js" } crossorigin="anonymous"></script>
}
`
// 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)
}