implement a multi-file template interpreter
This commit is contained in:
@ -13,8 +13,25 @@ import (
|
||||
|
||||
"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
|
||||
@ -96,6 +113,354 @@ func (ti *TemplateInterpreter) Interpret(masonryInput string, tmplText string) (
|
||||
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
|
||||
@ -121,6 +486,8 @@ func NewTemplateRegistry() *TemplateRegistry {
|
||||
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",
|
||||
@ -224,6 +591,24 @@ func NewTemplateRegistry() *TemplateRegistry {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user