Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
version: v1.0
name: Go
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
blocks:
- name: Test
task:
jobs:
- name: go test
commands:
- sem-version go 1.18
- export GOPATH=~/go
- 'export PATH=/home/semaphore/go/bin:$PATH'
- checkout
- go get ./...
- go test ./...
version: v1.0
name: Go
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
blocks:
- name: Test
task:
jobs:
- name: go test
commands:
- sem-version go 1.18
- export GOPATH=~/go
- 'export PATH=/home/semaphore/go/bin:$PATH'
- checkout
- go get ./...
- go test ./...
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Please use the [issue tracker](https://github.com/flosch/pongo2/issues) if you'r
- [Advanced C-like expressions](https://github.com/flosch/pongo2/blob/master/template_tests/expressions.tpl).
- [Complex function calls within expressions](https://github.com/flosch/pongo2/blob/master/template_tests/function_calls_wrapper.tpl).
- [Easy API to create new filters and tags](http://godoc.org/github.com/flosch/pongo2#RegisterFilter) ([including parsing arguments](http://godoc.org/github.com/flosch/pongo2#Parser))
- [Customizing variable resolution](http://godoc.org/github.com/flosch/pongo2#hdr-Variable_resolution)
- Additional features:
- Macros including importing macros from other files (see [template_tests/macro.tpl](https://github.com/flosch/pongo2/blob/master/template_tests/macro.tpl))
- [Template sandboxing](https://godoc.org/github.com/flosch/pongo2#TemplateSet) ([directory patterns](http://golang.org/pkg/path/filepath/#Match), banned tags/filters)
Expand Down
48 changes: 48 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,54 @@ func SetAutoescape(newValue bool) {
// {{ myfunc("test", 42) }}
// {{ user.name }}
// {{ pongo2.version }}
//
// Variable resolution
//
// By the default sub variables are resolved by
// 1. Field names inside structs
// 2. Keys of values inside maps
// 3. Indexes of values inside slices
//
// This behavior can be customized using either struct tag "pongo2" like:
// type MyStruct struct {
// FieldA string `pongo2:"uh"`
// FieldB string `pongo2:"yeah"`
// }
//
// my.tmpl:
// {{ myStruct.uh }} {{ myStruct.yeah }}
//
// ...or by implementing NamedFieldResolver or IndexedFieldResolver.
//
// type MyStruct struct {
// fieldA string
// }
//
// // GetNamedField implements NamedFieldResolver
// func (s MyStruct) GetNamedField(s string) (interface{}, error) {
// switch s {
// case "uh":
// return s.fieldA, nil
// case "yeah":
// return "YEAH!", nil
// default:
// return nil, pongo2.ErrNoSuchField
// }
//
// // GetNamedField implements IndexedFieldResolver
// func (s MyStruct) GetIndexedField(s int) (interface{}, error) {
// switch s {
// case 0:
// return s.fieldA, nil
// case 1:
// return "YEAH!", nil
// default:
// return nil, pongo2.ErrNoSuchField
// }
//
// my.tmpl:
// {{ myStruct.uh }} {{ myStruct.yeah }}
// {{ myStruct.0 }} {{ myStruct.1 }}
type Context map[string]any

func (c Context) checkForValidIdentifiers() *Error {
Expand Down
230 changes: 167 additions & 63 deletions variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@ import (
"strings"
)

var (
ErrNoSuchField = errors.New("no such field")
)

// NamedFieldResolver can be implemented by every value inside a Context.
// By default, reflection is used to resolve fields of a struct or a map.
type NamedFieldResolver interface {
// GetNamedField will be called with the requested field name.
// Any error will lead to the immediate interruption of the evaluation;
// except in ErrNoSuchField: In this case the default evaluation process will
// continue (reflection).
GetNamedField(string) (interface{}, error)
}

// IndexedFieldResolver can be implemented by every value inside a Context.
// By default, reflection is used to resolve index of a slice.
type IndexedFieldResolver interface {
// GetIndexedField will be called with the requested field index.
// Any error will lead to the immediate interruption of the evaluation;
// except in ErrNoSuchField: In this case the default evaluation process will
// continue (reflection).
GetIndexedField(int) (interface{}, error)
}

const (
varTypeInt = iota
varTypeIdent
Expand Down Expand Up @@ -304,76 +328,25 @@ func (vr *variableResolver) resolve(ctx *ExecutionContext) (*Value, error) {
}

// Look up which part must be called now
var resolver func(*ExecutionContext, reflect.Value, *variablePart) (_ reflect.Value, done bool, _ error)
switch part.typ {
case varTypeInt:
// Calling an index is only possible for:
// * slices/arrays/strings
switch current.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
if part.i >= 0 && current.Len() > part.i {
current = current.Index(part.i)
} else {
// In Django, exceeding the length of a list is just empty.
return AsValue(nil), nil
}
default:
return nil, fmt.Errorf("can't access an index on type %s (variable %s)",
current.Kind().String(), vr.String())
}
resolver = vr.resolveIndexedField
case varTypeIdent:
// Calling a field or key
switch current.Kind() {
case reflect.Struct:
current = current.FieldByName(part.s)
case reflect.Map:
current = current.MapIndex(reflect.ValueOf(part.s))
default:
return nil, fmt.Errorf("can't access a field by name on type %s (variable %s)",
current.Kind().String(), vr.String())
}
resolver = vr.resolveNamedField
case varTypeSubscript:
// Calling an index is only possible for:
// * slices/arrays/strings
switch current.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
sv, err := part.subscript.Evaluate(ctx)
if err != nil {
return nil, err
}
si := sv.Integer()
if si >= 0 && current.Len() > si {
current = current.Index(si)
} else {
// In Django, exceeding the length of a list is just empty.
return AsValue(nil), nil
}
// Calling a field or key
case reflect.Struct:
sv, err := part.subscript.Evaluate(ctx)
if err != nil {
return nil, err
}
current = current.FieldByName(sv.String())
case reflect.Map:
sv, err := part.subscript.Evaluate(ctx)
if err != nil {
return nil, err
}
if sv.IsNil() {
return AsValue(nil), nil
}
if sv.val.Type().AssignableTo(current.Type().Key()) {
current = current.MapIndex(sv.val)
} else {
return AsValue(nil), nil
}
default:
return nil, fmt.Errorf("can't access an index on type %s (variable %s)",
current.Kind().String(), vr.String())
}
resolver = vr.resolveSubscriptField
default:
panic("unimplemented")
}

if v, done, err := resolver(ctx, current, part); err != nil {
return nil, err
} else if done {
return &Value{val: v}, nil
} else {
current = v
}
}
}

Expand Down Expand Up @@ -524,6 +497,137 @@ func (vr *variableResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return value, nil
}

func (vr *variableResolver) resolveNamedField(_ *ExecutionContext, of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) {
// If current does implement NamedFieldResolver, call it with the actual field name.
if fr, ok := of.Interface().(NamedFieldResolver); ok {
if val, err := fr.GetNamedField(by.s); err == ErrNoSuchField {
// Continue with reflection, below...
} else if err != nil {
return reflect.Value{}, false, fmt.Errorf("can't access field %s on type %s (variable %s): %w",
by.s, of.Kind().String(), vr.String(), err)
} else {
return reflect.ValueOf(val), false, nil
}
}

// Calling a field or key
switch of.Kind() {
case reflect.Struct:
return vr.resolveStructField(of, by.s), false, nil
case reflect.Map:
return of.MapIndex(reflect.ValueOf(by.s)), false, nil
default:
return reflect.Value{}, false, fmt.Errorf("can't access a field by name on type %s (variable %s)",
of.Kind().String(), vr.String())
}
}

func (vr *variableResolver) resolveStructField(of reflect.Value, name string) reflect.Value {
t := of.Type()
nf := t.NumField()
var byAliasIndex, byNameIndex []int
for i := 0; i < nf; i++ {
f := t.Field(i)
// Only respect exported field to prevent security issues in templates.
if f.IsExported() {
// We remember this field if its name matches.
if f.Name == name {
byNameIndex = f.Index
}
alias := vr.resolveStructFieldTag(f)
// We remember this field if its alias via tag matches.
if alias == name {
byAliasIndex = f.Index
}
}
}

if byAliasIndex != nil {
return of.FieldByIndex(byAliasIndex)
}
if byNameIndex != nil {
return of.FieldByIndex(byNameIndex)
}
return reflect.Value{}
}

func (vr *variableResolver) resolveStructFieldTag(of reflect.StructField) (alias string) {
plain := of.Tag.Get("pongo2")
plainParts := strings.SplitN(plain, ",", 2)
return strings.TrimSpace(plainParts[0])
}

func (vr *variableResolver) resolveIndexedField(_ *ExecutionContext, of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) {
// If current does implement IndexedFieldResolver, call it with the actual index.
if fr, ok := of.Interface().(IndexedFieldResolver); ok {
if val, err := fr.GetIndexedField(by.i); err == ErrNoSuchField {
// Continue with reflection, below...
} else if err != nil {
return reflect.Value{}, false, fmt.Errorf("can't access index %d on type %s (variable %s): %w",
by.i, of.Kind().String(), vr.String(), err)
} else {
return reflect.ValueOf(val), false, nil
}
}

// Calling an index is only possible for:
// * slices/arrays/strings
switch of.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
if by.i >= 0 && of.Len() > by.i {
return of.Index(by.i), false, nil
} else {
// In Django, exceeding the length of a list is just empty.
return reflect.ValueOf(nil), true, nil
}
default:
return reflect.Value{}, false, fmt.Errorf("can't access an index on type %s (variable %s)",
of.Kind().String(), vr.String())
}
}

func (vr *variableResolver) resolveSubscriptField(ctx *ExecutionContext, of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) {
// Calling an index is only possible for:
// * slices/arrays/strings
switch of.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
sv, err := by.subscript.Evaluate(ctx)
if err != nil {
return reflect.Value{}, false, err
}
si := sv.Integer()
if si >= 0 && of.Len() > si {
return of.Index(si), false, nil
} else {
// In Django, exceeding the length of a list is just empty.
return reflect.ValueOf(nil), true, nil
}
// Calling a field or key
case reflect.Struct:
sv, err := by.subscript.Evaluate(ctx)
if err != nil {
return reflect.Value{}, false, err
}
return of.FieldByName(sv.String()), false, nil
case reflect.Map:
sv, err := by.subscript.Evaluate(ctx)
if err != nil {
return reflect.Value{}, false, err
}
if sv.IsNil() {
return reflect.ValueOf(nil), true, nil
}
if sv.val.Type().AssignableTo(of.Type().Key()) {
return of.MapIndex(sv.val), false, nil
} else {
return reflect.ValueOf(nil), true, nil
}
default:
return reflect.Value{}, false, fmt.Errorf("can't access an index on type %s (variable %s)",
of.Kind().String(), vr.String())
}
}

func (v *nodeFilteredVariable) FilterApplied(name string) bool {
for _, filter := range v.filterChain {
if filter.name == name {
Expand Down
Loading