package interpreter import ( "bytes" "fmt" "masonry/lang" "os" "path/filepath" "regexp" "strconv" "strings" "text/template" "golang.org/x/text/cases" "golang.org/x/text/language" "gopkg.in/yaml.v3" ) // TemplateManifest defines the structure for multi-file template generation type TemplateManifest struct { Name string `yaml:"name"` Description string `yaml:"description"` Outputs []OutputFile `yaml:"outputs"` } // OutputFile defines a single output file configuration type OutputFile struct { Path string `yaml:"path"` Template string `yaml:"template"` Condition string `yaml:"condition,omitempty"` Iterator string `yaml:"iterator,omitempty"` ItemContext string `yaml:"item_context,omitempty"` } // TemplateInterpreter converts Masonry AST using template files type TemplateInterpreter struct { registry *TemplateRegistry } // NewTemplateInterpreter creates a new template interpreter func NewTemplateInterpreter() *TemplateInterpreter { return &TemplateInterpreter{ registry: NewTemplateRegistry(), } } // InterpretFromFile parses a Masonry file and applies a template file func (ti *TemplateInterpreter) InterpretFromFile(masonryFile, templateFile string) (string, error) { // Read the Masonry file masonryInput, err := os.ReadFile(masonryFile) if err != nil { return "", fmt.Errorf("error reading Masonry file: %w", err) } // Read the template file tmplText, err := os.ReadFile(templateFile) if err != nil { return "", fmt.Errorf("error reading template file: %w", err) } return ti.Interpret(string(masonryInput), string(tmplText)) } // InterpretFromDirectory parses a Masonry file and applies templates from a directory func (ti *TemplateInterpreter) InterpretFromDirectory(masonryFile, templateDir, rootTemplate string) (string, error) { // Load all templates from the directory err := ti.registry.LoadFromDirectory(templateDir) if err != nil { return "", fmt.Errorf("error loading templates from directory: %w", err) } // Read the Masonry file masonryInput, err := os.ReadFile(masonryFile) if err != nil { return "", fmt.Errorf("error reading Masonry file: %w", err) } // Get the root template content rootTemplatePath := filepath.Join(templateDir, rootTemplate) tmplText, err := os.ReadFile(rootTemplatePath) if err != nil { return "", fmt.Errorf("error reading root template file: %w", err) } return ti.Interpret(string(masonryInput), string(tmplText)) } func (ti *TemplateInterpreter) Interpret(masonryInput string, tmplText string) (string, error) { ast, err := lang.ParseInput(masonryInput) if err != nil { return "", fmt.Errorf("error parsing Masonry input: %w", err) } // Create template using the unified FuncMap from the registry tmpl := template.Must(template.New("rootTemplate").Funcs(ti.registry.GetFuncMap()).Parse(tmplText)) data := struct { AST lang.AST Registry *TemplateRegistry }{ AST: ast, Registry: ti.registry, } var buf bytes.Buffer // Execute template err = tmpl.Execute(&buf, data) if err != nil { return "", fmt.Errorf("error executing template: %w", err) } return buf.String(), nil } // InterpretToFiles processes a manifest and returns multiple output files func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manifestFile string) (map[string]string, error) { // Load templates from directory err := ti.registry.LoadFromDirectory(templateDir) if err != nil { return nil, fmt.Errorf("error loading templates: %w", err) } // Load manifest manifestPath := filepath.Join(templateDir, manifestFile) manifestData, err := os.ReadFile(manifestPath) if err != nil { return nil, fmt.Errorf("error reading manifest: %w", err) } var manifest TemplateManifest err = yaml.Unmarshal(manifestData, &manifest) if err != nil { return nil, fmt.Errorf("error parsing manifest: %w", err) } // Parse Masonry input masonryInput, err := os.ReadFile(masonryFile) if err != nil { return nil, fmt.Errorf("error reading Masonry file: %w", err) } ast, err := lang.ParseInput(string(masonryInput)) if err != nil { return nil, fmt.Errorf("error parsing Masonry input: %w", err) } // Process each output file outputs := make(map[string]string) for _, output := range manifest.Outputs { if output.Iterator != "" { // Handle per-item generation files, err := ti.generatePerItem(output, ast) if err != nil { return nil, err } for path, content := range files { outputs[path] = content } } else { // Handle single file generation (existing logic) if output.Condition != "" && !ti.evaluateCondition(output.Condition, ast) { continue } content, err := ti.executeTemplate(output.Template, ast) if err != nil { return nil, fmt.Errorf("error executing template %s: %w", output.Template, err) } // Execute path template to get dynamic filename pathContent, err := ti.executePathTemplate(output.Path, map[string]interface{}{ "AST": ast, "Registry": ti.registry, }) if err != nil { return nil, fmt.Errorf("error executing path template: %w", err) } outputs[pathContent] = content } } return outputs, nil } // generatePerItem handles per-item iteration for multi-file generation func (ti *TemplateInterpreter) generatePerItem(output OutputFile, ast lang.AST) (map[string]string, error) { items := ti.getIteratorItems(output.Iterator, ast) results := make(map[string]string) for _, item := range items { // Check condition with item context if output.Condition != "" && !ti.evaluateItemCondition(output.Condition, item, ast) { continue } // Create template data with item context data := ti.createItemTemplateData(output.ItemContext, item, ast) // Execute path template to get dynamic filename pathContent, err := ti.executePathTemplate(output.Path, data) if err != nil { return nil, err } // Execute content template content, err := ti.executeTemplateWithData(output.Template, data) if err != nil { return nil, err } results[pathContent] = content } return results, nil } // getIteratorItems extracts items from the AST based on the iterator type func (ti *TemplateInterpreter) getIteratorItems(iterator string, ast lang.AST) []interface{} { var items []interface{} switch iterator { case "pages": for _, def := range ast.Definitions { if def.Page != nil { items = append(items, def.Page) } } case "entities": for _, def := range ast.Definitions { if def.Entity != nil { items = append(items, def.Entity) } } case "endpoints": for _, def := range ast.Definitions { if def.Endpoint != nil { items = append(items, def.Endpoint) } } case "servers": for _, def := range ast.Definitions { if def.Server != nil { items = append(items, def.Server) } } case "sections": items = ti.getAllSections(ast) case "components": items = ti.getAllComponents(ast) } return items } // getAllSections traverses the AST to collect all sections from all pages func (ti *TemplateInterpreter) getAllSections(ast lang.AST) []interface{} { var sections []interface{} for _, def := range ast.Definitions { if def.Page != nil { // Get sections directly in the page for i := range def.Page.Sections { sections = append(sections, &def.Page.Sections[i]) } // Recursively get nested sections sections = append(sections, ti.getSectionsFromSections(def.Page.Sections)...) } } return sections } // getSectionsFromSections recursively extracts sections from a list of sections func (ti *TemplateInterpreter) getSectionsFromSections(sections []lang.Section) []interface{} { var result []interface{} for i := range sections { for j := range sections[i].Elements { element := §ions[i].Elements[j] if element.Section != nil { result = append(result, element.Section) // Recursively get sections from this section result = append(result, ti.getSectionsFromSections([]lang.Section{*element.Section})...) } } } return result } // getAllComponents traverses the AST to collect all components from pages, sections, and nested components func (ti *TemplateInterpreter) getAllComponents(ast lang.AST) []interface{} { var components []interface{} for _, def := range ast.Definitions { if def.Page != nil { // Get components directly in the page for i := range def.Page.Components { components = append(components, &def.Page.Components[i]) } // Get components from sections components = append(components, ti.getComponentsFromSections(def.Page.Sections)...) } } return components } // getComponentsFromSections recursively extracts components from sections func (ti *TemplateInterpreter) getComponentsFromSections(sections []lang.Section) []interface{} { var components []interface{} for i := range sections { for j := range sections[i].Elements { element := §ions[i].Elements[j] if element.Component != nil { components = append(components, element.Component) // Get nested components from this component components = append(components, ti.getComponentsFromComponents([]lang.Component{*element.Component})...) } else if element.Section != nil { // Recursively get components from nested sections components = append(components, ti.getComponentsFromSections([]lang.Section{*element.Section})...) } } } return components } // getComponentsFromComponents recursively extracts components from nested components func (ti *TemplateInterpreter) getComponentsFromComponents(components []lang.Component) []interface{} { var result []interface{} for i := range components { for j := range components[i].Elements { element := &components[i].Elements[j] if element.Section != nil { // Get components from nested sections result = append(result, ti.getComponentsFromSections([]lang.Section{*element.Section})...) } } } return result } // createItemTemplateData creates template data with the current item in context func (ti *TemplateInterpreter) createItemTemplateData(itemContext string, item interface{}, ast lang.AST) map[string]interface{} { if itemContext == "" { itemContext = "Item" // default } return map[string]interface{}{ "AST": ast, "Registry": ti.registry, itemContext: item, } } // evaluateCondition evaluates a condition string against the AST func (ti *TemplateInterpreter) evaluateCondition(condition string, ast lang.AST) bool { switch condition { case "has_entities": for _, def := range ast.Definitions { if def.Entity != nil { return true } } return false case "has_endpoints": for _, def := range ast.Definitions { if def.Endpoint != nil { return true } } return false case "has_pages": for _, def := range ast.Definitions { if def.Page != nil { return true } } return false case "has_servers": for _, def := range ast.Definitions { if def.Server != nil { return true } } return false default: return true } } // evaluateItemCondition evaluates a condition for a specific item func (ti *TemplateInterpreter) evaluateItemCondition(condition string, item interface{}, ast lang.AST) bool { _ = ast // Mark as intentionally unused for future extensibility switch condition { case "layout_admin": if page, ok := item.(*lang.Page); ok { return page.Layout == "admin" } case "layout_public": if page, ok := item.(*lang.Page); ok { return page.Layout == "public" } case "requires_auth": if page, ok := item.(*lang.Page); ok { return page.Auth } if endpoint, ok := item.(*lang.Endpoint); ok { return endpoint.Auth } } return true } // executeTemplate executes a template with the full AST context func (ti *TemplateInterpreter) executeTemplate(templateName string, ast lang.AST) (string, error) { if tmpl, exists := ti.registry.templates[templateName]; exists { data := struct { AST lang.AST Registry *TemplateRegistry }{ AST: ast, Registry: ti.registry, } var buf bytes.Buffer err := tmpl.Execute(&buf, data) return buf.String(), err } return "", fmt.Errorf("template %s not found", templateName) } // executeTemplateWithData executes a template with custom data func (ti *TemplateInterpreter) executeTemplateWithData(templateName string, data interface{}) (string, error) { if tmpl, exists := ti.registry.templates[templateName]; exists { var buf bytes.Buffer err := tmpl.Execute(&buf, data) return buf.String(), err } return "", fmt.Errorf("template %s not found", templateName) } // executePathTemplate executes a path template to generate dynamic filenames func (ti *TemplateInterpreter) executePathTemplate(pathTemplate string, data interface{}) (string, error) { tmpl, err := template.New("pathTemplate").Funcs(ti.registry.GetFuncMap()).Parse(pathTemplate) if err != nil { return "", fmt.Errorf("error parsing path template: %w", err) } var buf bytes.Buffer err = tmpl.Execute(&buf, data) if err != nil { return "", fmt.Errorf("error executing path template: %w", err) } return buf.String(), nil } type TemplateRegistry struct { templates map[string]*template.Template funcMap template.FuncMap } func NewTemplateRegistry() *TemplateRegistry { tr := &TemplateRegistry{ templates: make(map[string]*template.Template), } // Create funcMap with helper functions that will be available in all templates tr.funcMap = template.FuncMap{ "executeTemplate": func(name string, data interface{}) (string, error) { if tmpl, exists := tr.templates[name]; exists { var buf strings.Builder err := tmpl.Execute(&buf, data) return buf.String(), err } return "", fmt.Errorf("template %s not found", name) }, "hasTemplate": func(name string) bool { _, exists := tr.templates[name] return exists }, "title": cases.Title(language.English).String, "lower": strings.ToLower, "upper": strings.ToUpper, "goType": func(t string) string { typeMap := map[string]string{ "string": "string", "int": "int", "uuid": "string", "boolean": "bool", "timestamp": "time.Time", "text": "string", "object": "interface{}", } if goType, ok := typeMap[t]; ok { return goType } return "interface{}" }, "pathToHandlerName": func(path string) string { // Convert "/users/{id}" to "Users" re := regexp.MustCompile(`[^a-zA-Z0-9]+`) name := re.ReplaceAllString(path, " ") name = strings.TrimSpace(name) name = cases.Title(language.English).String(name) return strings.ReplaceAll(name, " ", "") }, "getHost": func(settings []lang.ServerSetting) string { for _, s := range settings { if s.Host != nil { if s.Host.Literal != nil { return "\"" + *s.Host.Literal + "\"" } return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Host.EnvVar.Name, func() string { if s.Host.EnvVar.Default != nil { return *s.Host.EnvVar.Default } return "localhost" }()) } } return "localhost" }, "getPort": func(settings []lang.ServerSetting) string { for _, s := range settings { if s.Port != nil { if s.Port.Literal != nil { return strconv.Itoa(*s.Port.Literal) } return fmt.Sprintf(`func() string { if v := os.Getenv("%s"); v != "" { return v }; return "%s" }()`, s.Port.EnvVar.Name, func() string { if s.Port.EnvVar.Default != nil { return *s.Port.EnvVar.Default } return "8080" }()) } } return "8080" }, "getServerHostPort": func(settings []lang.ServerSetting) string { host := "localhost" port := 8080 for _, s := range settings { if s.Host != nil { if s.Host.Literal != nil { host = *s.Host.Literal } if s.Host.EnvVar != nil && s.Host.EnvVar.Default != nil { host = *s.Host.EnvVar.Default } // If it's an env var, keep the default } if s.Port != nil { if s.Port.Literal != nil { port = *s.Port.Literal } if s.Port.EnvVar != nil && s.Port.EnvVar.Default != nil { if p, err := strconv.Atoi(*s.Port.EnvVar.Default); err == nil { port = p } } // If it's an env var, keep the default } } return fmt.Sprintf("%s:%d", host, port) }, "slice": func() []interface{} { return []interface{}{} }, "append": func(slice []interface{}, item interface{}) []interface{} { return append(slice, item) }, "add": func(a, b int) int { return a + b }, "derefString": func(s *string) string { if s != nil { return *s } return "" }, "derefInt": func(i *int) int { if i != nil { return *i } return 0 }, "relativePrefix": func(path string) string { // Remove leading slash and split by "/" cleanPath := strings.TrimPrefix(path, "/") if cleanPath == "" { return "../" } parts := strings.Split(cleanPath, "/") depth := len(parts) // Build relative prefix with "../" for each level var prefix strings.Builder for i := 0; i < depth; i++ { prefix.WriteString("../") } return prefix.String() }, } return tr } func (tr *TemplateRegistry) GetFuncMap() template.FuncMap { // Add the registry function to the existing funcMap funcMap := make(template.FuncMap) for k, v := range tr.funcMap { funcMap[k] = v } funcMap["registry"] = func() *TemplateRegistry { return tr } return funcMap } func (tr *TemplateRegistry) Register(name, content string) error { tmpl, err := template.New(name).Funcs(tr.funcMap).Parse(content) if err != nil { return err } tr.templates[name] = tmpl return nil } func (tr *TemplateRegistry) LoadFromDirectory(dir string) error { return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if strings.HasSuffix(path, ".tmpl") { content, err := os.ReadFile(path) if err != nil { return err } name := strings.TrimSuffix(filepath.Base(path), ".tmpl") return tr.Register(name, string(content)) } return nil }) }