645 lines
17 KiB
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)
|
|
}
|