Rewrite error handling logic
This commit is contained in:
parent
ba23bf512c
commit
01047706b6
@ -19,8 +19,8 @@ package ast
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
func PosError(n Node, filename, format string, v ...any) error {
|
func PosError(n Node, format string, v ...any) error {
|
||||||
return fmt.Errorf(filename+":"+n.Pos().String()+": "+format, v...)
|
return fmt.Errorf(n.Pos().String()+": "+format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Node interface {
|
type Node interface {
|
||||||
@ -28,12 +28,13 @@ type Node interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Position struct {
|
type Position struct {
|
||||||
|
Name string
|
||||||
Line int
|
Line int
|
||||||
Col int
|
Col int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Position) String() string {
|
func (p Position) String() string {
|
||||||
return fmt.Sprintf("%d:%d", p.Line, p.Col)
|
return fmt.Sprintf("%s: line %d, col %d", p.Name, p.Line, p.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
|
24
expr.go
24
expr.go
@ -19,20 +19,12 @@
|
|||||||
package salix
|
package salix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.elara.ws/salix/ast"
|
"go.elara.ws/salix/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrModulusFloat = errors.New("modulo operation cannot be performed on floats")
|
|
||||||
ErrTypeMismatch = errors.New("mismatched types")
|
|
||||||
ErrLogicalNonBool = errors.New("logical operations may only be performed on boolean values")
|
|
||||||
ErrInOpInvalidTypes = errors.New("the in operator can only be used on strings, arrays, and slices")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t *Template) evalExpr(expr ast.Expr, local map[string]any) (any, error) {
|
func (t *Template) evalExpr(expr ast.Expr, local map[string]any) (any, error) {
|
||||||
val, err := t.getValue(expr.First, local)
|
val, err := t.getValue(expr.First, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -65,25 +57,25 @@ func (t *Template) performOp(a, b reflect.Value, op ast.Operator) (any, error) {
|
|||||||
if a.CanConvert(b.Type().Elem()) {
|
if a.CanConvert(b.Type().Elem()) {
|
||||||
a = a.Convert(b.Type().Elem())
|
a = a.Convert(b.Type().Elem())
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(op, "%w (%s and %s)", ErrTypeMismatch, a.Type(), b.Type())
|
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||||
}
|
}
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if a.CanConvert(b.Type().Key()) {
|
if a.CanConvert(b.Type().Key()) {
|
||||||
a = a.Convert(b.Type().Key())
|
a = a.Convert(b.Type().Key())
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(op, "%w (%s and %s)", ErrTypeMismatch, a.Type(), b.Type())
|
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||||
}
|
}
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
if a.Kind() != reflect.String {
|
if a.Kind() != reflect.String {
|
||||||
return nil, t.posError(op, "%w (%s and %s)", ErrTypeMismatch, a.Type(), b.Type())
|
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, t.posError(op, "%w (got %s and %s)", ErrInOpInvalidTypes, a.Type(), b.Type())
|
return nil, ast.PosError(op, "the in operator can only be used on strings, arrays, and slices (got %s and %s)", a.Type(), b.Type())
|
||||||
}
|
}
|
||||||
} else if b.CanConvert(a.Type()) {
|
} else if b.CanConvert(a.Type()) {
|
||||||
b = b.Convert(a.Type())
|
b = b.Convert(a.Type())
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(op, "%w (%s and %s)", ErrTypeMismatch, a.Type(), b.Type())
|
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch op.Value {
|
switch op.Value {
|
||||||
@ -91,12 +83,12 @@ func (t *Template) performOp(a, b reflect.Value, op ast.Operator) (any, error) {
|
|||||||
return a.Equal(b), nil
|
return a.Equal(b), nil
|
||||||
case "&&":
|
case "&&":
|
||||||
if a.Kind() != reflect.Bool || b.Kind() != reflect.Bool {
|
if a.Kind() != reflect.Bool || b.Kind() != reflect.Bool {
|
||||||
return nil, t.posError(op, "%w", ErrLogicalNonBool)
|
return nil, ast.PosError(op, "logical operations may only be performed on boolean values")
|
||||||
}
|
}
|
||||||
return a.Bool() && b.Bool(), nil
|
return a.Bool() && b.Bool(), nil
|
||||||
case "||":
|
case "||":
|
||||||
if a.Kind() != reflect.Bool || b.Kind() != reflect.Bool {
|
if a.Kind() != reflect.Bool || b.Kind() != reflect.Bool {
|
||||||
return nil, t.posError(op, "%w", ErrLogicalNonBool)
|
return nil, ast.PosError(op, "logical operations may only be performed on boolean values")
|
||||||
}
|
}
|
||||||
return a.Bool() || b.Bool(), nil
|
return a.Bool() || b.Bool(), nil
|
||||||
case ">=":
|
case ">=":
|
||||||
@ -180,7 +172,7 @@ func (t *Template) performOp(a, b reflect.Value, op ast.Operator) (any, error) {
|
|||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
return a.Uint() % b.Uint(), nil
|
return a.Uint() % b.Uint(), nil
|
||||||
case reflect.Float64, reflect.Float32:
|
case reflect.Float64, reflect.Float32:
|
||||||
return nil, t.posError(op, "%w", ErrModulusFloat)
|
return nil, ast.PosError(op, "modulus operation cannot be performed on floats")
|
||||||
}
|
}
|
||||||
case "in":
|
case "in":
|
||||||
if a.Kind() == reflect.String && b.Kind() == reflect.String {
|
if a.Kind() == reflect.String && b.Kind() == reflect.String {
|
||||||
|
18
for_tag.go
18
for_tag.go
@ -19,33 +19,30 @@
|
|||||||
package salix
|
package salix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"go.elara.ws/salix/ast"
|
"go.elara.ws/salix/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrForTagInvalidArgs = errors.New("invalid arguments in for tag")
|
|
||||||
|
|
||||||
// forTag represents a #for tag within a Salix template
|
// forTag represents a #for tag within a Salix template
|
||||||
type forTag struct{}
|
type forTag struct{}
|
||||||
|
|
||||||
func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||||
if len(args) == 0 || len(args) > 2 {
|
if len(args) == 0 || len(args) > 2 {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(tc.Tag, "invalid argument amount")
|
||||||
}
|
}
|
||||||
|
|
||||||
var expr ast.Expr
|
var expr ast.Expr
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
expr2, ok := args[0].(ast.Expr)
|
expr2, ok := args[0].(ast.Expr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(args[0], "invalid argument type: %T (expected ast.Expr)", args[0])
|
||||||
}
|
}
|
||||||
expr = expr2
|
expr = expr2
|
||||||
} else if len(args) == 2 {
|
} else if len(args) == 2 {
|
||||||
expr2, ok := args[1].(ast.Expr)
|
expr2, ok := args[1].(ast.Expr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(args[1], "invalid argument type: %T (expected ast.Expr)", args[1])
|
||||||
}
|
}
|
||||||
expr = expr2
|
expr = expr2
|
||||||
}
|
}
|
||||||
@ -56,25 +53,24 @@ func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
varName, ok := unwrap(args[0]).(ast.Ident)
|
varName, ok := unwrap(args[0]).(ast.Ident)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(args[0], "invalid argument type: %T (expected ast.Ident)", expr.First)
|
||||||
}
|
}
|
||||||
vars = append(vars, varName.Value)
|
vars = append(vars, varName.Value)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
varName, ok := unwrap(expr.First).(ast.Ident)
|
varName, ok := unwrap(expr.First).(ast.Ident)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(expr.First, "invalid argument type: %T (expected ast.Ident)", args[0])
|
||||||
}
|
}
|
||||||
vars = append(vars, varName.Value)
|
vars = append(vars, varName.Value)
|
||||||
|
|
||||||
if len(expr.Rest) != 1 {
|
if len(expr.Rest) != 1 {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(expr.First, "invalid expression (expected 1 element, got %d)", len(expr.Rest))
|
||||||
}
|
}
|
||||||
rest := expr.Rest[0]
|
rest := expr.Rest[0]
|
||||||
|
|
||||||
if rest.Operator.Value != "in" {
|
if rest.Operator.Value != "in" {
|
||||||
return ErrForTagInvalidArgs
|
return tc.PosError(expr.First, `invalid operator in expression (expected "in", got %q)`, rest.Operator.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := tc.GetValue(rest, nil)
|
val, err := tc.GetValue(rest, nil)
|
||||||
|
26
if_tag.go
26
if_tag.go
@ -18,27 +18,17 @@
|
|||||||
|
|
||||||
package salix
|
package salix
|
||||||
|
|
||||||
import (
|
import "go.elara.ws/salix/ast"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"go.elara.ws/salix/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrIfExpectsOneArg = errors.New("if tag expects one argument")
|
|
||||||
ErrIfExpectsBool = errors.New("if tag expects a bool value")
|
|
||||||
ErrIfTwoElse = errors.New("if tags can only have one else tag inside")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ifTag represents a #if tag within a Salix template
|
// ifTag represents a #if tag within a Salix template
|
||||||
type ifTag struct{}
|
type ifTag struct{}
|
||||||
|
|
||||||
func (it ifTag) Run(tc *TagContext, block, args []ast.Node) error {
|
func (it ifTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return ErrIfExpectsOneArg
|
return tc.PosError(tc.Tag, "expected one argument, got %d", len(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
inner, err := it.findInner(block)
|
inner, err := it.findInner(tc, block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -50,7 +40,7 @@ func (it ifTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
|
|
||||||
cond, ok := val.(bool)
|
cond, ok := val.(bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrIfExpectsBool
|
return tc.PosError(args[0], "expected boolean argument, got %T", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cond {
|
if cond {
|
||||||
@ -65,7 +55,7 @@ func (it ifTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
|
|
||||||
cond, ok := val.(bool)
|
cond, ok := val.(bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrIfExpectsBool
|
return tc.PosError(elifTag.value, "expected boolean argument, got %T", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
nextIndex := len(block)
|
nextIndex := len(block)
|
||||||
@ -102,7 +92,7 @@ type elif struct {
|
|||||||
|
|
||||||
// findInner finds the inner elif and else tags in a block
|
// findInner finds the inner elif and else tags in a block
|
||||||
// passed to the if tag.
|
// passed to the if tag.
|
||||||
func (it ifTag) findInner(block []ast.Node) (innerTags, error) {
|
func (it ifTag) findInner(tc *TagContext, block []ast.Node) (innerTags, error) {
|
||||||
var out innerTags
|
var out innerTags
|
||||||
for i, node := range block {
|
for i, node := range block {
|
||||||
if tag, ok := node.(ast.Tag); ok {
|
if tag, ok := node.(ast.Tag); ok {
|
||||||
@ -112,7 +102,7 @@ func (it ifTag) findInner(block []ast.Node) (innerTags, error) {
|
|||||||
out.endRoot = i
|
out.endRoot = i
|
||||||
}
|
}
|
||||||
if len(tag.Params) > 1 {
|
if len(tag.Params) > 1 {
|
||||||
return innerTags{}, ErrIfExpectsOneArg
|
return innerTags{}, tc.PosError(tag.Params[1], "expected one argument, got %d", len(tag.Params))
|
||||||
}
|
}
|
||||||
out.elifTags = append(out.elifTags, elif{
|
out.elifTags = append(out.elifTags, elif{
|
||||||
index: i,
|
index: i,
|
||||||
@ -120,7 +110,7 @@ func (it ifTag) findInner(block []ast.Node) (innerTags, error) {
|
|||||||
})
|
})
|
||||||
case "else":
|
case "else":
|
||||||
if out.elseIndex != 0 {
|
if out.elseIndex != 0 {
|
||||||
return innerTags{}, ErrIfTwoElse
|
return innerTags{}, tc.PosError(tag, "cannot have more than one else tag in an if tag")
|
||||||
}
|
}
|
||||||
if out.endRoot == 0 {
|
if out.endRoot == 0 {
|
||||||
out.endRoot = i
|
out.endRoot = i
|
||||||
|
@ -34,7 +34,7 @@ type includeTag struct{}
|
|||||||
|
|
||||||
func (it includeTag) Run(tc *TagContext, block, args []ast.Node) error {
|
func (it includeTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return ErrIncludeInvalidArgs
|
return tc.PosError(tc.Tag, "expected at least one argument, got %d", len(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := tc.GetValue(args[0], nil)
|
val, err := tc.GetValue(args[0], nil)
|
||||||
@ -44,12 +44,12 @@ func (it includeTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
|
|
||||||
name, ok := val.(string)
|
name, ok := val.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrIncludeInvalidArgs
|
return tc.PosError(args[0], "invalid first argument type: %T (expected string)", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, ok := tc.t.ns.GetTemplate(name)
|
tmpl, ok := tc.t.ns.GetTemplate(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrNoSuchTemplate
|
return tc.PosError(args[0], "no such template: %q", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
local := map[string]any{}
|
local := map[string]any{}
|
||||||
@ -64,8 +64,8 @@ func (it includeTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
}
|
}
|
||||||
local[a.Name.Value] = val
|
local[a.Name.Value] = val
|
||||||
} else {
|
} else {
|
||||||
// If the argument isn't an assigment, return invalid args
|
// If the argument isn't an assigment, return an error
|
||||||
return ErrIncludeInvalidArgs
|
return tc.PosError(tc.Tag, "invalid argument type: %T (expected ast.Assignment)", val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
macro_tag.go
19
macro_tag.go
@ -1,22 +1,13 @@
|
|||||||
package salix
|
package salix
|
||||||
|
|
||||||
import (
|
import "go.elara.ws/salix/ast"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"go.elara.ws/salix/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrMacroInvalidArgs = errors.New("macro expects one string argument followed by variable assignments")
|
|
||||||
ErrNoSuchMacro = errors.New("no such template")
|
|
||||||
)
|
|
||||||
|
|
||||||
// macroTag represents an #macro tag within a Salix template
|
// macroTag represents an #macro tag within a Salix template
|
||||||
type macroTag struct{}
|
type macroTag struct{}
|
||||||
|
|
||||||
func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
|
func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return ErrMacroInvalidArgs
|
return tc.PosError(tc.Tag, "expected at least one argument, got %d", len(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
nameVal, err := tc.GetValue(args[0], nil)
|
nameVal, err := tc.GetValue(args[0], nil)
|
||||||
@ -26,7 +17,7 @@ func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
|
|
||||||
name, ok := nameVal.(string)
|
name, ok := nameVal.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrMacroInvalidArgs
|
return tc.PosError(args[0], "invalid first argument type: %T (expected string)", nameVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(block) == 0 {
|
if len(block) == 0 {
|
||||||
@ -43,13 +34,13 @@ func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
local[a.Name.Value] = val
|
local[a.Name.Value] = val
|
||||||
} else {
|
} else {
|
||||||
// If the argument isn't an assigment, return invalid args
|
// If the argument isn't an assigment, return invalid args
|
||||||
return ErrMacroInvalidArgs
|
return tc.PosError(arg, "%s: invalid argument type: %T (expected ast.Assignment)", tc.NodeToString(arg), arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro, ok := tc.t.macros[name]
|
macro, ok := tc.t.macros[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrNoSuchMacro
|
return tc.PosError(tc.Tag, "no such macro: %q", name)
|
||||||
}
|
}
|
||||||
return tc.Execute(macro, local)
|
return tc.Execute(macro, local)
|
||||||
} else {
|
} else {
|
||||||
|
2
parse.go
2
parse.go
@ -44,7 +44,7 @@ func (n *Namespace) Parse(r NamedReader) (Template, error) {
|
|||||||
|
|
||||||
// ParseWithFilename parses a salix template from r, using the given name.
|
// ParseWithFilename parses a salix template from r, using the given name.
|
||||||
func (n *Namespace) ParseWithName(name string, r io.Reader) (Template, error) {
|
func (n *Namespace) ParseWithName(name string, r io.Reader) (Template, error) {
|
||||||
astVal, err := parser.ParseReader(name, r)
|
astVal, err := parser.ParseReader(name, r, parser.GlobalStore("name", name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Template{}, err
|
return Template{}, err
|
||||||
}
|
}
|
||||||
|
611
parser/parser.go
611
parser/parser.go
File diff suppressed because it is too large
Load Diff
@ -41,6 +41,7 @@ func toNodeSlice(v any) []ast.Node {
|
|||||||
|
|
||||||
func getPos(c *current) ast.Position {
|
func getPos(c *current) ast.Position {
|
||||||
return ast.Position{
|
return ast.Position{
|
||||||
|
Name: c.globalStore["name"].(string),
|
||||||
Line: c.pos.line,
|
Line: c.pos.line,
|
||||||
Col: c.pos.col,
|
Col: c.pos.col,
|
||||||
}
|
}
|
||||||
|
161
salix.go
161
salix.go
@ -25,30 +25,11 @@ import (
|
|||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"go.elara.ws/salix/ast"
|
"go.elara.ws/salix/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoSuchFunc = errors.New("no such function")
|
|
||||||
ErrNoSuchVar = errors.New("no such variable")
|
|
||||||
ErrNoSuchMethod = errors.New("no such method")
|
|
||||||
ErrNoSuchField = errors.New("no such field")
|
|
||||||
ErrNoSuchTag = errors.New("no such tag")
|
|
||||||
ErrNotOperatorNonBool = errors.New("not operator cannot be used on a non-bool value")
|
|
||||||
ErrParamNumMismatch = errors.New("incorrect parameter amount")
|
|
||||||
ErrIncorrectParamType = errors.New("incorrect parameter type for function")
|
|
||||||
ErrEndTagWithoutStart = errors.New("end tag without a start tag")
|
|
||||||
ErrIncorrectIndexType = errors.New("incorrect index type")
|
|
||||||
ErrIndexOutOfRange = errors.New("index out of range")
|
|
||||||
ErrMapIndexNotFound = errors.New("map index not found")
|
|
||||||
ErrMapInvalidIndexType = errors.New("invalid map index type")
|
|
||||||
ErrFuncTooManyReturns = errors.New("template functions can only have two return values")
|
|
||||||
ErrFuncNoReturns = errors.New("template functions must return at least one value")
|
|
||||||
ErrFuncSecondReturnType = errors.New("the second return value of a template function must be an error")
|
|
||||||
ErrTernaryCondBool = errors.New("ternary condition must be a boolean")
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTML represents unescaped HTML strings
|
// HTML represents unescaped HTML strings
|
||||||
type HTML string
|
type HTML string
|
||||||
|
|
||||||
@ -112,7 +93,7 @@ func (t *Template) execute(w io.Writer, nodes []ast.Node, local map[string]any)
|
|||||||
case ast.Text:
|
case ast.Text:
|
||||||
_, err := w.Write(node.Data)
|
_, err := w.Write(node.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return t.posError(node, "%w", err)
|
return ast.PosError(node, "%w", err)
|
||||||
}
|
}
|
||||||
case ast.Tag:
|
case ast.Tag:
|
||||||
newOffset, err := t.execTag(node, w, nodes, i, local)
|
newOffset, err := t.execTag(node, w, nodes, i, local)
|
||||||
@ -125,7 +106,7 @@ func (t *Template) execute(w io.Writer, nodes []ast.Node, local map[string]any)
|
|||||||
// should be taken care of by execTag, so if we do,
|
// should be taken care of by execTag, so if we do,
|
||||||
// return an error because execTag was never called,
|
// return an error because execTag was never called,
|
||||||
// which means there was no start tag.
|
// which means there was no start tag.
|
||||||
return ErrEndTagWithoutStart
|
return ast.PosError(node, "end tag without a matching start tag: %s", node.Name.Value)
|
||||||
case ast.ExprTag:
|
case ast.ExprTag:
|
||||||
v, err := t.getValue(node.Value, local)
|
v, err := t.getValue(node.Value, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -235,6 +216,76 @@ func (t *Template) getValue(node ast.Node, local map[string]any) (any, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// valueToString converts an AST node to a textual representation
|
||||||
|
// for the user to see, such as in error messages. This does not
|
||||||
|
// directly correlate to Salix source code.
|
||||||
|
func valueToString(node ast.Node) string {
|
||||||
|
if node == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node := node.(type) {
|
||||||
|
case ast.Ident:
|
||||||
|
return node.Value
|
||||||
|
case ast.String:
|
||||||
|
return strconv.Quote(node.Value)
|
||||||
|
case ast.Integer:
|
||||||
|
return strconv.FormatInt(node.Value, 10)
|
||||||
|
case ast.Float:
|
||||||
|
return strconv.FormatFloat(node.Value, 'g', -1, 64)
|
||||||
|
case ast.Bool:
|
||||||
|
return strconv.FormatBool(node.Value)
|
||||||
|
case ast.Assignment:
|
||||||
|
return node.Name.Value + " = " + valueToString(node.Value)
|
||||||
|
case ast.Index:
|
||||||
|
return valueToString(node.Value) + "[" + valueToString(node.Index) + "]"
|
||||||
|
case ast.Ternary:
|
||||||
|
return valueToString(node.Condition) + " ? " + valueToString(node.IfTrue) + " : " + valueToString(node.Else)
|
||||||
|
case ast.FieldAccess:
|
||||||
|
return valueToString(node.Value) + "." + node.Name.Value
|
||||||
|
case ast.Value:
|
||||||
|
if node.Not {
|
||||||
|
return "!" + valueToString(node.Node)
|
||||||
|
}
|
||||||
|
return valueToString(node.Node)
|
||||||
|
case ast.FuncCall:
|
||||||
|
if len(node.Params) > 1 {
|
||||||
|
return node.Name.Value + "(" + valueToString(node.Params[0]) + ", ...)"
|
||||||
|
} else if len(node.Params) == 1 {
|
||||||
|
return node.Name.Value + "(" + valueToString(node.Params[0]) + ")"
|
||||||
|
} else {
|
||||||
|
return node.Name.Value + "()"
|
||||||
|
}
|
||||||
|
case ast.MethodCall:
|
||||||
|
if len(node.Params) > 1 {
|
||||||
|
return valueToString(node.Value) + "." + node.Name.Value + "(" + valueToString(node.Params[0]) + ", ...)"
|
||||||
|
} else if len(node.Params) == 1 {
|
||||||
|
return valueToString(node.Value) + "." + node.Name.Value + "(" + valueToString(node.Params[0]) + ")"
|
||||||
|
} else {
|
||||||
|
return valueToString(node.Value) + "." + node.Name.Value + "()"
|
||||||
|
}
|
||||||
|
case ast.Expr:
|
||||||
|
if len(node.Rest) == 0 {
|
||||||
|
return valueToString(node.First)
|
||||||
|
}
|
||||||
|
return valueToString(node.First) + node.Rest[0].Operator.Value + valueToString(node.Rest[0])
|
||||||
|
case ast.Tag:
|
||||||
|
if len(node.Params) > 1 {
|
||||||
|
return "#" + node.Name.Value + "(" + valueToString(node.Params[0]) + ", ...)"
|
||||||
|
} else if len(node.Params) == 1 {
|
||||||
|
return "#" + node.Name.Value + "(" + valueToString(node.Params[0]) + ")"
|
||||||
|
} else {
|
||||||
|
return "#" + node.Name.Value + "()"
|
||||||
|
}
|
||||||
|
case ast.EndTag:
|
||||||
|
return "#" + node.Name.Value
|
||||||
|
case ast.ExprTag:
|
||||||
|
return "#(" + valueToString(node.Value) + ")"
|
||||||
|
default:
|
||||||
|
return "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// unwrapASTValue unwraps an ast.Value node into its underlying value
|
// unwrapASTValue unwraps an ast.Value node into its underlying value
|
||||||
func (t *Template) unwrapASTValue(node ast.Value, local map[string]any) (any, error) {
|
func (t *Template) unwrapASTValue(node ast.Value, local map[string]any) (any, error) {
|
||||||
v, err := t.getValue(node.Node, local)
|
v, err := t.getValue(node.Node, local)
|
||||||
@ -245,7 +296,7 @@ func (t *Template) unwrapASTValue(node ast.Value, local map[string]any) (any, er
|
|||||||
if node.Not {
|
if node.Not {
|
||||||
rval := reflect.ValueOf(v)
|
rval := reflect.ValueOf(v)
|
||||||
if rval.Kind() != reflect.Bool {
|
if rval.Kind() != reflect.Bool {
|
||||||
return nil, ErrNotOperatorNonBool
|
return nil, ast.PosError(node, "%s: the ! operator can only be used on boolean values", valueToString(node))
|
||||||
}
|
}
|
||||||
return !rval.Bool(), nil
|
return !rval.Bool(), nil
|
||||||
}
|
}
|
||||||
@ -279,7 +330,7 @@ func (t *Template) getVar(id ast.Ident, local map[string]any) (any, error) {
|
|||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return reflect.Value{}, t.posError(id, "%w: %s", ErrNoSuchVar, id.Value)
|
return reflect.Value{}, ast.PosError(id, "no such variable: %s", id.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) getTag(name string) (Tag, bool) {
|
func (t *Template) getTag(name string) (Tag, bool) {
|
||||||
@ -305,7 +356,7 @@ func (t *Template) getTag(name string) (Tag, bool) {
|
|||||||
func (t *Template) execTag(node ast.Tag, w io.Writer, nodes []ast.Node, i int, local map[string]any) (newOffset int, err error) {
|
func (t *Template) execTag(node ast.Tag, w io.Writer, nodes []ast.Node, i int, local map[string]any) (newOffset int, err error) {
|
||||||
tag, ok := t.getTag(node.Name.Value)
|
tag, ok := t.getTag(node.Name.Value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, t.posError(node, "%w: %s", ErrNoSuchTag, node.Name.Value)
|
return 0, ast.PosError(node, "no such tag: %s", node.Name.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var block []ast.Node
|
var block []ast.Node
|
||||||
@ -314,11 +365,11 @@ func (t *Template) execTag(node ast.Tag, w io.Writer, nodes []ast.Node, i int, l
|
|||||||
i += len(block) + 1
|
i += len(block) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
tc := &TagContext{w, t, local}
|
tc := &TagContext{node, w, t, local}
|
||||||
|
|
||||||
err = tag.Run(tc, block, node.Params)
|
err = tag.Run(tc, block, node.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, errors.Join(ast.PosError(node, "%s ->", valueToString(node)), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return i, nil
|
return i, nil
|
||||||
@ -328,7 +379,7 @@ func (t *Template) execTag(node ast.Tag, w io.Writer, nodes []ast.Node, i int, l
|
|||||||
func (t *Template) execFuncCall(fc ast.FuncCall, local map[string]any) (any, error) {
|
func (t *Template) execFuncCall(fc ast.FuncCall, local map[string]any) (any, error) {
|
||||||
fn, err := t.getVar(fc.Name, local)
|
fn, err := t.getVar(fc.Name, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, t.posError(fc, "%w: %s", ErrNoSuchFunc, fc.Name.Value)
|
return nil, ast.PosError(fc, "no such function: %s", fc.Name.Value)
|
||||||
}
|
}
|
||||||
return t.execFunc(reflect.ValueOf(fn), fc, fc.Params, local)
|
return t.execFunc(reflect.ValueOf(fn), fc, fc.Params, local)
|
||||||
}
|
}
|
||||||
@ -348,33 +399,34 @@ func (t *Template) getIndex(i ast.Index, local map[string]any) (any, error) {
|
|||||||
rval := reflect.ValueOf(val)
|
rval := reflect.ValueOf(val)
|
||||||
rindex := reflect.ValueOf(index)
|
rindex := reflect.ValueOf(index)
|
||||||
switch rval.Kind() {
|
switch rval.Kind() {
|
||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array, reflect.String:
|
||||||
intType := reflect.TypeOf(0)
|
intType := reflect.TypeOf(0)
|
||||||
if rindex.CanConvert(intType) {
|
if rindex.CanConvert(intType) {
|
||||||
rindex = rindex.Convert(intType)
|
rindex = rindex.Convert(intType)
|
||||||
} else {
|
} else {
|
||||||
return nil, ErrIncorrectIndexType
|
return nil, ast.PosError(i, "%s: invalid index type: %T", valueToString(i), index)
|
||||||
}
|
}
|
||||||
|
|
||||||
intIndex := rindex.Interface().(int)
|
intIndex := rindex.Interface().(int)
|
||||||
if intIndex < rval.Len() {
|
if intIndex < rval.Len() {
|
||||||
return rval.Index(intIndex).Interface(), nil
|
return rval.Index(intIndex).Interface(), nil
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(i, "%w: %d", ErrIndexOutOfRange, intIndex)
|
return nil, ast.PosError(i, "%s: index out of range: %d", valueToString(i), intIndex)
|
||||||
}
|
}
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if rindex.CanConvert(rval.Type().Key()) {
|
if rindex.CanConvert(rval.Type().Key()) {
|
||||||
rindex = rindex.Convert(rval.Type().Key())
|
rindex = rindex.Convert(rval.Type().Key())
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(i, "%w: %T (expected %s)", ErrMapInvalidIndexType, index, rval.Type().Key())
|
return nil, ast.PosError(i, "%s: invalid map index type: %T (expected %s)", valueToString(i), index, rval.Type().Key())
|
||||||
}
|
}
|
||||||
if out := rval.MapIndex(rindex); out.IsValid() {
|
if out := rval.MapIndex(rindex); out.IsValid() {
|
||||||
return out.Interface(), nil
|
return out.Interface(), nil
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(i, "%w: %q", ErrMapIndexNotFound, index)
|
return nil, ast.PosError(i, "%s: map index not found: %q", valueToString(i), index)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return nil, ast.PosError(i, "%s: cannot index type: %T", valueToString(i), val)
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getField tries to get a struct field from the underlying value
|
// getField tries to get a struct field from the underlying value
|
||||||
@ -389,7 +441,7 @@ func (t *Template) getField(fa ast.FieldAccess, local map[string]any) (any, erro
|
|||||||
}
|
}
|
||||||
field := rval.FieldByName(fa.Name.Value)
|
field := rval.FieldByName(fa.Name.Value)
|
||||||
if !field.IsValid() {
|
if !field.IsValid() {
|
||||||
return nil, t.posError(fa, "%w: %s", ErrNoSuchField, fa.Name.Value)
|
return nil, ast.PosError(fa, "%s: no such field: %s", valueToString(fa), fa.Name.Value)
|
||||||
}
|
}
|
||||||
return field.Interface(), nil
|
return field.Interface(), nil
|
||||||
}
|
}
|
||||||
@ -406,7 +458,7 @@ func (t *Template) execMethodCall(mc ast.MethodCall, local map[string]any) (any,
|
|||||||
}
|
}
|
||||||
mtd := rval.MethodByName(mc.Name.Value)
|
mtd := rval.MethodByName(mc.Name.Value)
|
||||||
if !mtd.IsValid() {
|
if !mtd.IsValid() {
|
||||||
return nil, t.posError(mc, "%w: %s", ErrNoSuchMethod, mc.Name.Value)
|
return nil, ast.PosError(mc, "no such method: %s", mc.Name.Value)
|
||||||
}
|
}
|
||||||
return t.execFunc(mtd, mc, mc.Params, local)
|
return t.execFunc(mtd, mc, mc.Params, local)
|
||||||
}
|
}
|
||||||
@ -415,17 +467,17 @@ func (t *Template) execMethodCall(mc ast.MethodCall, local map[string]any) (any,
|
|||||||
func (t *Template) execFunc(fn reflect.Value, node ast.Node, args []ast.Node, local map[string]any) (any, error) {
|
func (t *Template) execFunc(fn reflect.Value, node ast.Node, args []ast.Node, local map[string]any) (any, error) {
|
||||||
fnType := fn.Type()
|
fnType := fn.Type()
|
||||||
if fnType.NumIn() != len(args) {
|
if fnType.NumIn() != len(args) {
|
||||||
return nil, t.posError(node, "%w: %d (expected %d)", ErrParamNumMismatch, len(args), fnType.NumIn())
|
return nil, ast.PosError(node, "%s: invalid parameter amount: %d (expected %d)", valueToString(node), len(args), fnType.NumIn())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateFunc(fnType); err != nil {
|
if err := validateFunc(fnType, node); err != nil {
|
||||||
return nil, t.posError(node, "%w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
params := make([]reflect.Value, fnType.NumIn())
|
params := make([]reflect.Value, fnType.NumIn())
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
if _, ok := arg.(ast.Assignment); ok {
|
if _, ok := arg.(ast.Assignment); ok {
|
||||||
return nil, t.posError(arg, "assignment cannot be used as a function argument")
|
return nil, ast.PosError(arg, "%s: an assignment cannot be used as a function argument", valueToString(node))
|
||||||
}
|
}
|
||||||
paramVal, err := t.getValue(arg, local)
|
paramVal, err := t.getValue(arg, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -435,7 +487,7 @@ func (t *Template) execFunc(fn reflect.Value, node ast.Node, args []ast.Node, lo
|
|||||||
if params[i].CanConvert(fnType.In(i)) {
|
if params[i].CanConvert(fnType.In(i)) {
|
||||||
params[i] = params[i].Convert(fnType.In(i))
|
params[i] = params[i].Convert(fnType.In(i))
|
||||||
} else {
|
} else {
|
||||||
return nil, t.posError(node, "%w", ErrIncorrectParamType)
|
return nil, ast.PosError(node, "%s: invalid parameter type: %T (expected %s)", valueToString(node), paramVal, fnType.In(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,23 +495,26 @@ func (t *Template) execFunc(fn reflect.Value, node ast.Node, args []ast.Node, lo
|
|||||||
if len(ret) == 1 {
|
if len(ret) == 1 {
|
||||||
retv := ret[0].Interface()
|
retv := ret[0].Interface()
|
||||||
if err, ok := retv.(error); ok {
|
if err, ok := retv.(error); ok {
|
||||||
return nil, err
|
return nil, ast.PosError(node, "%s: %w", valueToString(node), err)
|
||||||
}
|
}
|
||||||
return ret[0].Interface(), nil
|
return ret[0].Interface(), nil
|
||||||
} else {
|
} else {
|
||||||
return ret[0].Interface(), ret[1].Interface().(error)
|
if ret[1].IsNil() {
|
||||||
|
return ret[0].Interface(), nil
|
||||||
|
}
|
||||||
|
return ret[0].Interface(), ast.PosError(node, "%s: %w", valueToString(node), ret[1].Interface().(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) evalTernary(tr ast.Ternary, local map[string]any) (any, error) {
|
func (t *Template) evalTernary(tr ast.Ternary, local map[string]any) (any, error) {
|
||||||
condVal, err := t.getValue(tr.Condition, local)
|
condVal, err := t.getValue(tr.Condition, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, t.posError(tr.Condition, "%w", ErrTernaryCondBool)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cond, ok := condVal.(bool)
|
cond, ok := condVal.(bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("ternary condition must be a boolean")
|
return nil, ast.PosError(tr.Condition, "%s: ternary condition must be a boolean value", valueToString(tr.Condition))
|
||||||
}
|
}
|
||||||
|
|
||||||
if cond {
|
if cond {
|
||||||
@ -486,21 +541,17 @@ func (t *Template) handleAssignment(a ast.Assignment, local map[string]any) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) posError(n ast.Node, format string, v ...any) error {
|
func validateFunc(t reflect.Type, node ast.Node) error {
|
||||||
return ast.PosError(n, t.name, format, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFunc(t reflect.Type) error {
|
|
||||||
numOut := t.NumOut()
|
numOut := t.NumOut()
|
||||||
if numOut > 2 {
|
if numOut > 2 {
|
||||||
return ErrFuncTooManyReturns
|
return ast.PosError(node, "template functions cannot have more than two return values")
|
||||||
} else if numOut == 0 {
|
} else if numOut == 0 {
|
||||||
return ErrFuncNoReturns
|
return ast.PosError(node, "template functions must have at least one return value")
|
||||||
}
|
}
|
||||||
|
|
||||||
if numOut == 2 {
|
if numOut == 2 {
|
||||||
if !t.Out(1).Implements(reflect.TypeOf(error(nil))) {
|
errType := reflect.TypeOf((*error)(nil)).Elem()
|
||||||
return ErrFuncSecondReturnType
|
if !t.Out(1).Implements(errType) {
|
||||||
|
return ast.PosError(node, "the second return value of a template function must be an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
tags.go
13
tags.go
@ -39,6 +39,7 @@ var globalTags = map[string]Tag{
|
|||||||
|
|
||||||
// TagContext is passed to Tag implementations to allow them to control the interpreter
|
// TagContext is passed to Tag implementations to allow them to control the interpreter
|
||||||
type TagContext struct {
|
type TagContext struct {
|
||||||
|
Tag ast.Tag
|
||||||
w io.Writer
|
w io.Writer
|
||||||
t *Template
|
t *Template
|
||||||
local map[string]any
|
local map[string]any
|
||||||
@ -65,6 +66,18 @@ func (tc *TagContext) GetValue(node ast.Node, local map[string]any) (any, error)
|
|||||||
return tc.t.getValue(node, mergeMap(tc.local, local))
|
return tc.t.getValue(node, mergeMap(tc.local, local))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PosError returns an error with the file position prepended. This should be used
|
||||||
|
// for errors wherever possible, to make it easier for users to find errors.
|
||||||
|
func (tc *TagContext) PosError(node ast.Node, format string, v ...any) error {
|
||||||
|
return ast.PosError(node, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeToString returns a textual representation of the given AST node for users to see,
|
||||||
|
// such as in error messages. This does not directly correlate to Salix source code.
|
||||||
|
func (tc *TagContext) NodeToString(node ast.Node) string {
|
||||||
|
return valueToString(node)
|
||||||
|
}
|
||||||
|
|
||||||
// Write writes b to the underlying writer. It implements
|
// Write writes b to the underlying writer. It implements
|
||||||
// the io.Writer interface.
|
// the io.Writer interface.
|
||||||
func (tc *TagContext) Write(b []byte) (int, error) {
|
func (tc *TagContext) Write(b []byte) (int, error) {
|
||||||
|
7
vars.go
7
vars.go
@ -19,6 +19,7 @@
|
|||||||
package salix
|
package salix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -38,12 +39,12 @@ var globalVars = map[string]any{
|
|||||||
"join": strings.Join,
|
"join": strings.Join,
|
||||||
}
|
}
|
||||||
|
|
||||||
func tmplLen(v any) int {
|
func tmplLen(v any) (int, error) {
|
||||||
val := reflect.ValueOf(v)
|
val := reflect.ValueOf(v)
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
case reflect.Array, reflect.Slice, reflect.String, reflect.Map:
|
case reflect.Array, reflect.Slice, reflect.String, reflect.Map:
|
||||||
return val.Len()
|
return val.Len(), nil
|
||||||
default:
|
default:
|
||||||
return -1
|
return 0, fmt.Errorf("cannot get length of %T", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user