Compare commits
26 Commits
fc6e1744bd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cbbaee47d4 | |||
| bcb8222ebf | |||
| 6d260619b7 | |||
| 944663c2b1 | |||
| c5470a45dd | |||
| 849295bb5f | |||
| d5c33f9e5d | |||
| f1a998c25b | |||
| 167f448ae1 | |||
| bb9b6c8128 | |||
| e20fedbafc | |||
| b8d3314ada | |||
| b4bc463326 | |||
| 314b0af456 | |||
| 094c48c4d1 | |||
| c28df761d1 | |||
| ea7b9f8da9 | |||
| 110f050c1c | |||
| 0040828d2d | |||
| ed568f81bf | |||
| e810247f13 | |||
| d91eef09bf | |||
| 4501ef63d6 | |||
| 89f75f1709 | |||
| eacbd633bd | |||
| 82001061da |
@@ -99,7 +99,7 @@ In this example:
|
||||
|
||||
### `for` tag
|
||||
|
||||
Salix's `for` tag is used for iterating over slices, arrays, and maps. It can assign one or two variables depending on your needs. When using a single variable, it sets that variable to the current element in the case of slices or arrays, or the current value for maps. With two variables, it assigns the first to the index (in the case of slices or arrays) or the key (for maps), and the second to the element or value, respectively. Here's an example of the for tag in action:
|
||||
Salix's `for` tag is used for iterating over iterable variables, such as slices, maps, iterator functions, etc. It can assign one or two variables depending on your needs. When using a single variable, it sets that variable to the current element in the case of slices or arrays, or the current value for maps. With two variables, it assigns the first to the index (in the case of slices or arrays) or the key (for maps), and the second to the element or value, respectively. Here's an example of the for tag in action:
|
||||
|
||||
```
|
||||
#for(id, name in users):
|
||||
@@ -129,6 +129,8 @@ The include tag allows you to import content from other templates in the namespa
|
||||
#include("header.html")
|
||||
```
|
||||
|
||||
If the file name starts with a question mark, nonexistent files will be ignored.
|
||||
|
||||
#### Using the `include` tag with extra arguments
|
||||
|
||||
The `include` tag can accept extra local variables as arguments. Here's an example with a `title` variable:
|
||||
@@ -153,6 +155,8 @@ The macro tag is a powerful feature that allows you to define reusable template
|
||||
|
||||
When a macro tag has a block, it sets the macro's content. When it doesn't, it inserts the contents of the macro. In the above example, a macro is defined and then inserted.
|
||||
|
||||
If the macro name starts with a question mark, nonexistent macros will be ignored.
|
||||
|
||||
#### Using the `macro` tag with extra arguments
|
||||
|
||||
Similar to the `include` tag, the `macro` tag can accept extra local variables as arguments. You can define these variables when including the macro. Here's an example:
|
||||
@@ -170,6 +174,7 @@ Functions used in a template can accept any number of arguments but are limited
|
||||
Salix includes several useful global functions in all templates:
|
||||
|
||||
- `len(v any) int`: Returns the length of the value passed in. If the length can't be found for the value passed in, it returns an error.
|
||||
- `json(v any) string`: Returns a JSON string for the value passed in.
|
||||
- `toUpper(s string) string`: Returns `s`, but with all characters mapped to their uppercase equivalents.
|
||||
- `toLower(s string) string`: Returns `s`, but with all characters mapped to their lowercase equivalents.
|
||||
- `hasPrefix(s, prefix string) bool`: Returns true if `s` starts with `prefix`.
|
||||
@@ -181,6 +186,8 @@ Salix includes several useful global functions in all templates:
|
||||
- `count(s, substr string) int`: Returns the amount of times that `substr` appears in `s`.
|
||||
- `split(s, sep string) []string`: Returns a slice containing all substrings separated by `sep`.
|
||||
- `join(ss []string, sep string) string`: Returns a string with all substrings in `ss` joined by `sep`.
|
||||
- `replace(s, old, new string, n int)`: Returns a string with `n` occurrences of `old` in `s` replaced with `new`.
|
||||
- `replaceAll(s, old, new string)`: Returns a string with all occurrences of `old` in `s` replaced with `new`.
|
||||
|
||||
### Adding Custom Functions
|
||||
|
||||
|
||||
26
ast/ast.go
26
ast/ast.go
@@ -20,6 +20,14 @@ func (p Position) String() string {
|
||||
return fmt.Sprintf("%s: line %d, col %d", p.Name, p.Line, p.Col)
|
||||
}
|
||||
|
||||
type Nil struct {
|
||||
Position Position
|
||||
}
|
||||
|
||||
func (n Nil) Pos() Position {
|
||||
return n.Position
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Name Ident
|
||||
Params []Node
|
||||
@@ -64,6 +72,24 @@ type Value struct {
|
||||
Not bool
|
||||
}
|
||||
|
||||
type Map struct {
|
||||
Map map[Node]Node
|
||||
Position Position
|
||||
}
|
||||
|
||||
func (m Map) Pos() Position {
|
||||
return m.Position
|
||||
}
|
||||
|
||||
type Array struct {
|
||||
Array []Node
|
||||
Position Position
|
||||
}
|
||||
|
||||
func (a Array) Pos() Position {
|
||||
return a.Position
|
||||
}
|
||||
|
||||
type Expr struct {
|
||||
First Node
|
||||
Operator Operator
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<head>
|
||||
<title>#(title)</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
</head>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css">
|
||||
</head>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>#(title)</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>#(page.Title)</title>
|
||||
</head>
|
||||
<body>
|
||||
#for(i, user in users):
|
||||
<div>
|
||||
<h2>#(toLower(user.Name))</h2>
|
||||
<p>User ID: #(i)</p>
|
||||
#if(user.LoggedIn): <p>This user is logged in</p> #!if
|
||||
#if(user.IsAdmin): <p>This user is an admin!</p> #!if
|
||||
<p>Registered: #(user.RegisteredTime.Format("01-02-2006"))</p>
|
||||
</div>
|
||||
#!for
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<title>#(page.Title)</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
#for(i, user in users):
|
||||
<div>
|
||||
<h2>#(toLower(user.Name))</h2>
|
||||
<p>User ID: #(i)</p>
|
||||
#if(user.LoggedIn): <p>This user is logged in</p> #!if
|
||||
#if(user.IsAdmin): <p>This user is an admin!</p> #!if
|
||||
<p>Registered: #(user.RegisteredTime.Format("01-02-2006"))</p>
|
||||
</div>
|
||||
#!for
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
74
expr.go
74
expr.go
@@ -32,28 +32,14 @@ func (t *Template) evalExpr(expr ast.Expr, local map[string]any) (any, error) {
|
||||
return a.Interface(), nil
|
||||
}
|
||||
|
||||
func (t *Template) performOp(a, b reflect.Value, op ast.Operator) (any, error) {
|
||||
func (t *Template) performOp(a, b reflect.Value, op ast.Operator) (result any, err error) {
|
||||
if op.Value == "in" {
|
||||
switch b.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
if a.CanConvert(b.Type().Elem()) {
|
||||
a = a.Convert(b.Type().Elem())
|
||||
} else {
|
||||
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
case reflect.Map:
|
||||
if a.CanConvert(b.Type().Key()) {
|
||||
a = a.Convert(b.Type().Key())
|
||||
} else {
|
||||
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
case reflect.String:
|
||||
if a.Kind() != reflect.String {
|
||||
return nil, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
default:
|
||||
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())
|
||||
a, b, err = handleIn(op, a, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !a.IsValid() || !b.IsValid() {
|
||||
return handleNil(op, a, b)
|
||||
} else if b.CanConvert(a.Type()) {
|
||||
b = b.Convert(a.Type())
|
||||
} else {
|
||||
@@ -174,3 +160,51 @@ func (t *Template) performOp(a, b reflect.Value, op ast.Operator) (any, error) {
|
||||
}
|
||||
return false, ast.PosError(op, "unknown operator: %q", op.Value)
|
||||
}
|
||||
|
||||
func handleIn(op ast.Operator, a, b reflect.Value) (c, d reflect.Value, err error) {
|
||||
switch b.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
if a.CanConvert(b.Type().Elem()) {
|
||||
a = a.Convert(b.Type().Elem())
|
||||
} else {
|
||||
return a, b, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
case reflect.Map:
|
||||
if a.CanConvert(b.Type().Key()) {
|
||||
a = a.Convert(b.Type().Key())
|
||||
} else {
|
||||
return a, b, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
case reflect.String:
|
||||
if a.Kind() != reflect.String {
|
||||
return a, b, ast.PosError(op, "mismatched types in expression (%s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
default:
|
||||
return a, b, ast.PosError(op, "the in operator can only be used on strings, arrays, and slices (got %s and %s)", a.Type(), b.Type())
|
||||
}
|
||||
return a, b, nil
|
||||
}
|
||||
|
||||
func handleNil(op ast.Operator, a, b reflect.Value) (any, error) {
|
||||
if !a.IsValid() && !b.IsValid() {
|
||||
return true, nil
|
||||
} else if !a.IsValid() {
|
||||
return nil, ast.PosError(op, "nil must be on the right side of an expression")
|
||||
} else if !b.IsValid() {
|
||||
if op.Value != "==" && op.Value != "!=" {
|
||||
return nil, ast.PosError(op, "invalid operator for nil value (expected == or !=, got %s)", op.Value)
|
||||
}
|
||||
|
||||
switch a.Kind() {
|
||||
case reflect.Chan, reflect.Slice, reflect.Map, reflect.Func, reflect.Interface, reflect.Pointer:
|
||||
if op.Value == "==" {
|
||||
return a.IsNil(), nil
|
||||
} else {
|
||||
return !a.IsNil(), nil
|
||||
}
|
||||
default:
|
||||
return nil, ast.PosError(op, "values of type %s cannot be compared against nil", a.Type())
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
81
for_tag.go
81
for_tag.go
@@ -1,6 +1,7 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"go.elara.ws/salix/ast"
|
||||
@@ -10,34 +11,26 @@ import (
|
||||
type forTag struct{}
|
||||
|
||||
func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
if len(args) == 0 || len(args) > 2 {
|
||||
if len(args) == 0 || len(args) > 3 {
|
||||
return tc.PosError(tc.Tag, "invalid argument amount")
|
||||
}
|
||||
|
||||
var expr ast.Expr
|
||||
if len(args) == 1 {
|
||||
expr2, ok := args[0].(ast.Expr)
|
||||
if !ok {
|
||||
return tc.PosError(args[0], "invalid argument type: %T (expected ast.Expr)", args[0])
|
||||
}
|
||||
expr = expr2
|
||||
} else if len(args) == 2 {
|
||||
expr2, ok := args[1].(ast.Expr)
|
||||
if !ok {
|
||||
return tc.PosError(args[1], "invalid argument type: %T (expected ast.Expr)", args[1])
|
||||
}
|
||||
expr = expr2
|
||||
expr, ok := args[len(args)-1].(ast.Expr)
|
||||
if !ok {
|
||||
return tc.PosError(args[0], "invalid argument type: %T (expected ast.Expr)", args[0])
|
||||
}
|
||||
|
||||
var vars []string
|
||||
var in reflect.Value
|
||||
|
||||
if len(args) == 2 {
|
||||
varName, ok := unwrap(args[0]).(ast.Ident)
|
||||
if !ok {
|
||||
return tc.PosError(args[0], "invalid argument type: %T (expected ast.Ident)", expr.First)
|
||||
if len(args) > 1 {
|
||||
for _, arg := range args[:len(args)-1] {
|
||||
varName, ok := unwrap(arg).(ast.Ident)
|
||||
if !ok {
|
||||
return tc.PosError(arg, "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)
|
||||
@@ -62,6 +55,15 @@ func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
in = reflect.ValueOf(val)
|
||||
|
||||
switch in.Kind() {
|
||||
case reflect.Int:
|
||||
local := map[string]any{}
|
||||
for i := range in.Int() {
|
||||
local[vars[0]] = i
|
||||
err = tc.Execute(block, local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
local := map[string]any{}
|
||||
for i := 0; i < in.Len(); i++ {
|
||||
@@ -70,6 +72,8 @@ func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
} else if len(vars) == 2 {
|
||||
local[vars[0]] = i
|
||||
local[vars[1]] = in.Index(i).Interface()
|
||||
} else {
|
||||
return errors.New("slices and arrays can only use two for loop variables")
|
||||
}
|
||||
|
||||
err = tc.Execute(block, local)
|
||||
@@ -80,18 +84,57 @@ func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
case reflect.Map:
|
||||
local := map[string]any{}
|
||||
iter := in.MapRange()
|
||||
i := 0
|
||||
for iter.Next() {
|
||||
if len(vars) == 1 {
|
||||
local[vars[0]] = iter.Value().Interface()
|
||||
} else if len(vars) == 2 {
|
||||
local[vars[0]] = iter.Key().Interface()
|
||||
local[vars[1]] = iter.Value().Interface()
|
||||
} else if len(vars) == 3 {
|
||||
local[vars[0]] = i
|
||||
local[vars[1]] = iter.Key().Interface()
|
||||
local[vars[2]] = iter.Value().Interface()
|
||||
}
|
||||
|
||||
err = tc.Execute(block, local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
case reflect.Func:
|
||||
local := map[string]any{}
|
||||
i := 0
|
||||
if len(vars) == 1 {
|
||||
for val := range in.Seq() {
|
||||
local[vars[0]] = val.Interface()
|
||||
err = tc.Execute(block, local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if len(vars) == 2 {
|
||||
for val1, val2 := range in.Seq2() {
|
||||
local[vars[0]] = val1.Interface()
|
||||
local[vars[1]] = val2.Interface()
|
||||
err = tc.Execute(block, local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for val1, val2 := range in.Seq2() {
|
||||
local[vars[0]] = i
|
||||
local[vars[1]] = val1.Interface()
|
||||
local[vars[2]] = val2.Interface()
|
||||
err = tc.Execute(block, local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
48
fs_tag.go
Normal file
48
fs_tag.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
"go.elara.ws/salix/ast"
|
||||
)
|
||||
|
||||
// FSTag writes files from an fs.FS to a template
|
||||
//
|
||||
// No escaping is done on the files, so make sure to avoid user-generated data.
|
||||
type FSTag struct {
|
||||
// FS is the filesystem that files will be loaded from.
|
||||
FS fs.FS
|
||||
|
||||
// PathPrefix is joined to the path string before a file is read.
|
||||
PathPrefix string
|
||||
|
||||
// Extension is appended to the end of the path string before a file is read.
|
||||
Extension string
|
||||
}
|
||||
|
||||
func (ft FSTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
if len(args) != 1 {
|
||||
return tc.PosError(tc.Tag, "expected one argument, got %d", len(args))
|
||||
}
|
||||
|
||||
pathVal, err := tc.GetValue(args[0], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pathStr, ok := pathVal.(string)
|
||||
if !ok {
|
||||
return tc.PosError(args[0], "expected string argument, got %T", pathVal)
|
||||
}
|
||||
|
||||
fl, err := ft.FS.Open(path.Join(ft.PathPrefix, pathStr) + ft.Extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fl.Close()
|
||||
|
||||
_, err = io.Copy(tc, fl)
|
||||
return err
|
||||
}
|
||||
25
func_test.go
25
func_test.go
@@ -2,6 +2,8 @@ package salix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -29,8 +31,6 @@ func TestFuncCall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: COMMIT THIS!!!
|
||||
|
||||
func TestFuncCallInvalidParamCount(t *testing.T) {
|
||||
fn := func() int { return 0 }
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestFuncCallVariadic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFuncCallError(t *testing.T) {
|
||||
var expectedErr = errors.New("expected error")
|
||||
expectedErr := errors.New("expected error")
|
||||
fn := func() error { return expectedErr }
|
||||
|
||||
// test()
|
||||
@@ -133,7 +133,7 @@ func TestFuncCallMultiReturn(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFuncCallMultiReturnError(t *testing.T) {
|
||||
var expectedErr = errors.New("expected error")
|
||||
expectedErr := errors.New("expected error")
|
||||
fn := func() (string, error) { return "", expectedErr }
|
||||
|
||||
// test()
|
||||
@@ -199,3 +199,20 @@ func TestFuncCallAssignment(t *testing.T) {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFunc(t *testing.T) {
|
||||
testCases := []reflect.Type{
|
||||
reflect.TypeFor[func()](), // Template functions must return at least one value
|
||||
reflect.TypeFor[func() (x, y int)](), // Second return value must be an error
|
||||
reflect.TypeFor[func() (x, y, z int)](), // Template functions cannot have more than two return values
|
||||
}
|
||||
|
||||
for index, testCase := range testCases {
|
||||
t.Run(fmt.Sprint(index), func(t *testing.T) {
|
||||
err := validateFunc(testCase, ast.Bool{Value: true, Position: testPos(t)})
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,17 @@ func (it includeTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
return tc.PosError(args[0], "invalid first argument type: %T (expected string)", val)
|
||||
}
|
||||
|
||||
ignoreMissing := false
|
||||
if name[0] == '?' {
|
||||
name = name[1:]
|
||||
ignoreMissing = true
|
||||
}
|
||||
|
||||
tmpl, ok := tc.t.ns.GetTemplate(name)
|
||||
if !ok {
|
||||
if ignoreMissing {
|
||||
return nil
|
||||
}
|
||||
return tc.PosError(args[0], "no such template: %q", name)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
return tc.PosError(args[0], "invalid first argument type: %T (expected string)", nameVal)
|
||||
}
|
||||
|
||||
ignoreMissing := false
|
||||
if name[0] == '?' {
|
||||
name = name[1:]
|
||||
ignoreMissing = true
|
||||
}
|
||||
|
||||
if len(block) == 0 {
|
||||
local := map[string]any{}
|
||||
|
||||
@@ -40,6 +46,9 @@ func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
|
||||
macro, ok := tc.t.macros[name]
|
||||
if !ok {
|
||||
if ignoreMissing {
|
||||
return nil
|
||||
}
|
||||
return tc.PosError(tc.Tag, "no such macro: %q", name)
|
||||
}
|
||||
return tc.Execute(macro, local)
|
||||
|
||||
13
namespace.go
13
namespace.go
@@ -20,7 +20,10 @@ type Namespace struct {
|
||||
// WriteOnSuccess indicates whether the output should only be written if generation fully succeeds.
|
||||
// This option buffers the output of the template, so it will use more memory. (default: false)
|
||||
WriteOnSuccess bool
|
||||
escapeHTML *bool
|
||||
// NilToZero indictes whether nil pointer values should be converted to zero values of their underlying
|
||||
// types.
|
||||
NilToZero bool
|
||||
escapeHTML *bool
|
||||
}
|
||||
|
||||
// New returns a new template namespace
|
||||
@@ -86,6 +89,14 @@ func (n *Namespace) WithWhitespaceMutations(b bool) *Namespace {
|
||||
return n
|
||||
}
|
||||
|
||||
// WithNilToZero enables or disables conversion of nil values to zero values for the namespace
|
||||
func (n *Namespace) WithNilToZero(b bool) *Namespace {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
n.NilToZero = true
|
||||
return n
|
||||
}
|
||||
|
||||
// GetTemplate tries to get a template from the namespace's template map.
|
||||
// If it finds the template, it returns the template and true. If it
|
||||
// doesn't find it, it returns nil and false.
|
||||
|
||||
1193
parser/parser.go
1193
parser/parser.go
File diff suppressed because it is too large
Load Diff
@@ -83,8 +83,21 @@ ExprTag = '#' ignoreErr:'?'? '(' item:Expr ')' {
|
||||
}, nil
|
||||
}
|
||||
|
||||
Expr = Ternary / Assignment / LogicalExpr
|
||||
Assignable = Ternary / LogicalExpr
|
||||
Expr = Assignment / TernaryExpr
|
||||
Assignable = TernaryExpr
|
||||
|
||||
TernaryExpr = _ cond:LogicalExpr vals:(_ '?' _ Value _ ':' _ Value)? {
|
||||
if vals == nil {
|
||||
return cond, nil
|
||||
} else {
|
||||
s := toAnySlice(vals)
|
||||
return ast.Ternary{
|
||||
Condition: cond.(ast.Node),
|
||||
IfTrue: s[3].(ast.Node),
|
||||
Else: s[7].(ast.Node),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
LogicalExpr = _ first:ComparisonExpr rest:(_ LogicalOp _ ComparisonExpr)* _ {
|
||||
return toExpr(c, first, rest), nil
|
||||
@@ -116,13 +129,49 @@ ParamList = '(' params:(Expr ( ',' _ Expr )* )? ')' {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Value = not:"!"? node:(MethodCall / FieldAccess / Index / String / RawString / Float / Integer / Bool / FuncCall / VariableOr / Ident / ParenExpr) {
|
||||
Value = not:"!"? node:(Nil / MethodCall / FieldAccess / Index / String / RawString / Float / Integer / Bool / FuncCall / VariableOr / Ident / ParenExpr / Array / Map) {
|
||||
return ast.Value{
|
||||
Node: node.(ast.Node),
|
||||
Not: not != nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Map = '{' _ fpair:(Assignable _ ':' _ Assignable)? _ pairs:(',' _ Assignable _ ':' _ Assignable _)* _ ','? _ '}' {
|
||||
out := ast.Map{
|
||||
Map: map[ast.Node]ast.Node{},
|
||||
Position: getPos(c),
|
||||
}
|
||||
|
||||
fpairSlice := toAnySlice(fpair)
|
||||
if fpairSlice == nil {
|
||||
return out, nil
|
||||
} else {
|
||||
out.Map[fpairSlice[0].(ast.Node)] = fpairSlice[4].(ast.Node)
|
||||
for _, pair := range toAnySlice(pairs) {
|
||||
pairSlice := toAnySlice(pair)
|
||||
out.Map[pairSlice[2].(ast.Node)] = pairSlice[6].(ast.Node)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Array = '[' _ fval:Assignable? _ vals:(',' _ Assignable _)* ','? _ ']' {
|
||||
out := ast.Array{Position: getPos(c)}
|
||||
|
||||
if fval == nil {
|
||||
return out, nil
|
||||
} else {
|
||||
out.Array = append(out.Array, fval.(ast.Node))
|
||||
for _, val := range toAnySlice(vals) {
|
||||
valSlice := toAnySlice(val)
|
||||
out.Array = append(out.Array, valSlice[2].(ast.Node))
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
VariableOr = variable:Ident _ '|' _ or:Assignable {
|
||||
return ast.VariableOr{
|
||||
Variable: variable.(ast.Ident),
|
||||
@@ -138,14 +187,6 @@ Assignment = name:Ident _ '=' _ value:Assignable {
|
||||
}, nil
|
||||
}
|
||||
|
||||
Ternary = cond:Assignable _ '?' _ ifTrue:Value _ ':' _ elseVal:Value {
|
||||
return ast.Ternary{
|
||||
Condition: cond.(ast.Node),
|
||||
IfTrue: ifTrue.(ast.Node),
|
||||
Else: elseVal.(ast.Node),
|
||||
}, nil
|
||||
}
|
||||
|
||||
MethodCall = value:Value '.' name:Ident params:ParamList {
|
||||
return ast.MethodCall{
|
||||
Value: value.(ast.Node),
|
||||
@@ -247,6 +288,10 @@ ArithmeticOp = ('+' / '-' / '/' / '*' / '%') {
|
||||
}, nil
|
||||
}
|
||||
|
||||
Nil = "nil" {
|
||||
return ast.Nil{Position: getPos(c)}, nil
|
||||
}
|
||||
|
||||
Text = . [^#]* { return ast.Text{Data: c.text, Position: getPos(c)}, nil }
|
||||
|
||||
_ "whitespace" ← [ \t\r\n]*
|
||||
|
||||
114
salix.go
114
salix.go
@@ -26,6 +26,7 @@ type Template struct {
|
||||
// WriteOnSuccess indicates whether the output should only be written if generation fully succeeds.
|
||||
// This option buffers the output of the template, so it will use more memory. (default: false)
|
||||
WriteOnSuccess bool
|
||||
NilToZero bool
|
||||
|
||||
tags map[string]Tag
|
||||
vars map[string]any
|
||||
@@ -66,6 +67,12 @@ func (t Template) WithWriteOnSuccess(b bool) Template {
|
||||
return t
|
||||
}
|
||||
|
||||
// WithNilToZero enables or disables conversion of nil values to zero values.
|
||||
func (t Template) WithNilToZero(b bool) Template {
|
||||
t.NilToZero = true
|
||||
return t
|
||||
}
|
||||
|
||||
// Execute executes a parsed template and writes
|
||||
// the result to w.
|
||||
func (t Template) Execute(w io.Writer) error {
|
||||
@@ -147,6 +154,10 @@ func (t *Template) getEscapeHTML() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Template) getNilToZero() bool {
|
||||
return t.NilToZero || t.ns.NilToZero
|
||||
}
|
||||
|
||||
func (t *Template) toString(v any) string {
|
||||
if h, ok := v.(HTML); ok {
|
||||
return string(h)
|
||||
@@ -166,7 +177,7 @@ func (t *Template) getBlock(nodes []ast.Node, offset, startLine int, name string
|
||||
// If we encounter another tag with the same name,
|
||||
// increment tagAmount so that we know that the next
|
||||
// end tag isn't the end of this tag.
|
||||
if node.Name.Value == name {
|
||||
if node.Name.Value == name && node.HasBody {
|
||||
tagAmount++
|
||||
}
|
||||
out = append(out, node)
|
||||
@@ -222,8 +233,14 @@ func (t *Template) getValue(node ast.Node, local map[string]any) (any, error) {
|
||||
return t.evalTernary(node, local)
|
||||
case ast.VariableOr:
|
||||
return t.evalVariableOr(node, local)
|
||||
case ast.Map:
|
||||
return t.convertMap(node, local)
|
||||
case ast.Array:
|
||||
return t.convertArray(node, local)
|
||||
case ast.Assignment:
|
||||
return node, t.handleAssignment(node, local)
|
||||
case ast.Nil:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
@@ -290,6 +307,23 @@ func valueToString(node ast.Node) string {
|
||||
} else {
|
||||
return "#" + node.Name.Value + "()"
|
||||
}
|
||||
case ast.Map:
|
||||
k, v := getOneMapPair(node)
|
||||
if len(node.Map) > 1 {
|
||||
return "{" + valueToString(k) + ": " + valueToString(v) + ", ...}"
|
||||
} else if len(node.Map) == 1 {
|
||||
return "{" + valueToString(k) + ": " + valueToString(v) + "}"
|
||||
} else {
|
||||
return "{}"
|
||||
}
|
||||
case ast.Array:
|
||||
if len(node.Array) > 1 {
|
||||
return "[" + valueToString(node.Array[0]) + ", ...]"
|
||||
} else if len(node.Array) == 1 {
|
||||
return "[" + valueToString(node.Array[0]) + "]"
|
||||
} else {
|
||||
return "[]"
|
||||
}
|
||||
case ast.EndTag:
|
||||
return "#!" + node.Name.Value
|
||||
case ast.ExprTag:
|
||||
@@ -299,6 +333,13 @@ func valueToString(node ast.Node) string {
|
||||
}
|
||||
}
|
||||
|
||||
func getOneMapPair(m ast.Map) (k, v ast.Node) {
|
||||
for key, val := range m.Map {
|
||||
return key, val
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// unwrapASTValue unwraps an ast.Value node into its underlying value
|
||||
func (t *Template) unwrapASTValue(node ast.Value, local map[string]any) (any, error) {
|
||||
v, err := t.getValue(node.Node, local)
|
||||
@@ -306,17 +347,57 @@ func (t *Template) unwrapASTValue(node ast.Value, local map[string]any) (any, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rval := reflect.ValueOf(v)
|
||||
|
||||
if node.Not {
|
||||
rval := reflect.ValueOf(v)
|
||||
if rval.Kind() != reflect.Bool {
|
||||
return nil, ast.PosError(node, "%s: the ! operator can only be used on boolean values", valueToString(node))
|
||||
}
|
||||
return !rval.Bool(), nil
|
||||
}
|
||||
|
||||
if rval.Kind() == reflect.Pointer && rval.IsNil() && t.getNilToZero() {
|
||||
rtyp := rval.Type().Elem()
|
||||
return reflect.New(rtyp).Interface(), nil
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
// convertMap converts an ast.Map value into a map[any]any by recursively calling
|
||||
// getValue on each of its keys and values.
|
||||
func (t *Template) convertMap(node ast.Map, local map[string]any) (any, error) {
|
||||
out := make(map[any]any, len(node.Map))
|
||||
for keyNode, valNode := range node.Map {
|
||||
key, err := t.getValue(keyNode, local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val, err := t.getValue(valNode, local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out[key] = val
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// convertArray converts an ast.Array into an []any by recursively calling getValue
|
||||
// on each of its elements.
|
||||
func (t *Template) convertArray(node ast.Array, local map[string]any) (any, error) {
|
||||
out := make([]any, len(node.Array))
|
||||
for i, valNode := range node.Array {
|
||||
val, err := t.getValue(valNode, local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = val
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// getVar tries to get a variable from the local map. If it's not found,
|
||||
// it'll try the global variable map. If it doesn't exist in either map,
|
||||
// it will return an error.
|
||||
@@ -429,10 +510,16 @@ func (t *Template) getIndex(i ast.Index, local map[string]any) (any, error) {
|
||||
}
|
||||
|
||||
intIndex := rindex.Interface().(int)
|
||||
if intIndex < rval.Len() {
|
||||
if intIndex < 0 {
|
||||
intIndex = rval.Len() + intIndex
|
||||
if intIndex < 0 {
|
||||
return nil, ast.PosError(i, "%s: index out of range: %d (length %d)", valueToString(i), rindex.Interface(), rval.Len())
|
||||
}
|
||||
out = rval.Index(intIndex)
|
||||
} else if intIndex < rval.Len() {
|
||||
out = rval.Index(intIndex)
|
||||
} else {
|
||||
return nil, ast.PosError(i, "%s: index out of range: %d", valueToString(i), intIndex)
|
||||
return nil, ast.PosError(i, "%s: index out of range: %d (length %d)", valueToString(i), intIndex, rval.Len())
|
||||
}
|
||||
case reflect.Map:
|
||||
if rindex.CanConvert(rval.Type().Key()) {
|
||||
@@ -484,18 +571,23 @@ func (t *Template) execMethodCall(mc ast.MethodCall, local map[string]any) (any,
|
||||
if !rval.IsValid() {
|
||||
return nil, ast.PosError(mc, "%s: cannot call method on nil value", valueToString(mc))
|
||||
}
|
||||
for rval.Kind() == reflect.Pointer {
|
||||
rval = rval.Elem()
|
||||
}
|
||||
// First, check for a method with the given name
|
||||
mtd := rval.MethodByName(mc.Name.Value)
|
||||
if mtd.IsValid() {
|
||||
return t.execFunc(mtd, mc, mc.Params, local)
|
||||
}
|
||||
// If the method doesn't exist, also check for a field storing a function.
|
||||
field := rval.FieldByName(mc.Name.Value)
|
||||
if field.IsValid() && field.Kind() == reflect.Func {
|
||||
return t.execFunc(field, mc, mc.Params, local)
|
||||
// If the method doesn't exist, we need to check for fields, so dereference any pointers
|
||||
// because pointers can't have fields
|
||||
for rval.Kind() == reflect.Pointer {
|
||||
rval = rval.Elem()
|
||||
}
|
||||
// Make sure we actually have a struct
|
||||
if rval.Kind() == reflect.Struct {
|
||||
// If the method doesn't exist, also check for a field storing a function.
|
||||
field := rval.FieldByName(mc.Name.Value)
|
||||
if field.IsValid() && field.Kind() == reflect.Func {
|
||||
return t.execFunc(field, mc, mc.Params, local)
|
||||
}
|
||||
}
|
||||
// If neither of those exist, return an error
|
||||
return nil, ast.PosError(mc, "no such method: %s", mc.Name.Value)
|
||||
|
||||
@@ -33,6 +33,28 @@ func TestSliceGetIndex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceGetNegativeIndex(t *testing.T) {
|
||||
testSlice := []any{1, "2", 3.0}
|
||||
|
||||
tmpl := testTmpl(t)
|
||||
|
||||
// test[-2]
|
||||
ast := ast.Index{
|
||||
Value: ast.Ident{Value: "test", Position: testPos(t)},
|
||||
Index: ast.Integer{Value: -2, Position: testPos(t)},
|
||||
Position: testPos(t),
|
||||
}
|
||||
|
||||
val, err := tmpl.getIndex(ast, map[string]any{"test": testSlice})
|
||||
if err != nil {
|
||||
t.Fatalf("getIndex error: %s", err)
|
||||
}
|
||||
|
||||
if val != testSlice[len(testSlice)-2] {
|
||||
t.Errorf("Expected %q, got %q", "2", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceGetIndexOutOfRange(t *testing.T) {
|
||||
testSlice := []any{}
|
||||
tmpl := testTmpl(t)
|
||||
@@ -50,6 +72,23 @@ func TestSliceGetIndexOutOfRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceGetNegativeIndexOutOfRange(t *testing.T) {
|
||||
testSlice := []any{0, 1, 2, 3}
|
||||
tmpl := testTmpl(t)
|
||||
|
||||
// test[-5]
|
||||
ast := ast.Index{
|
||||
Value: ast.Ident{Value: "test", Position: testPos(t)},
|
||||
Index: ast.Integer{Value: -5, Position: testPos(t)},
|
||||
Position: testPos(t),
|
||||
}
|
||||
|
||||
_, err := tmpl.getIndex(ast, map[string]any{"test": testSlice})
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceGetIndexInvalidType(t *testing.T) {
|
||||
testSlice := []any{}
|
||||
tmpl := testTmpl(t)
|
||||
|
||||
9
vars.go
9
vars.go
@@ -1,6 +1,7 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
|
||||
var globalVars = map[string]any{
|
||||
"len": tmplLen,
|
||||
"json": tmplJSON,
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
@@ -19,6 +21,8 @@ var globalVars = map[string]any{
|
||||
"count": strings.Count,
|
||||
"split": strings.Split,
|
||||
"join": strings.Join,
|
||||
"replace": strings.Replace,
|
||||
"replaceAll": strings.ReplaceAll,
|
||||
"sprintf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
@@ -31,3 +35,8 @@ func tmplLen(v any) (int, error) {
|
||||
return 0, fmt.Errorf("cannot get length of %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func tmplJSON(v any) (HTML, error) {
|
||||
data, err := json.Marshal(v)
|
||||
return HTML(data), err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user