Add namespaces and include tag, improve whitespace handling
This commit is contained in:
parent
fb7010dae3
commit
76c0a48b87
11
README.md
11
README.md
@ -30,16 +30,15 @@ Salix's syntax is similar to Leaf and (in my opinion at least), it's much more f
|
||||
### API Usage
|
||||
|
||||
```go
|
||||
t, err := salix.New().
|
||||
WithVarMap(vars).
|
||||
WithFuncMap(funcs).
|
||||
WithEscapeHTML(true).
|
||||
ParseFile("example.salix.txt")
|
||||
t, err := salix.New().ParseFile("example.salix.txt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = t.Execute(os.Stdout)
|
||||
err = t.WithVarMap(vars).
|
||||
WithFuncMap(funcs).
|
||||
WithEscapeHTML(true).
|
||||
Execute(os.Stdout)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -56,15 +56,14 @@ func main() {
|
||||
"page": Page{Title: "Users"},
|
||||
}
|
||||
|
||||
t, err := salix.New().
|
||||
WithVarMap(vars).
|
||||
WithEscapeHTML(true).
|
||||
ParseString("readme.salix.html", tmpl)
|
||||
t, err := salix.New().ParseString("readme.salix.html", tmpl)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = t.Execute(os.Stdout)
|
||||
err = t.WithVarMap(vars).
|
||||
WithEscapeHTML(true).
|
||||
Execute(os.Stdout)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
@ -23,13 +23,12 @@ func main() {
|
||||
}
|
||||
|
||||
t, err := salix.New().
|
||||
WithVarMap(vars).
|
||||
ParseString("replybot.salix.txt", tmpl)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = t.Execute(os.Stdout)
|
||||
err = t.WithVarMap(vars).Execute(os.Stdout)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
#if(len(matches) > 1):Imgur links were#else:An Imgur link was#!if detected in your #(type). Here are links to the same #if(len(matches) > 1):locations#else:location#!if on alternative frontends that protect your privacy.
|
||||
|
||||
#for(i, match in matches):
|
||||
#if(len(matches) > 1):Link #(i+1):#!if
|
||||
- [imgur.artemislena.eu](https://imgur.artemislena.eu/#(match[1]))
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
var ErrForTagInvalidArgs = errors.New("invalid arguments in for tag")
|
||||
|
||||
// forTag represents a #if tag within a Salix template
|
||||
// forTag represents a #for tag within a Salix template
|
||||
type forTag struct{}
|
||||
|
||||
func (ft forTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
|
31
funcs.go
31
funcs.go
@ -1,31 +0,0 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var defaultFuncs = map[string]any{
|
||||
"len": tmplLen,
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"trimPrefix": strings.TrimPrefix,
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
"trimSuffix": strings.TrimSuffix,
|
||||
"trimSpace": strings.TrimSpace,
|
||||
"equalFold": strings.EqualFold,
|
||||
"count": strings.Count,
|
||||
"split": strings.Split,
|
||||
"join": strings.Join,
|
||||
}
|
||||
|
||||
func tmplLen(v any) int {
|
||||
val := reflect.ValueOf(v)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.String, reflect.Map:
|
||||
return val.Len()
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
38
include_tag.go
Normal file
38
include_tag.go
Normal file
@ -0,0 +1,38 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"go.elara.ws/salix/internal/ast"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIncludeInvalidArgs = errors.New("include expects one string argument")
|
||||
ErrNoSuchTemplate = errors.New("no such template")
|
||||
)
|
||||
|
||||
// forTag represents an #include tag within a Salix template
|
||||
type includeTag struct{}
|
||||
|
||||
func (it includeTag) Run(tc *TagContext, block, args []ast.Node) error {
|
||||
if len(args) != 1 {
|
||||
return ErrIncludeInvalidArgs
|
||||
}
|
||||
|
||||
val, err := tc.GetValue(args[0], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrIncludeInvalidArgs
|
||||
}
|
||||
|
||||
tmpl, ok := tc.t.ns.GetTemplate(name)
|
||||
if !ok {
|
||||
return ErrNoSuchTemplate
|
||||
}
|
||||
|
||||
return tc.Execute(tmpl.ast, nil)
|
||||
}
|
78
namespace.go
Normal file
78
namespace.go
Normal file
@ -0,0 +1,78 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Namespace represents a collection of templates that can include each other
|
||||
type Namespace struct {
|
||||
mu sync.Mutex
|
||||
tmpls map[string]*Template
|
||||
vars map[string]reflect.Value
|
||||
tags map[string]Tag
|
||||
}
|
||||
|
||||
// New returns a new template namespace
|
||||
func New() *Namespace {
|
||||
return &Namespace{
|
||||
tmpls: map[string]*Template{},
|
||||
vars: map[string]reflect.Value{},
|
||||
tags: map[string]Tag{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithVarMap sets the namespace's variable map to m
|
||||
func (n *Namespace) WithVarMap(m map[string]any) *Namespace {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
n.vars = map[string]reflect.Value{}
|
||||
if m != nil {
|
||||
for k, v := range m {
|
||||
n.vars[k] = reflect.ValueOf(v)
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// WithTagMap sets the namespace's tag map to m
|
||||
func (n *Namespace) WithTagMap(m map[string]Tag) *Namespace {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
if m != nil {
|
||||
n.tags = m
|
||||
} else {
|
||||
n.tags = map[string]Tag{}
|
||||
}
|
||||
|
||||
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.
|
||||
func (n *Namespace) GetTemplate(name string) (*Template, bool) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
t, ok := n.tmpls[name]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
// getVar tries to get a variable from the namespace's variable map
|
||||
func (n *Namespace) getVar(name string) (reflect.Value, bool) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
v, ok := n.vars[name]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// getTag tries to get a tag from the namespace's tag map
|
||||
func (n *Namespace) getTag(name string) (Tag, bool) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
t, ok := n.tags[name]
|
||||
return t, ok
|
||||
}
|
140
parse.go
140
parse.go
@ -3,36 +3,52 @@ package salix
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.elara.ws/salix/internal/ast"
|
||||
"go.elara.ws/salix/internal/parser"
|
||||
)
|
||||
|
||||
// Parse parses a salix template from r. If the reader has a Name method
|
||||
// that returns a string, that will be used as the filename.
|
||||
func (t *Template) Parse(r io.Reader) (*Template, error) {
|
||||
fname := "<input>"
|
||||
if r, ok := r.(interface{ Name() string }); ok {
|
||||
fname = r.Name()
|
||||
}
|
||||
return t.ParseWithFilename(fname, r)
|
||||
// NamedReader is a reader with a name
|
||||
type NamedReader interface {
|
||||
io.Reader
|
||||
Name() string
|
||||
}
|
||||
|
||||
// ParseWithFilename parses a salix template from r, using the given filename.
|
||||
func (t *Template) ParseWithFilename(filename string, r io.Reader) (*Template, error) {
|
||||
astVal, err := parser.ParseReader(filename, r)
|
||||
// Parse parses a salix template from a NamedReader, which is an io.Reader
|
||||
// with a Name method that returns a string. os.File implements NamedReader.
|
||||
func (n *Namespace) Parse(r NamedReader) (*Template, error) {
|
||||
return n.ParseWithName(r.Name(), r)
|
||||
}
|
||||
|
||||
// ParseWithFilename parses a salix template from r, using the given name.
|
||||
func (n *Namespace) ParseWithName(name string, r io.Reader) (*Template, error) {
|
||||
astVal, err := parser.ParseReader(name, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.file = filename
|
||||
t.ast = astVal.([]ast.Node)
|
||||
|
||||
t := &Template{
|
||||
ns: n,
|
||||
name: name,
|
||||
ast: astVal.([]ast.Node),
|
||||
tags: map[string]Tag{},
|
||||
vars: map[string]reflect.Value{},
|
||||
}
|
||||
|
||||
performWhitespaceMutations(t.ast)
|
||||
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
n.tmpls[name] = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ParseFile parses the file at path as a salix template
|
||||
func (t *Template) ParseFile(path string) (*Template, error) {
|
||||
// ParseFile parses the file at path as a salix template. It uses the path as the name.
|
||||
func (t *Namespace) ParseFile(path string) (*Template, error) {
|
||||
fl, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -41,12 +57,98 @@ func (t *Template) ParseFile(path string) (*Template, error) {
|
||||
return t.Parse(fl)
|
||||
}
|
||||
|
||||
// ParseFile parses a file at the given path in a filesystem. It uses the path as the name.
|
||||
func (t *Namespace) ParseFS(fsys fs.FS, path string) (*Template, error) {
|
||||
fl, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fl.Close()
|
||||
return t.ParseWithName(path, fl)
|
||||
}
|
||||
|
||||
// ParseString parses a string using the given filename.
|
||||
func (t *Template) ParseString(filename, tmpl string) (*Template, error) {
|
||||
return t.ParseWithFilename(filename, strings.NewReader(tmpl))
|
||||
func (t *Namespace) ParseString(filename, tmpl string) (*Template, error) {
|
||||
return t.ParseWithName(filename, strings.NewReader(tmpl))
|
||||
}
|
||||
|
||||
// ParseString parses bytes using the given filename.
|
||||
func (t *Template) ParseBytes(filename string, tmpl []byte) (*Template, error) {
|
||||
return t.ParseWithFilename(filename, bytes.NewReader(tmpl))
|
||||
func (t *Namespace) ParseBytes(filename string, tmpl []byte) (*Template, error) {
|
||||
return t.ParseWithName(filename, bytes.NewReader(tmpl))
|
||||
}
|
||||
|
||||
// performWhitespaceMutations mutates nodes in the AST to remove
|
||||
// whitespace where it isn't needed.
|
||||
func performWhitespaceMutations(nodes []ast.Node) {
|
||||
// lastTag keeps track of which line the
|
||||
// last tag was found on, so that if an end
|
||||
// tag was found on the same line, we know it's inline
|
||||
// and we don't need to handle its whitespace.
|
||||
lastTag := 0
|
||||
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
switch node := nodes[i].(type) {
|
||||
case ast.Tag:
|
||||
// If the node has no body, it's an inline tag,
|
||||
// so we don't need to handle any whitespace around it.
|
||||
if !node.HasBody {
|
||||
continue
|
||||
}
|
||||
handleWhitespace(nodes, i)
|
||||
lastTag = node.Position.Line
|
||||
case ast.EndTag:
|
||||
if lastTag != node.Position.Line {
|
||||
handleWhitespace(nodes, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleWhitespace mutates nodes above and below tags and end tags
|
||||
// to remove the unneeded whitespace around them.
|
||||
func handleWhitespace(nodes []ast.Node, i int) {
|
||||
lastIndex := len(nodes) - 1
|
||||
|
||||
var prevNode ast.Text
|
||||
var nextNode ast.Text
|
||||
|
||||
if i != 0 {
|
||||
if node, ok := nodes[i-1].(ast.Text); ok {
|
||||
prevNode = node
|
||||
}
|
||||
}
|
||||
|
||||
if i != lastIndex {
|
||||
if node, ok := nodes[i+1].(ast.Text); ok {
|
||||
nextNode = node
|
||||
}
|
||||
}
|
||||
|
||||
if prevNode.Data != nil && bytes.Contains(nextNode.Data, []byte{'\n'}) {
|
||||
prevNode.Data = trimWhitespaceSuffix(prevNode.Data)
|
||||
nodes[i-1] = prevNode
|
||||
}
|
||||
|
||||
if nextNode.Data != nil {
|
||||
nextNode.Data = bytes.TrimPrefix(nextNode.Data, []byte{'\n'})
|
||||
nodes[i+1] = nextNode
|
||||
}
|
||||
}
|
||||
|
||||
// trimWhitespaceSuffix removes everything up to and including the first newline
|
||||
// it finds, starting from the end of the slice. If a non-whitespace character is
|
||||
// encountered before a newline, the data is returned unmodified.
|
||||
func trimWhitespaceSuffix(data []byte) []byte {
|
||||
// Start from the end of the slice
|
||||
for i := len(data) - 1; i >= 0; i-- {
|
||||
if data[i] == '\n' {
|
||||
// If a newline is found, return the slice without the newline and anything after
|
||||
return data[:i+1]
|
||||
} else if data[i] != ' ' && data[i] != '\t' && data[i] != '\r' {
|
||||
// If a non-whitespace character is found, return the original slice
|
||||
return data
|
||||
}
|
||||
}
|
||||
// If no newline or non-whitespace character is found, return the original slice
|
||||
return data
|
||||
}
|
||||
|
171
salix.go
171
salix.go
@ -1,12 +1,10 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"maps"
|
||||
"reflect"
|
||||
|
||||
"go.elara.ws/salix/internal/ast"
|
||||
@ -31,87 +29,77 @@ var (
|
||||
ErrFuncSecondReturnType = errors.New("the second return value of a template function must be an error")
|
||||
)
|
||||
|
||||
// HTML represents unescaped HTML strings
|
||||
type HTML string
|
||||
|
||||
// Template represents a Salix template
|
||||
type Template struct {
|
||||
file string
|
||||
ns *Namespace
|
||||
name string
|
||||
ast []ast.Node
|
||||
|
||||
escapeHTML bool
|
||||
|
||||
tags map[string]Tag
|
||||
funcs map[string]reflect.Value
|
||||
vars map[string]reflect.Value
|
||||
}
|
||||
|
||||
// New creates a new base template with the default
|
||||
// tags and functions added.
|
||||
func New() *Template {
|
||||
t := &Template{
|
||||
funcs: map[string]reflect.Value{},
|
||||
vars: map[string]reflect.Value{},
|
||||
tags: map[string]Tag{},
|
||||
}
|
||||
return t.WithTagMap(defaultTags).WithFuncMap(defaultFuncs)
|
||||
}
|
||||
|
||||
// Clone returns a shallow clone of a template.
|
||||
func (t *Template) Clone() *Template {
|
||||
return &Template{
|
||||
file: t.file,
|
||||
// WithVarMap returns a copy of the template with its variable map set to m.
|
||||
func (t *Template) WithVarMap(m map[string]any) *Template {
|
||||
newTmpl := &Template{
|
||||
ns: t.ns,
|
||||
name: t.name,
|
||||
ast: t.ast,
|
||||
|
||||
escapeHTML: t.escapeHTML,
|
||||
|
||||
tags: maps.Clone(t.tags),
|
||||
funcs: maps.Clone(t.funcs),
|
||||
vars: maps.Clone(t.vars),
|
||||
}
|
||||
tags: t.tags,
|
||||
vars: map[string]reflect.Value{},
|
||||
}
|
||||
|
||||
// WithFuncMap adds all the functions in m to the template's
|
||||
// global function map. If a function with the given name already
|
||||
// exists, it will be overwritten.
|
||||
func (t *Template) WithFuncMap(m map[string]any) *Template {
|
||||
if m != nil {
|
||||
for name, fn := range m {
|
||||
t.funcs[name] = reflect.ValueOf(fn)
|
||||
for k, v := range m {
|
||||
newTmpl.vars[k] = reflect.ValueOf(v)
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// WithVarMap adds all the variables in m to the template's
|
||||
// global variable map. If a variable with the given name already
|
||||
// exists, it will be overwritten.
|
||||
func (t *Template) WithVarMap(m map[string]any) *Template {
|
||||
if m != nil {
|
||||
for name, val := range m {
|
||||
t.vars[name] = reflect.ValueOf(val)
|
||||
}
|
||||
}
|
||||
return t
|
||||
return newTmpl
|
||||
}
|
||||
|
||||
// WithTagMap adds all the tags in m to the template's
|
||||
// global tag map. If a tag with the given name already
|
||||
// exists, it will be overwritten.
|
||||
// WithTagMap returns a copy of the template with its tag map set to m.
|
||||
func (t *Template) WithTagMap(m map[string]Tag) *Template {
|
||||
if m != nil {
|
||||
for name, tag := range m {
|
||||
t.tags[name] = tag
|
||||
}
|
||||
}
|
||||
return t
|
||||
// Make sure the tag map is never nil to avoid panics
|
||||
if m == nil {
|
||||
m = map[string]Tag{}
|
||||
}
|
||||
|
||||
// WithEscapeHTML enables or disables HTML escaping.
|
||||
return &Template{
|
||||
ns: t.ns,
|
||||
name: t.name,
|
||||
ast: t.ast,
|
||||
|
||||
escapeHTML: t.escapeHTML,
|
||||
|
||||
tags: m,
|
||||
vars: t.vars,
|
||||
}
|
||||
}
|
||||
|
||||
// WithEscapeHTML returns a copy of the template with HTML escaping enabled or disabled.
|
||||
// The HTML escaping functionality is NOT context-aware.
|
||||
// Using the HTML type allows you to get around the escaping.
|
||||
// Using the HTML type allows you to get around the escaping if needed.
|
||||
func (t *Template) WithEscapeHTML(b bool) *Template {
|
||||
t.escapeHTML = true
|
||||
return t
|
||||
return &Template{
|
||||
ns: t.ns,
|
||||
name: t.name,
|
||||
ast: t.ast,
|
||||
|
||||
escapeHTML: b,
|
||||
|
||||
tags: t.tags,
|
||||
vars: t.vars,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes a parsed template and writes
|
||||
@ -181,15 +169,10 @@ func (t *Template) getBlock(nodes []ast.Node, offset, startLine int, name string
|
||||
if node.Name.Value == name {
|
||||
tagAmount--
|
||||
}
|
||||
// Once tagAmount is zero (all the tags of the same name)
|
||||
// have been closed with an end tag, we can handle our newlines
|
||||
// and return the nodes we've accumulated.
|
||||
// Once tagAmount is zero (all the tags of the same name
|
||||
// have been closed with an end tag), we can return
|
||||
// the nodes we've accumulated.
|
||||
if tagAmount == 0 {
|
||||
// If the end tag is on the same line as the start tag,
|
||||
// we don't need to remove any newlines.
|
||||
if node.Position.Line != startLine {
|
||||
t.handleNewlines(nodes, i)
|
||||
}
|
||||
return out
|
||||
} else {
|
||||
out = append(out, node)
|
||||
@ -207,7 +190,11 @@ func (t *Template) getValue(node ast.Node, local map[string]any) (any, error) {
|
||||
case ast.Value:
|
||||
return t.unwrapASTValue(node, local)
|
||||
case ast.Ident:
|
||||
return t.getVar(node, local)
|
||||
val, err := t.getVar(node, local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return val.Interface(), nil
|
||||
case ast.String:
|
||||
return node.Value, nil
|
||||
case ast.Float:
|
||||
@ -254,56 +241,58 @@ func (t *Template) unwrapASTValue(node ast.Value, local map[string]any) (any, er
|
||||
// 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.
|
||||
func (t *Template) getVar(id ast.Ident, local map[string]any) (any, error) {
|
||||
func (t *Template) getVar(id ast.Ident, local map[string]any) (reflect.Value, error) {
|
||||
if local != nil {
|
||||
v, ok := local[id.Value]
|
||||
if ok {
|
||||
return v, nil
|
||||
return reflect.ValueOf(v), nil
|
||||
}
|
||||
}
|
||||
|
||||
v, ok := t.vars[id.Value]
|
||||
if !ok {
|
||||
return nil, t.posError(id, "%w: %s", ErrNoSuchVar, id.Value)
|
||||
if ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return v.Interface(), nil
|
||||
v, ok = t.ns.getVar(id.Value)
|
||||
if ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// handleNewlines removes newlines above and below the given index
|
||||
// in the nodes slice.
|
||||
func (t *Template) handleNewlines(nodes []ast.Node, i int) {
|
||||
if i != 0 {
|
||||
if node, ok := nodes[i-1].(ast.Text); ok {
|
||||
ni := bytes.LastIndexByte(node.Data, '\n')
|
||||
if ni != -1 {
|
||||
node.Data = node.Data[:ni]
|
||||
}
|
||||
nodes[i-1] = node
|
||||
}
|
||||
v, ok = globalVars[id.Value]
|
||||
if ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
lastIndex := len(nodes) - 1
|
||||
if i != lastIndex {
|
||||
if node, ok := nodes[i+1].(ast.Text); ok {
|
||||
ni := bytes.IndexByte(node.Data, '\n')
|
||||
if ni != -1 {
|
||||
node.Data = node.Data[ni:]
|
||||
return reflect.Value{}, t.posError(id, "%w: %s", ErrNoSuchVar, id.Value)
|
||||
}
|
||||
nodes[i+1] = node
|
||||
|
||||
func (t *Template) getTag(name string) (Tag, bool) {
|
||||
tag, ok := t.tags[name]
|
||||
if ok {
|
||||
return tag, true
|
||||
}
|
||||
|
||||
tag, ok = t.ns.getTag(name)
|
||||
if ok {
|
||||
return tag, true
|
||||
}
|
||||
|
||||
tag, ok = globalTags[name]
|
||||
if ok {
|
||||
return tag, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// execTag executes a tag
|
||||
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.tags[node.Name.Value]
|
||||
tag, ok := t.getTag(node.Name.Value)
|
||||
if !ok {
|
||||
return 0, t.posError(node, "%w: %s", ErrNoSuchTag, node.Name.Value)
|
||||
}
|
||||
|
||||
t.handleNewlines(nodes, i)
|
||||
|
||||
var block []ast.Node
|
||||
if node.HasBody {
|
||||
block = t.getBlock(nodes, i+1, node.Position.Line, node.Name.Value)
|
||||
@ -322,8 +311,8 @@ func (t *Template) execTag(node ast.Tag, w io.Writer, nodes []ast.Node, i int, l
|
||||
|
||||
// execFuncCall executes a function call
|
||||
func (t *Template) execFuncCall(fc ast.FuncCall, local map[string]any) (any, error) {
|
||||
fn, ok := t.funcs[fc.Name.Value]
|
||||
if !ok {
|
||||
fn, err := t.getVar(fc.Name, local)
|
||||
if err != nil {
|
||||
return nil, t.posError(fc, "%w: %s", ErrNoSuchFunc, fc.Name.Value)
|
||||
}
|
||||
return t.execFunc(fn, fc, fc.Params, local)
|
||||
@ -439,7 +428,7 @@ func (t *Template) execFunc(fn reflect.Value, node ast.Node, args []ast.Node, lo
|
||||
}
|
||||
|
||||
func (t *Template) posError(n ast.Node, format string, v ...any) error {
|
||||
return ast.PosError(n, t.file, format, v...)
|
||||
return ast.PosError(n, t.name, format, v...)
|
||||
}
|
||||
|
||||
func validateFunc(t reflect.Type) error {
|
||||
|
7
tags.go
7
tags.go
@ -6,25 +6,30 @@ import (
|
||||
"go.elara.ws/salix/internal/ast"
|
||||
)
|
||||
|
||||
// Tag represents a tag in a Salix template
|
||||
type Tag interface {
|
||||
Run(tc *TagContext, block, args []ast.Node) error
|
||||
}
|
||||
|
||||
var defaultTags = map[string]Tag{
|
||||
var globalTags = map[string]Tag{
|
||||
"if": ifTag{},
|
||||
"for": forTag{},
|
||||
"include": includeTag{},
|
||||
}
|
||||
|
||||
// TagContext is passed to Tag implementations to allow them to control the interpreter
|
||||
type TagContext struct {
|
||||
w io.Writer
|
||||
t *Template
|
||||
local map[string]any
|
||||
}
|
||||
|
||||
// Execute runs the interpreter on the given AST nodes, with the given local variables.
|
||||
func (tc *TagContext) Execute(nodes []ast.Node, local map[string]any) error {
|
||||
return tc.t.execute(tc.w, nodes, mergeMap(tc.local, local))
|
||||
}
|
||||
|
||||
// GetValue evaluates the given AST node using the given local variables.
|
||||
func (tc *TagContext) GetValue(node ast.Node, local map[string]any) (any, error) {
|
||||
return tc.t.getValue(node, mergeMap(tc.local, local))
|
||||
}
|
||||
|
31
vars.go
Normal file
31
vars.go
Normal file
@ -0,0 +1,31 @@
|
||||
package salix
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var globalVars = map[string]reflect.Value{
|
||||
"len": reflect.ValueOf(tmplLen),
|
||||
"toUpper": reflect.ValueOf(strings.ToUpper),
|
||||
"toLower": reflect.ValueOf(strings.ToLower),
|
||||
"hasPrefix": reflect.ValueOf(strings.HasPrefix),
|
||||
"trimPrefix": reflect.ValueOf(strings.TrimPrefix),
|
||||
"hasSuffix": reflect.ValueOf(strings.HasSuffix),
|
||||
"trimSuffix": reflect.ValueOf(strings.TrimSuffix),
|
||||
"trimSpace": reflect.ValueOf(strings.TrimSpace),
|
||||
"equalFold": reflect.ValueOf(strings.EqualFold),
|
||||
"count": reflect.ValueOf(strings.Count),
|
||||
"split": reflect.ValueOf(strings.Split),
|
||||
"join": reflect.ValueOf(strings.Join),
|
||||
}
|
||||
|
||||
func tmplLen(v any) int {
|
||||
val := reflect.ValueOf(v)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.String, reflect.Map:
|
||||
return val.Len()
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user