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
|
### API Usage
|
||||||
|
|
||||||
```go
|
```go
|
||||||
t, err := salix.New().
|
t, err := salix.New().ParseFile("example.salix.txt")
|
||||||
WithVarMap(vars).
|
|
||||||
WithFuncMap(funcs).
|
|
||||||
WithEscapeHTML(true).
|
|
||||||
ParseFile("example.salix.txt")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.Execute(os.Stdout)
|
err = t.WithVarMap(vars).
|
||||||
|
WithFuncMap(funcs).
|
||||||
|
WithEscapeHTML(true).
|
||||||
|
Execute(os.Stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -56,15 +56,14 @@ func main() {
|
|||||||
"page": Page{Title: "Users"},
|
"page": Page{Title: "Users"},
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := salix.New().
|
t, err := salix.New().ParseString("readme.salix.html", tmpl)
|
||||||
WithVarMap(vars).
|
|
||||||
WithEscapeHTML(true).
|
|
||||||
ParseString("readme.salix.html", tmpl)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.Execute(os.Stdout)
|
err = t.WithVarMap(vars).
|
||||||
|
WithEscapeHTML(true).
|
||||||
|
Execute(os.Stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t, err := salix.New().
|
t, err := salix.New().
|
||||||
WithVarMap(vars).
|
|
||||||
ParseString("replybot.salix.txt", tmpl)
|
ParseString("replybot.salix.txt", tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.Execute(os.Stdout)
|
err = t.WithVarMap(vars).Execute(os.Stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
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.
|
#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):
|
#for(i, match in matches):
|
||||||
#if(len(matches) > 1):Link #(i+1):#!if
|
#if(len(matches) > 1):Link #(i+1):#!if
|
||||||
- [imgur.artemislena.eu](https://imgur.artemislena.eu/#(match[1]))
|
- [imgur.artemislena.eu](https://imgur.artemislena.eu/#(match[1]))
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
var ErrForTagInvalidArgs = errors.New("invalid arguments in for tag")
|
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{}
|
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 {
|
||||||
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.elara.ws/salix/internal/ast"
|
"go.elara.ws/salix/internal/ast"
|
||||||
"go.elara.ws/salix/internal/parser"
|
"go.elara.ws/salix/internal/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse parses a salix template from r. If the reader has a Name method
|
// NamedReader is a reader with a name
|
||||||
// that returns a string, that will be used as the filename.
|
type NamedReader interface {
|
||||||
func (t *Template) Parse(r io.Reader) (*Template, error) {
|
io.Reader
|
||||||
fname := "<input>"
|
Name() string
|
||||||
if r, ok := r.(interface{ Name() string }); ok {
|
|
||||||
fname = r.Name()
|
|
||||||
}
|
|
||||||
return t.ParseWithFilename(fname, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWithFilename parses a salix template from r, using the given filename.
|
// Parse parses a salix template from a NamedReader, which is an io.Reader
|
||||||
func (t *Template) ParseWithFilename(filename string, r io.Reader) (*Template, error) {
|
// with a Name method that returns a string. os.File implements NamedReader.
|
||||||
astVal, err := parser.ParseReader(filename, r)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFile parses the file at path as a salix template
|
// ParseFile parses the file at path as a salix template. It uses the path as the name.
|
||||||
func (t *Template) ParseFile(path string) (*Template, error) {
|
func (t *Namespace) ParseFile(path string) (*Template, error) {
|
||||||
fl, err := os.Open(path)
|
fl, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -41,12 +57,98 @@ func (t *Template) ParseFile(path string) (*Template, error) {
|
|||||||
return t.Parse(fl)
|
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.
|
// ParseString parses a string using the given filename.
|
||||||
func (t *Template) ParseString(filename, tmpl string) (*Template, error) {
|
func (t *Namespace) ParseString(filename, tmpl string) (*Template, error) {
|
||||||
return t.ParseWithFilename(filename, strings.NewReader(tmpl))
|
return t.ParseWithName(filename, strings.NewReader(tmpl))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseString parses bytes using the given filename.
|
// ParseString parses bytes using the given filename.
|
||||||
func (t *Template) ParseBytes(filename string, tmpl []byte) (*Template, error) {
|
func (t *Namespace) ParseBytes(filename string, tmpl []byte) (*Template, error) {
|
||||||
return t.ParseWithFilename(filename, bytes.NewReader(tmpl))
|
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
|
package salix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"go.elara.ws/salix/internal/ast"
|
"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")
|
ErrFuncSecondReturnType = errors.New("the second return value of a template function must be an error")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HTML represents unescaped HTML strings
|
||||||
type HTML string
|
type HTML string
|
||||||
|
|
||||||
// Template represents a Salix template
|
// Template represents a Salix template
|
||||||
type Template struct {
|
type Template struct {
|
||||||
file string
|
ns *Namespace
|
||||||
|
name string
|
||||||
ast []ast.Node
|
ast []ast.Node
|
||||||
|
|
||||||
escapeHTML bool
|
escapeHTML bool
|
||||||
|
|
||||||
tags map[string]Tag
|
tags map[string]Tag
|
||||||
funcs map[string]reflect.Value
|
|
||||||
vars map[string]reflect.Value
|
vars map[string]reflect.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new base template with the default
|
// WithVarMap returns a copy of the template with its variable map set to m.
|
||||||
// tags and functions added.
|
func (t *Template) WithVarMap(m map[string]any) *Template {
|
||||||
func New() *Template {
|
newTmpl := &Template{
|
||||||
t := &Template{
|
ns: t.ns,
|
||||||
funcs: map[string]reflect.Value{},
|
name: t.name,
|
||||||
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,
|
|
||||||
ast: t.ast,
|
ast: t.ast,
|
||||||
|
|
||||||
escapeHTML: t.escapeHTML,
|
escapeHTML: t.escapeHTML,
|
||||||
|
|
||||||
tags: maps.Clone(t.tags),
|
tags: t.tags,
|
||||||
funcs: maps.Clone(t.funcs),
|
vars: map[string]reflect.Value{},
|
||||||
vars: maps.Clone(t.vars),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
if m != nil {
|
||||||
for name, fn := range m {
|
for k, v := range m {
|
||||||
t.funcs[name] = reflect.ValueOf(fn)
|
newTmpl.vars[k] = reflect.ValueOf(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t
|
|
||||||
|
return newTmpl
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithVarMap adds all the variables in m to the template's
|
// WithTagMap returns a copy of the template with its tag map set to m.
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
func (t *Template) WithTagMap(m map[string]Tag) *Template {
|
func (t *Template) WithTagMap(m map[string]Tag) *Template {
|
||||||
if m != nil {
|
// Make sure the tag map is never nil to avoid panics
|
||||||
for name, tag := range m {
|
if m == nil {
|
||||||
t.tags[name] = tag
|
m = map[string]Tag{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &Template{
|
||||||
|
ns: t.ns,
|
||||||
|
name: t.name,
|
||||||
|
ast: t.ast,
|
||||||
|
|
||||||
|
escapeHTML: t.escapeHTML,
|
||||||
|
|
||||||
|
tags: m,
|
||||||
|
vars: t.vars,
|
||||||
}
|
}
|
||||||
return t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithEscapeHTML enables or disables HTML escaping.
|
// WithEscapeHTML returns a copy of the template with HTML escaping enabled or disabled.
|
||||||
// The HTML escaping functionality is NOT context-aware.
|
// 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 {
|
func (t *Template) WithEscapeHTML(b bool) *Template {
|
||||||
t.escapeHTML = true
|
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
|
// 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 {
|
if node.Name.Value == name {
|
||||||
tagAmount--
|
tagAmount--
|
||||||
}
|
}
|
||||||
// Once tagAmount is zero (all the tags of the same name)
|
// Once tagAmount is zero (all the tags of the same name
|
||||||
// have been closed with an end tag, we can handle our newlines
|
// have been closed with an end tag), we can return
|
||||||
// and return the nodes we've accumulated.
|
// the nodes we've accumulated.
|
||||||
if tagAmount == 0 {
|
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
|
return out
|
||||||
} else {
|
} else {
|
||||||
out = append(out, node)
|
out = append(out, node)
|
||||||
@ -207,7 +190,11 @@ func (t *Template) getValue(node ast.Node, local map[string]any) (any, error) {
|
|||||||
case ast.Value:
|
case ast.Value:
|
||||||
return t.unwrapASTValue(node, local)
|
return t.unwrapASTValue(node, local)
|
||||||
case ast.Ident:
|
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:
|
case ast.String:
|
||||||
return node.Value, nil
|
return node.Value, nil
|
||||||
case ast.Float:
|
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,
|
// 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'll try the global variable map. If it doesn't exist in either map,
|
||||||
// it will return an error.
|
// 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 {
|
if local != nil {
|
||||||
v, ok := local[id.Value]
|
v, ok := local[id.Value]
|
||||||
if ok {
|
if ok {
|
||||||
return v, nil
|
return reflect.ValueOf(v), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v, ok := t.vars[id.Value]
|
v, ok := t.vars[id.Value]
|
||||||
if !ok {
|
if ok {
|
||||||
return nil, t.posError(id, "%w: %s", ErrNoSuchVar, id.Value)
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return v.Interface(), nil
|
v, ok = t.ns.getVar(id.Value)
|
||||||
|
if ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok = globalVars[id.Value]
|
||||||
|
if ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.Value{}, t.posError(id, "%w: %s", ErrNoSuchVar, id.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNewlines removes newlines above and below the given index
|
func (t *Template) getTag(name string) (Tag, bool) {
|
||||||
// in the nodes slice.
|
tag, ok := t.tags[name]
|
||||||
func (t *Template) handleNewlines(nodes []ast.Node, i int) {
|
if ok {
|
||||||
if i != 0 {
|
return tag, true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastIndex := len(nodes) - 1
|
tag, ok = t.ns.getTag(name)
|
||||||
if i != lastIndex {
|
if ok {
|
||||||
if node, ok := nodes[i+1].(ast.Text); ok {
|
return tag, true
|
||||||
ni := bytes.IndexByte(node.Data, '\n')
|
|
||||||
if ni != -1 {
|
|
||||||
node.Data = node.Data[ni:]
|
|
||||||
}
|
|
||||||
nodes[i+1] = node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tag, ok = globalTags[name]
|
||||||
|
if ok {
|
||||||
|
return tag, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// execTag executes a tag
|
// 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) {
|
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 {
|
if !ok {
|
||||||
return 0, t.posError(node, "%w: %s", ErrNoSuchTag, node.Name.Value)
|
return 0, t.posError(node, "%w: %s", ErrNoSuchTag, node.Name.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.handleNewlines(nodes, i)
|
|
||||||
|
|
||||||
var block []ast.Node
|
var block []ast.Node
|
||||||
if node.HasBody {
|
if node.HasBody {
|
||||||
block = t.getBlock(nodes, i+1, node.Position.Line, node.Name.Value)
|
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
|
// execFuncCall executes a function call
|
||||||
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, ok := t.funcs[fc.Name.Value]
|
fn, err := t.getVar(fc.Name, local)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, t.posError(fc, "%w: %s", ErrNoSuchFunc, fc.Name.Value)
|
return nil, t.posError(fc, "%w: %s", ErrNoSuchFunc, fc.Name.Value)
|
||||||
}
|
}
|
||||||
return t.execFunc(fn, fc, fc.Params, local)
|
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 {
|
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 {
|
func validateFunc(t reflect.Type) error {
|
||||||
|
7
tags.go
7
tags.go
@ -6,25 +6,30 @@ import (
|
|||||||
"go.elara.ws/salix/internal/ast"
|
"go.elara.ws/salix/internal/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tag represents a tag in a Salix template
|
||||||
type Tag interface {
|
type Tag interface {
|
||||||
Run(tc *TagContext, block, args []ast.Node) error
|
Run(tc *TagContext, block, args []ast.Node) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultTags = map[string]Tag{
|
var globalTags = map[string]Tag{
|
||||||
"if": ifTag{},
|
"if": ifTag{},
|
||||||
"for": forTag{},
|
"for": forTag{},
|
||||||
|
"include": includeTag{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TagContext is passed to Tag implementations to allow them to control the interpreter
|
||||||
type TagContext struct {
|
type TagContext struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
t *Template
|
t *Template
|
||||||
local map[string]any
|
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 {
|
func (tc *TagContext) Execute(nodes []ast.Node, local map[string]any) error {
|
||||||
return tc.t.execute(tc.w, nodes, mergeMap(tc.local, local))
|
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) {
|
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))
|
||||||
}
|
}
|
||||||
|
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