383 lines
9.0 KiB
Go
383 lines
9.0 KiB
Go
|
package raymond
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"reflect"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
// Options represents the options argument provided to helpers and context functions.
|
||
|
type Options struct {
|
||
|
// evaluation visitor
|
||
|
eval *evalVisitor
|
||
|
|
||
|
// params
|
||
|
params []interface{}
|
||
|
hash map[string]interface{}
|
||
|
}
|
||
|
|
||
|
// helpers stores all globally registered helpers
|
||
|
var helpers = make(map[string]reflect.Value)
|
||
|
|
||
|
// protects global helpers
|
||
|
var helpersMutex sync.RWMutex
|
||
|
|
||
|
func init() {
|
||
|
// register builtin helpers
|
||
|
RegisterHelper("if", ifHelper)
|
||
|
RegisterHelper("unless", unlessHelper)
|
||
|
RegisterHelper("with", withHelper)
|
||
|
RegisterHelper("each", eachHelper)
|
||
|
RegisterHelper("log", logHelper)
|
||
|
RegisterHelper("lookup", lookupHelper)
|
||
|
RegisterHelper("equal", equalHelper)
|
||
|
}
|
||
|
|
||
|
// RegisterHelper registers a global helper. That helper will be available to all templates.
|
||
|
func RegisterHelper(name string, helper interface{}) {
|
||
|
helpersMutex.Lock()
|
||
|
defer helpersMutex.Unlock()
|
||
|
|
||
|
if helpers[name] != zero {
|
||
|
panic(fmt.Errorf("Helper already registered: %s", name))
|
||
|
}
|
||
|
|
||
|
val := reflect.ValueOf(helper)
|
||
|
ensureValidHelper(name, val)
|
||
|
|
||
|
helpers[name] = val
|
||
|
}
|
||
|
|
||
|
// RegisterHelpers registers several global helpers. Those helpers will be available to all templates.
|
||
|
func RegisterHelpers(helpers map[string]interface{}) {
|
||
|
for name, helper := range helpers {
|
||
|
RegisterHelper(name, helper)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ensureValidHelper panics if given helper is not valid
|
||
|
func ensureValidHelper(name string, funcValue reflect.Value) {
|
||
|
if funcValue.Kind() != reflect.Func {
|
||
|
panic(fmt.Errorf("Helper must be a function: %s", name))
|
||
|
}
|
||
|
|
||
|
funcType := funcValue.Type()
|
||
|
|
||
|
if funcType.NumOut() != 1 {
|
||
|
panic(fmt.Errorf("Helper function must return a string or a SafeString: %s", name))
|
||
|
}
|
||
|
|
||
|
// @todo Check if first returned value is a string, SafeString or interface{} ?
|
||
|
}
|
||
|
|
||
|
// findHelper finds a globally registered helper
|
||
|
func findHelper(name string) reflect.Value {
|
||
|
helpersMutex.RLock()
|
||
|
defer helpersMutex.RUnlock()
|
||
|
|
||
|
return helpers[name]
|
||
|
}
|
||
|
|
||
|
// newOptions instanciates a new Options
|
||
|
func newOptions(eval *evalVisitor, params []interface{}, hash map[string]interface{}) *Options {
|
||
|
return &Options{
|
||
|
eval: eval,
|
||
|
params: params,
|
||
|
hash: hash,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// newEmptyOptions instanciates a new empty Options
|
||
|
func newEmptyOptions(eval *evalVisitor) *Options {
|
||
|
return &Options{
|
||
|
eval: eval,
|
||
|
hash: make(map[string]interface{}),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Context Values
|
||
|
//
|
||
|
|
||
|
// Value returns field value from current context.
|
||
|
func (options *Options) Value(name string) interface{} {
|
||
|
value := options.eval.evalField(options.eval.curCtx(), name, false)
|
||
|
if !value.IsValid() {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return value.Interface()
|
||
|
}
|
||
|
|
||
|
// ValueStr returns string representation of field value from current context.
|
||
|
func (options *Options) ValueStr(name string) string {
|
||
|
return Str(options.Value(name))
|
||
|
}
|
||
|
|
||
|
// Ctx returns current evaluation context.
|
||
|
func (options *Options) Ctx() interface{} {
|
||
|
return options.eval.curCtx().Interface()
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Hash Arguments
|
||
|
//
|
||
|
|
||
|
// HashProp returns hash property.
|
||
|
func (options *Options) HashProp(name string) interface{} {
|
||
|
return options.hash[name]
|
||
|
}
|
||
|
|
||
|
// HashStr returns string representation of hash property.
|
||
|
func (options *Options) HashStr(name string) string {
|
||
|
return Str(options.hash[name])
|
||
|
}
|
||
|
|
||
|
// Hash returns entire hash.
|
||
|
func (options *Options) Hash() map[string]interface{} {
|
||
|
return options.hash
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Parameters
|
||
|
//
|
||
|
|
||
|
// Param returns parameter at given position.
|
||
|
func (options *Options) Param(pos int) interface{} {
|
||
|
if len(options.params) > pos {
|
||
|
return options.params[pos]
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// ParamStr returns string representation of parameter at given position.
|
||
|
func (options *Options) ParamStr(pos int) string {
|
||
|
return Str(options.Param(pos))
|
||
|
}
|
||
|
|
||
|
// Params returns all parameters.
|
||
|
func (options *Options) Params() []interface{} {
|
||
|
return options.params
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Private data
|
||
|
//
|
||
|
|
||
|
// Data returns private data value.
|
||
|
func (options *Options) Data(name string) interface{} {
|
||
|
return options.eval.dataFrame.Get(name)
|
||
|
}
|
||
|
|
||
|
// DataStr returns string representation of private data value.
|
||
|
func (options *Options) DataStr(name string) string {
|
||
|
return Str(options.eval.dataFrame.Get(name))
|
||
|
}
|
||
|
|
||
|
// DataFrame returns current private data frame.
|
||
|
func (options *Options) DataFrame() *DataFrame {
|
||
|
return options.eval.dataFrame
|
||
|
}
|
||
|
|
||
|
// NewDataFrame instanciates a new data frame that is a copy of current evaluation data frame.
|
||
|
//
|
||
|
// Parent of returned data frame is set to current evaluation data frame.
|
||
|
func (options *Options) NewDataFrame() *DataFrame {
|
||
|
return options.eval.dataFrame.Copy()
|
||
|
}
|
||
|
|
||
|
// newIterDataFrame instanciates a new data frame and set iteration specific vars
|
||
|
func (options *Options) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
||
|
return options.eval.dataFrame.newIterDataFrame(length, i, key)
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Evaluation
|
||
|
//
|
||
|
|
||
|
// evalBlock evaluates block with given context, private data and iteration key
|
||
|
func (options *Options) evalBlock(ctx interface{}, data *DataFrame, key interface{}) string {
|
||
|
result := ""
|
||
|
|
||
|
if block := options.eval.curBlock(); (block != nil) && (block.Program != nil) {
|
||
|
result = options.eval.evalProgram(block.Program, ctx, data, key)
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
// Fn evaluates block with current evaluation context.
|
||
|
func (options *Options) Fn() string {
|
||
|
return options.evalBlock(nil, nil, nil)
|
||
|
}
|
||
|
|
||
|
// FnCtxData evaluates block with given context and private data frame.
|
||
|
func (options *Options) FnCtxData(ctx interface{}, data *DataFrame) string {
|
||
|
return options.evalBlock(ctx, data, nil)
|
||
|
}
|
||
|
|
||
|
// FnWith evaluates block with given context.
|
||
|
func (options *Options) FnWith(ctx interface{}) string {
|
||
|
return options.evalBlock(ctx, nil, nil)
|
||
|
}
|
||
|
|
||
|
// FnData evaluates block with given private data frame.
|
||
|
func (options *Options) FnData(data *DataFrame) string {
|
||
|
return options.evalBlock(nil, data, nil)
|
||
|
}
|
||
|
|
||
|
// Inverse evaluates "else block".
|
||
|
func (options *Options) Inverse() string {
|
||
|
result := ""
|
||
|
if block := options.eval.curBlock(); (block != nil) && (block.Inverse != nil) {
|
||
|
result, _ = block.Inverse.Accept(options.eval).(string)
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
// Eval evaluates field for given context.
|
||
|
func (options *Options) Eval(ctx interface{}, field string) interface{} {
|
||
|
if ctx == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if field == "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
val := options.eval.evalField(reflect.ValueOf(ctx), field, false)
|
||
|
if !val.IsValid() {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return val.Interface()
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Misc
|
||
|
//
|
||
|
|
||
|
// isIncludableZero returns true if 'includeZero' option is set and first param is the number 0
|
||
|
func (options *Options) isIncludableZero() bool {
|
||
|
b, ok := options.HashProp("includeZero").(bool)
|
||
|
if ok && b {
|
||
|
nb, ok := options.Param(0).(int)
|
||
|
if ok && nb == 0 {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Builtin helpers
|
||
|
//
|
||
|
|
||
|
// #if block helper
|
||
|
func ifHelper(conditional interface{}, options *Options) interface{} {
|
||
|
if options.isIncludableZero() || IsTrue(conditional) {
|
||
|
return options.Fn()
|
||
|
}
|
||
|
|
||
|
return options.Inverse()
|
||
|
}
|
||
|
|
||
|
// #unless block helper
|
||
|
func unlessHelper(conditional interface{}, options *Options) interface{} {
|
||
|
if options.isIncludableZero() || IsTrue(conditional) {
|
||
|
return options.Inverse()
|
||
|
}
|
||
|
|
||
|
return options.Fn()
|
||
|
}
|
||
|
|
||
|
// #with block helper
|
||
|
func withHelper(context interface{}, options *Options) interface{} {
|
||
|
if IsTrue(context) {
|
||
|
return options.FnWith(context)
|
||
|
}
|
||
|
|
||
|
return options.Inverse()
|
||
|
}
|
||
|
|
||
|
// #each block helper
|
||
|
func eachHelper(context interface{}, options *Options) interface{} {
|
||
|
if !IsTrue(context) {
|
||
|
return options.Inverse()
|
||
|
}
|
||
|
|
||
|
result := ""
|
||
|
|
||
|
val := reflect.ValueOf(context)
|
||
|
switch val.Kind() {
|
||
|
case reflect.Array, reflect.Slice:
|
||
|
for i := 0; i < val.Len(); i++ {
|
||
|
// computes private data
|
||
|
data := options.newIterDataFrame(val.Len(), i, nil)
|
||
|
|
||
|
// evaluates block
|
||
|
result += options.evalBlock(val.Index(i).Interface(), data, i)
|
||
|
}
|
||
|
case reflect.Map:
|
||
|
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
||
|
keys := val.MapKeys()
|
||
|
for i := 0; i < len(keys); i++ {
|
||
|
key := keys[i].Interface()
|
||
|
ctx := val.MapIndex(keys[i]).Interface()
|
||
|
|
||
|
// computes private data
|
||
|
data := options.newIterDataFrame(len(keys), i, key)
|
||
|
|
||
|
// evaluates block
|
||
|
result += options.evalBlock(ctx, data, key)
|
||
|
}
|
||
|
case reflect.Struct:
|
||
|
var exportedFields []int
|
||
|
|
||
|
// collect exported fields only
|
||
|
for i := 0; i < val.NumField(); i++ {
|
||
|
if tField := val.Type().Field(i); tField.PkgPath == "" {
|
||
|
exportedFields = append(exportedFields, i)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i, fieldIndex := range exportedFields {
|
||
|
key := val.Type().Field(fieldIndex).Name
|
||
|
ctx := val.Field(fieldIndex).Interface()
|
||
|
|
||
|
// computes private data
|
||
|
data := options.newIterDataFrame(len(exportedFields), i, key)
|
||
|
|
||
|
// evaluates block
|
||
|
result += options.evalBlock(ctx, data, key)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
// #log helper
|
||
|
func logHelper(message string) interface{} {
|
||
|
log.Print(message)
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// #lookup helper
|
||
|
func lookupHelper(obj interface{}, field string, options *Options) interface{} {
|
||
|
return Str(options.Eval(obj, field))
|
||
|
}
|
||
|
|
||
|
// #equal helper
|
||
|
// Ref: https://github.com/aymerick/raymond/issues/7
|
||
|
func equalHelper(a interface{}, b interface{}, options *Options) interface{} {
|
||
|
if Str(a) == Str(b) {
|
||
|
return options.Fn()
|
||
|
}
|
||
|
|
||
|
return ""
|
||
|
}
|