feat: Add generator function for WebAwesome
This commit is contained in:
@@ -15,5 +15,5 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/prad/nebula"
|
"github.com/sonr-io/nebula"
|
||||||
)
|
)
|
||||||
|
|||||||
79
cmd/generate/main.go
Normal file
79
cmd/generate/main.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sonr-io/nebula/internal/generator"
|
||||||
|
"github.com/sonr-io/nebula/internal/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
input = flag.String("input", "web-types.json", "Path to web-types.json")
|
||||||
|
output = flag.String("output", "pkg/wa", "Output directory for generated files")
|
||||||
|
pkg = flag.String("package", "wa", "Package name for generated files")
|
||||||
|
verbose = flag.Bool("verbose", false, "Verbose output")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(*input)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wt parser.WebTypes
|
||||||
|
if err := json.Unmarshal(data, &wt); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(*output, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gen := generator.New(*pkg, *verbose)
|
||||||
|
|
||||||
|
// Generate component files
|
||||||
|
for _, el := range wt.Contributions.HTML.Elements {
|
||||||
|
filename := filepath.Join(*output, gen.ComponentFilename(el.Name))
|
||||||
|
if err := gen.GenerateComponent(filename, el); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating %s: %v\n", el.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *verbose {
|
||||||
|
fmt.Printf("Generated: %s\n", filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate shared types and utilities
|
||||||
|
if err := gen.GenerateTypes(filepath.Join(*output, "types.go")); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating types: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate builder helpers
|
||||||
|
if err := gen.GenerateBuilders(filepath.Join(*output, "builders.go"), wt.Contributions.HTML.Elements); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating builders: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate CDN loader
|
||||||
|
if err := gen.GenerateCDN(filepath.Join(*output, "cdn.templ"), wt.Version); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating CDN loader: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate events helper
|
||||||
|
if err := gen.GenerateEvents(filepath.Join(*output, "events.go"), wt.Contributions.HTML.Elements); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating events: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Generated %d components in %s\n", len(wt.Contributions.HTML.Elements), *output)
|
||||||
|
}
|
||||||
7020
config.json
Normal file
7020
config.json
Normal file
File diff suppressed because one or more lines are too long
644
internal/generator/generator.go
Normal file
644
internal/generator/generator.go
Normal file
@@ -0,0 +1,644 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
137
internal/generator/strings.go
Normal file
137
internal/generator/strings.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kebabRegex = regexp.MustCompile(`[-_]`)
|
||||||
|
mdLinkRegex = regexp.MustCompile(`\[([^\]]+)\]\([^)]+\)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToPascalCase converts kebab-case or snake_case to PascalCase
|
||||||
|
func ToPascalCase(s string) string {
|
||||||
|
parts := kebabRegex.Split(s, -1)
|
||||||
|
for i, p := range parts {
|
||||||
|
if len(p) > 0 {
|
||||||
|
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToCamelCase converts kebab-case or snake_case to camelCase
|
||||||
|
func ToCamelCase(s string) string {
|
||||||
|
p := ToPascalCase(s)
|
||||||
|
if len(p) > 0 {
|
||||||
|
return strings.ToLower(p[:1]) + p[1:]
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripWaPrefix removes the "wa-" prefix from component names
|
||||||
|
func StripWaPrefix(s string) string {
|
||||||
|
return strings.TrimPrefix(s, "wa-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGoType converts TypeScript type to Go type
|
||||||
|
func ToGoType(tsType string) string {
|
||||||
|
tsType = strings.TrimSpace(tsType)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tsType == "boolean":
|
||||||
|
return "bool"
|
||||||
|
case tsType == "number":
|
||||||
|
return "float64"
|
||||||
|
case tsType == "string":
|
||||||
|
return "string"
|
||||||
|
case tsType == "string | undefined", tsType == "string | null":
|
||||||
|
return "string"
|
||||||
|
case tsType == "number | undefined", tsType == "number | null":
|
||||||
|
return "float64"
|
||||||
|
case tsType == "boolean | undefined", tsType == "boolean | null":
|
||||||
|
return "bool"
|
||||||
|
case strings.HasPrefix(tsType, "'") && strings.Contains(tsType, "|"):
|
||||||
|
// Union of string literals -> string
|
||||||
|
return "string"
|
||||||
|
case strings.Contains(tsType, "|"):
|
||||||
|
// Complex union, default to string
|
||||||
|
return "string"
|
||||||
|
default:
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBuilderType returns the type used in builder methods (always non-pointer for fluent API)
|
||||||
|
func ToBuilderType(tsType string) string {
|
||||||
|
return ToGoType(tsType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBoolean checks if the TypeScript type is boolean
|
||||||
|
func IsBoolean(tsType string) bool {
|
||||||
|
return strings.Contains(tsType, "boolean")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNumber checks if the TypeScript type is numeric
|
||||||
|
func IsNumber(tsType string) bool {
|
||||||
|
return tsType == "number" || strings.HasPrefix(tsType, "number")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanDescription cleans markdown and truncates description
|
||||||
|
func CleanDescription(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Take first line
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
first := strings.TrimSpace(lines[0])
|
||||||
|
// Strip markdown links
|
||||||
|
first = mdLinkRegex.ReplaceAllString(first, "$1")
|
||||||
|
// Remove backticks
|
||||||
|
first = strings.ReplaceAll(first, "`", "")
|
||||||
|
// Truncate
|
||||||
|
if len(first) > 120 {
|
||||||
|
first = first[:117] + "..."
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractEnumValues extracts string literal values from a union type
|
||||||
|
func ExtractEnumValues(tsType string) []string {
|
||||||
|
if !strings.Contains(tsType, "'") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`'([^']+)'`)
|
||||||
|
matches := re.FindAllStringSubmatch(tsType, -1)
|
||||||
|
values := make([]string, 0, len(matches))
|
||||||
|
for _, m := range matches {
|
||||||
|
if len(m) > 1 {
|
||||||
|
values = append(values, m[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToEventName converts wa-event-name to OnEventName
|
||||||
|
func ToEventName(s string) string {
|
||||||
|
s = strings.TrimPrefix(s, "wa-")
|
||||||
|
return "On" + ToPascalCase(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSlotName converts slot name to Go-friendly name
|
||||||
|
func ToSlotName(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return "Default"
|
||||||
|
}
|
||||||
|
return ToPascalCase(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeGoString escapes a string for use in Go source code
|
||||||
|
func EscapeGoString(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||||
|
s = strings.ReplaceAll(s, "\"", "\\\"")
|
||||||
|
s = strings.ReplaceAll(s, "\n", "\\n")
|
||||||
|
s = strings.ReplaceAll(s, "\t", "\\t")
|
||||||
|
return s
|
||||||
|
}
|
||||||
65
internal/parser/types.go
Normal file
65
internal/parser/types.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Package parser contains the code parser for the WebAwesome library.
|
||||||
|
package parser
|
||||||
|
|
||||||
|
// WebTypes represents the root structure of web-types.json
|
||||||
|
type WebTypes struct {
|
||||||
|
Schema string `json:"$schema"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
DescriptionMarkup string `json:"description-markup"`
|
||||||
|
Contributions struct {
|
||||||
|
HTML struct {
|
||||||
|
Elements []Element `json:"elements"`
|
||||||
|
} `json:"html"`
|
||||||
|
} `json:"contributions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element represents a web component definition
|
||||||
|
type Element struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DocURL string `json:"doc-url"`
|
||||||
|
Attributes []Attribute `json:"attributes"`
|
||||||
|
Slots []Slot `json:"slots"`
|
||||||
|
Events []Event `json:"events"`
|
||||||
|
JS JSInfo `json:"js"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute represents a component attribute/property
|
||||||
|
type Attribute struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Value AttributeValue `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttributeValue contains type and default value info
|
||||||
|
type AttributeValue struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Default string `json:"default,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot represents a named slot in the component
|
||||||
|
type Slot struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event represents an event emitted by the component
|
||||||
|
type Event struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSInfo contains JavaScript-specific property and event info
|
||||||
|
type JSInfo struct {
|
||||||
|
Properties []JSProperty `json:"properties"`
|
||||||
|
Events []Event `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSProperty represents a JavaScript property
|
||||||
|
type JSProperty struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user