Add namespaces and include tag, improve whitespace handling
This commit is contained in:
		
							
								
								
									
										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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										179
									
								
								salix.go
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								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 | ||||
| 	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 { | ||||
|   | ||||
							
								
								
									
										11
									
								
								tags.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								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{ | ||||
| 	"if":  ifTag{}, | ||||
| 	"for": forTag{}, | ||||
| 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 | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user