add custom js functions for templates
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -18,9 +19,10 @@ import (
|
||||
|
||||
// TemplateManifest defines the structure for multi-file template generation
|
||||
type TemplateManifest struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Outputs []OutputFile `yaml:"outputs"`
|
||||
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
|
||||
@ -35,12 +37,14 @@ type OutputFile struct {
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,13 +119,7 @@ func (ti *TemplateInterpreter) Interpret(masonryInput string, tmplText string) (
|
||||
|
||||
// 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
|
||||
// Load manifest first to get custom functions
|
||||
manifestPath := filepath.Join(templateDir, manifestFile)
|
||||
manifestData, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
@ -134,6 +132,18 @@ func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manife
|
||||
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 {
|
||||
@ -259,30 +269,31 @@ func (ti *TemplateInterpreter) getAllSections(ast lang.AST) []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])
|
||||
// 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)...)
|
||||
}
|
||||
}
|
||||
// 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{} {
|
||||
// getSectionsFromSection recursively extracts sections from a single section
|
||||
func (ti *TemplateInterpreter) getSectionsFromSection(section 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})...)
|
||||
}
|
||||
for i := range section.Elements {
|
||||
element := §ion.Elements[i]
|
||||
if element.Section != nil {
|
||||
result = append(result, element.Section)
|
||||
// Recursively get sections from this section
|
||||
result = append(result, ti.getSectionsFromSection(*element.Section)...)
|
||||
}
|
||||
}
|
||||
|
||||
@ -295,50 +306,52 @@ func (ti *TemplateInterpreter) getAllComponents(ast lang.AST) []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 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)...)
|
||||
}
|
||||
}
|
||||
// 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{} {
|
||||
// getComponentsFromSection recursively extracts components from a section
|
||||
func (ti *TemplateInterpreter) getComponentsFromSection(section 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})...)
|
||||
}
|
||||
for i := range section.Elements {
|
||||
element := §ion.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
|
||||
}
|
||||
|
||||
// getComponentsFromComponents recursively extracts components from nested components
|
||||
func (ti *TemplateInterpreter) getComponentsFromComponents(components []lang.Component) []interface{} {
|
||||
// getComponentsFromComponent recursively extracts components from nested components
|
||||
func (ti *TemplateInterpreter) getComponentsFromComponent(component 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})...)
|
||||
}
|
||||
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)...)
|
||||
}
|
||||
}
|
||||
|
||||
@ -648,3 +661,107 @@ func (tr *TemplateRegistry) LoadFromDirectory(dir string) error {
|
||||
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]
|
||||
}
|
||||
|
Reference in New Issue
Block a user