Add namespaces and include tag, improve whitespace handling

This commit is contained in:
2023-10-29 15:47:47 -07:00
parent fb7010dae3
commit 76c0a48b87
12 changed files with 372 additions and 162 deletions

179
salix.go
View File

@@ -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
tags map[string]Tag
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
return newTmpl
}
// 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
}
// 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
}
// Make sure the tag map is never nil to avoid panics
if m == nil {
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.
// 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
}
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
// 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
}
func (t *Template) getTag(name string) (Tag, bool) {
tag, ok := t.tags[name]
if ok {
return tag, true
}
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:]
}
nodes[i+1] = node
}
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 {