123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- 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 ""
- }
|