Files
masonry/interpreter/template_interpreter.go

768 lines
21 KiB
Go

package interpreter
import (
"bytes"
"fmt"
"masonry/lang"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
"github.com/robertkrimen/otto"
"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"`
Functions map[string]string `yaml:"functions,omitempty"`
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
vm *otto.Otto
}
// NewTemplateInterpreter creates a new template interpreter
func NewTemplateInterpreter() *TemplateInterpreter {
return &TemplateInterpreter{
registry: NewTemplateRegistry(),
vm: otto.New(),
}
}
// 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 manifest first to get custom functions
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)
}
// Load custom JavaScript functions from the manifest BEFORE loading templates
err = ti.loadCustomFunctions(manifest.Functions)
if err != nil {
return nil, fmt.Errorf("error loading custom functions: %w", err)
}
// Now load templates from directory (they will have access to custom functions)
err = ti.registry.LoadFromDirectory(templateDir)
if err != nil {
return nil, fmt.Errorf("error loading templates: %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 from page elements
for i := range def.Page.Elements {
element := &def.Page.Elements[i]
if element.Section != nil {
sections = append(sections, element.Section)
// Recursively get nested sections
sections = append(sections, ti.getSectionsFromSection(*element.Section)...)
}
}
}
}
return sections
}
// getSectionsFromSection recursively extracts sections from a single section
func (ti *TemplateInterpreter) getSectionsFromSection(section lang.Section) []interface{} {
var result []interface{}
for i := range section.Elements {
element := &section.Elements[i]
if element.Section != nil {
result = append(result, element.Section)
// Recursively get sections from this section
result = append(result, ti.getSectionsFromSection(*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 from page elements
for i := range def.Page.Elements {
element := &def.Page.Elements[i]
if element.Component != nil {
components = append(components, element.Component)
// Get nested components from this component
components = append(components, ti.getComponentsFromComponent(*element.Component)...)
} else if element.Section != nil {
// Get components from sections
components = append(components, ti.getComponentsFromSection(*element.Section)...)
}
}
}
}
return components
}
// getComponentsFromSection recursively extracts components from a section
func (ti *TemplateInterpreter) getComponentsFromSection(section lang.Section) []interface{} {
var components []interface{}
for i := range section.Elements {
element := &section.Elements[i]
if element.Component != nil {
components = append(components, element.Component)
// Get nested components from this component
components = append(components, ti.getComponentsFromComponent(*element.Component)...)
} else if element.Section != nil {
// Recursively get components from nested sections
components = append(components, ti.getComponentsFromSection(*element.Section)...)
}
}
return components
}
// getComponentsFromComponent recursively extracts components from nested components
func (ti *TemplateInterpreter) getComponentsFromComponent(component lang.Component) []interface{} {
var result []interface{}
for i := range component.Elements {
element := &component.Elements[i]
if element.Section != nil {
// Get components from nested sections
result = append(result, ti.getComponentsFromSection(*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
})
}
// loadCustomFunctions loads JavaScript functions into the template function map
func (ti *TemplateInterpreter) loadCustomFunctions(functions map[string]string) error {
if functions == nil {
return nil
}
for funcName, jsCode := range functions {
// Validate function name
if !isValidFunctionName(funcName) {
return fmt.Errorf("invalid function name: %s", funcName)
}
// Create a wrapper function that calls the JavaScript code
templateFunc := ti.createJavaScriptTemplateFunction(funcName, jsCode)
ti.registry.funcMap[funcName] = templateFunc
}
return nil
}
// createJavaScriptTemplateFunction creates a Go template function that executes JavaScript
func (ti *TemplateInterpreter) createJavaScriptTemplateFunction(funcName, jsCode string) interface{} {
return func(args ...interface{}) (interface{}, error) {
// Create a new VM instance for this function call to avoid conflicts
vm := otto.New()
// Set up global helper functions in the JavaScript environment
vm.Set("log", func(call otto.FunctionCall) otto.Value {
// For debugging - could be extended to proper logging
fmt.Printf("[JS %s]: %v\n", funcName, call.ArgumentList)
return otto.UndefinedValue()
})
// Convert Go arguments to JavaScript values
for i, arg := range args {
vm.Set(fmt.Sprintf("arg%d", i), arg)
}
// Set a convenience 'args' array
argsArray, _ := vm.Object("args = []")
for i, arg := range args {
argsArray.Set(strconv.Itoa(i), arg)
}
// Execute the JavaScript function
jsWrapper := fmt.Sprintf(`
(function() {
%s
if (typeof main === 'function') {
return main.apply(this, args);
} else {
throw new Error('Custom function must define a main() function');
}
})();
`, jsCode)
result, err := vm.Run(jsWrapper)
if err != nil {
return nil, fmt.Errorf("error executing JavaScript function %s: %w", funcName, err)
}
// Convert JavaScript result back to Go value
if result.IsUndefined() || result.IsNull() {
return nil, nil
}
goValue, err := result.Export()
if err != nil {
return nil, fmt.Errorf("error converting JavaScript result to Go value: %w", err)
}
return goValue, nil
}
}
// isValidFunctionName checks if the function name is valid for Go templates
func isValidFunctionName(name string) bool {
// Basic validation: alphanumeric and underscore, must start with letter
if len(name) == 0 {
return false
}
if !((name[0] >= 'a' && name[0] <= 'z') || (name[0] >= 'A' && name[0] <= 'Z') || name[0] == '_') {
return false
}
for _, char := range name[1:] {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '_') {
return false
}
}
// Avoid conflicts with built-in template functions
builtinFunctions := map[string]bool{
"and": true, "or": true, "not": true, "len": true, "index": true,
"print": true, "printf": true, "println": true, "html": true, "js": true,
"call": true, "urlquery": true, "eq": true, "ne": true, "lt": true,
"le": true, "gt": true, "ge": true,
}
return !builtinFunctions[name]
}