salix/parse.go

193 lines
4.8 KiB
Go

package salix
import (
"bytes"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"go.elara.ws/salix/ast"
"go.elara.ws/salix/parser"
)
// NamedReader is a reader with a name
type NamedReader interface {
io.Reader
Name() string
}
// 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, parser.GlobalStore("name", name))
if err != nil {
return Template{}, err
}
t := Template{
ns: n,
name: name,
ast: astVal.([]ast.Node),
tags: map[string]Tag{},
vars: map[string]any{},
}
if n.WhitespaceMutations {
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. It uses the path as the name.
func (t *Namespace) ParseFile(path string) (Template, error) {
fl, err := os.Open(path)
if err != nil {
return Template{}, err
}
defer fl.Close()
return t.Parse(fl)
}
// ParseGlob parses all the files that were matched by the given glob
// nd adds them to the namespace.
func (t *Namespace) ParseGlob(glob string) error {
matches, err := filepath.Glob(glob)
if err != nil {
return err
}
for _, match := range matches {
_, err := t.ParseFile(match)
if err != nil {
return err
}
}
return nil
}
// 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 Template{}, err
}
defer fl.Close()
return t.ParseWithName(path, fl)
}
// ParseGlob parses all the files in the filesystem that were matched by the given glob
// and adds them to the namespace.
func (t *Namespace) ParseFSGlob(fsys fs.FS, glob string) error {
matches, err := fs.Glob(fsys, glob)
if err != nil {
return err
}
for _, match := range matches {
_, err := t.ParseFS(fsys, match)
if err != nil {
return err
}
}
return nil
}
// ParseString parses a string using the given filename.
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 *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
}