651 lines
18 KiB
Go
651 lines
18 KiB
Go
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
|
|
})
|
|
}
|