feat: Add generator function for WebAwesome
This commit is contained in:
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