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" ) // 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 } 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, "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 }, } 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 }) }