Compare commits
9 Commits
19398a93e6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cbbaee47d4 | |||
| bcb8222ebf | |||
| 6d260619b7 | |||
| 944663c2b1 | |||
| c5470a45dd | |||
| 849295bb5f | |||
| d5c33f9e5d | |||
| f1a998c25b | |||
| 167f448ae1 |
@@ -99,7 +99,7 @@ In this example:
|
|||||||
|
|
||||||
### `for` tag
|
### `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):
|
#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")
|
#include("header.html")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the file name starts with a question mark, nonexistent files will be ignored.
|
||||||
|
|
||||||
#### Using the `include` tag with extra arguments
|
#### 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:
|
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.
|
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
|
#### 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:
|
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:
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ func (p Position) String() string {
|
|||||||
return fmt.Sprintf("%s: line %d, col %d", p.Name, p.Line, p.Col)
|
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 {
|
type Tag struct {
|
||||||
Name Ident
|
Name Ident
|
||||||
Params []Node
|
Params []Node
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>#(title)</title>
|
<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>
|
</head>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>#(title)</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-dark">
|
<nav class="navbar is-dark">
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<title>#(page.Title)</title>
|
<head>
|
||||||
</head>
|
<title>#(page.Title)</title>
|
||||||
<body>
|
</head>
|
||||||
#for(i, user in users):
|
|
||||||
<div>
|
<body>
|
||||||
<h2>#(toLower(user.Name))</h2>
|
#for(i, user in users):
|
||||||
<p>User ID: #(i)</p>
|
<div>
|
||||||
#if(user.LoggedIn): <p>This user is logged in</p> #!if
|
<h2>#(toLower(user.Name))</h2>
|
||||||
#if(user.IsAdmin): <p>This user is an admin!</p> #!if
|
<p>User ID: #(i)</p>
|
||||||
<p>Registered: #(user.RegisteredTime.Format("01-02-2006"))</p>
|
#if(user.LoggedIn): <p>This user is logged in</p> #!if
|
||||||
</div>
|
#if(user.IsAdmin): <p>This user is an admin!</p> #!if
|
||||||
#!for
|
<p>Registered: #(user.RegisteredTime.Format("01-02-2006"))</p>
|
||||||
</body>
|
</div>
|
||||||
|
#!for
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</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
|
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" {
|
if op.Value == "in" {
|
||||||
switch b.Kind() {
|
a, b, err = handleIn(op, a, b)
|
||||||
case reflect.Slice, reflect.Array:
|
if err != nil {
|
||||||
if a.CanConvert(b.Type().Elem()) {
|
return nil, err
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
} else if !a.IsValid() || !b.IsValid() {
|
||||||
|
return handleNil(op, a, b)
|
||||||
} else if b.CanConvert(a.Type()) {
|
} else if b.CanConvert(a.Type()) {
|
||||||
b = b.Convert(a.Type())
|
b = b.Convert(a.Type())
|
||||||
} else {
|
} 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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
41
for_tag.go
41
for_tag.go
@@ -55,6 +55,15 @@ func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
in = reflect.ValueOf(val)
|
in = reflect.ValueOf(val)
|
||||||
|
|
||||||
switch in.Kind() {
|
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:
|
case reflect.Slice, reflect.Array:
|
||||||
local := map[string]any{}
|
local := map[string]any{}
|
||||||
for i := 0; i < in.Len(); i++ {
|
for i := 0; i < in.Len(); i++ {
|
||||||
@@ -95,6 +104,38 @@ func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
|||||||
|
|
||||||
i++
|
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++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,3 +1,3 @@
|
|||||||
module go.elara.ws/salix
|
module go.elara.ws/salix
|
||||||
|
|
||||||
go 1.21.2
|
go 1.23.0
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ type Namespace struct {
|
|||||||
WriteOnSuccess bool
|
WriteOnSuccess bool
|
||||||
// NilToZero indictes whether nil pointer values should be converted to zero values of their underlying
|
// NilToZero indictes whether nil pointer values should be converted to zero values of their underlying
|
||||||
// types.
|
// types.
|
||||||
NilToZero bool
|
NilToZero bool
|
||||||
escapeHTML *bool
|
escapeHTML *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new template namespace
|
// New returns a new template namespace
|
||||||
|
|||||||
956
parser/parser.go
956
parser/parser.go
File diff suppressed because it is too large
Load Diff
@@ -83,8 +83,21 @@ ExprTag = '#' ignoreErr:'?'? '(' item:Expr ')' {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr = Ternary / Assignment / LogicalExpr
|
Expr = Assignment / TernaryExpr
|
||||||
Assignable = Ternary / LogicalExpr
|
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)* _ {
|
LogicalExpr = _ first:ComparisonExpr rest:(_ LogicalOp _ ComparisonExpr)* _ {
|
||||||
return toExpr(c, first, rest), nil
|
return toExpr(c, first, rest), nil
|
||||||
@@ -116,7 +129,7 @@ ParamList = '(' params:(Expr ( ',' _ Expr )* )? ')' {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Value = not:"!"? node:(MethodCall / FieldAccess / Index / String / RawString / Float / Integer / Bool / FuncCall / VariableOr / Ident / ParenExpr / Array / Map) {
|
Value = not:"!"? node:(Nil / MethodCall / FieldAccess / Index / String / RawString / Float / Integer / Bool / FuncCall / VariableOr / Ident / ParenExpr / Array / Map) {
|
||||||
return ast.Value{
|
return ast.Value{
|
||||||
Node: node.(ast.Node),
|
Node: node.(ast.Node),
|
||||||
Not: not != nil,
|
Not: not != nil,
|
||||||
@@ -174,14 +187,6 @@ Assignment = name:Ident _ '=' _ value:Assignable {
|
|||||||
}, nil
|
}, 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 {
|
MethodCall = value:Value '.' name:Ident params:ParamList {
|
||||||
return ast.MethodCall{
|
return ast.MethodCall{
|
||||||
Value: value.(ast.Node),
|
Value: value.(ast.Node),
|
||||||
@@ -283,6 +288,10 @@ ArithmeticOp = ('+' / '-' / '/' / '*' / '%') {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Nil = "nil" {
|
||||||
|
return ast.Nil{Position: getPos(c)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
Text = . [^#]* { return ast.Text{Data: c.text, Position: getPos(c)}, nil }
|
Text = . [^#]* { return ast.Text{Data: c.text, Position: getPos(c)}, nil }
|
||||||
|
|
||||||
_ "whitespace" ← [ \t\r\n]*
|
_ "whitespace" ← [ \t\r\n]*
|
||||||
|
|||||||
4
salix.go
4
salix.go
@@ -26,7 +26,7 @@ type Template struct {
|
|||||||
// WriteOnSuccess indicates whether the output should only be written if generation fully succeeds.
|
// 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)
|
// This option buffers the output of the template, so it will use more memory. (default: false)
|
||||||
WriteOnSuccess bool
|
WriteOnSuccess bool
|
||||||
NilToZero bool
|
NilToZero bool
|
||||||
|
|
||||||
tags map[string]Tag
|
tags map[string]Tag
|
||||||
vars map[string]any
|
vars map[string]any
|
||||||
@@ -239,6 +239,8 @@ func (t *Template) getValue(node ast.Node, local map[string]any) (any, error) {
|
|||||||
return t.convertArray(node, local)
|
return t.convertArray(node, local)
|
||||||
case ast.Assignment:
|
case ast.Assignment:
|
||||||
return node, t.handleAssignment(node, local)
|
return node, t.handleAssignment(node, local)
|
||||||
|
case ast.Nil:
|
||||||
|
return nil, nil
|
||||||
default:
|
default:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user