137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package step
|
|
|
|
import "os"
|
|
import "io"
|
|
import "time"
|
|
import "io/fs"
|
|
import "context"
|
|
import "path/filepath"
|
|
import "html/template"
|
|
import "git.tebibyte.media/sashakoshka/goutil/sync"
|
|
|
|
// TODO change Parse to Load, ParseReader to Parse
|
|
|
|
// Environment is an execution environment for STEP documents. It can parse
|
|
// documents into itself and resolve dependencies among them.
|
|
type Environment struct {
|
|
// FS, if specified, is the filesystem that this environment will load
|
|
// documents from. Its methods must be safe for concurrent execution.
|
|
FS fs.FS
|
|
// FuncProviders is a slice of FuncMap providers whoose functions will
|
|
// be available to any documents this environment processes.
|
|
FuncProviders []FuncProvider
|
|
|
|
documents usync.Locker[map[string] *Document]
|
|
funcMap template.FuncMap
|
|
}
|
|
|
|
// Init must be called before utilizng the environment.
|
|
func (this *Environment) Init (ctx context.Context) error {
|
|
this.documents = usync.NewLocker(make(map[string] *Document))
|
|
this.funcMap = make(template.FuncMap)
|
|
for _, provider := range this.FuncProviders {
|
|
funcMap := provider.FuncMap()
|
|
if funcMap == nil { continue }
|
|
for name, function := range funcMap {
|
|
this.funcMap[name] = function
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
// Parse parses the named document and returns it. The name is treated as a file
|
|
// path relative to the current working directory. Documents are cached into the
|
|
// environment once parsed, and are returned on subsequent calls that have the
|
|
// same name.
|
|
func (this *Environment) Parse (name string) (*Document, error) {
|
|
name = filepath.Clean(name)
|
|
input, err := this.GetFS().Open(name)
|
|
if err != nil {
|
|
// if the file couldn't be opened, delete from cache in case the
|
|
// file was deleted
|
|
this.Unload(name)
|
|
return nil, err
|
|
}
|
|
defer input.Close()
|
|
info, err := input.Stat()
|
|
if err != nil { return nil, err }
|
|
return this.parse(name, info.ModTime(), input)
|
|
}
|
|
|
|
// ParseReader is like Parse, but parses a reader and just takes your word for
|
|
// it that it corresponds to the file of the given name. It always re-parses.
|
|
func (this *Environment) ParseReader (name string, input io.Reader) (*Document, error) {
|
|
name = filepath.Clean(name)
|
|
return this.parse(name, time.Now(), input)
|
|
}
|
|
|
|
// Unload removes a named document. It will be reloaded if necessary.
|
|
func (this *Environment) Unload (name string) {
|
|
documents, done := this.documents.Borrow()
|
|
defer done()
|
|
delete(documents, name)
|
|
}
|
|
|
|
func (this *Environment) parse (name string, modTime time.Time, input io.Reader) (*Document, error) {
|
|
documents, done := this.documents.Borrow()
|
|
defer done()
|
|
|
|
// check cache
|
|
if document, ok := documents[name]; ok {
|
|
if modTime.Before(document.parseTime) {
|
|
return document, nil
|
|
}
|
|
}
|
|
|
|
// read entire file and split into front matter and body
|
|
buffer, err := io.ReadAll(input)
|
|
if err != nil { return nil, err }
|
|
frontMatter, body, err := SplitFrontMatter(string(buffer))
|
|
if err != nil { return nil, err }
|
|
|
|
// assemble the document struct
|
|
document := &Document {
|
|
environment: this,
|
|
name: name,
|
|
parseTime: time.Now(),
|
|
frontMatter: frontMatter,
|
|
template: template.New(name),
|
|
}
|
|
if author, ok := frontMatter["author"]; ok {
|
|
document.Author = author
|
|
}
|
|
if title, ok := frontMatter["title"]; ok {
|
|
document.Title = title
|
|
}
|
|
if extends, ok := frontMatter["extends"]; ok {
|
|
document.Extends = extends
|
|
}
|
|
|
|
// parse template from the body
|
|
document.template.Funcs(this.funcMap)
|
|
_, err = document.template.Parse(body)
|
|
if err != nil { return nil, err }
|
|
|
|
// add template functions which need a document pointer for context
|
|
for _, provider := range this.FuncProviders {
|
|
provider, ok := provider.(FuncProviderFor)
|
|
if !ok { continue }
|
|
funcMap := provider.FuncMapFor(document)
|
|
if funcMap == nil { continue }
|
|
for name, function := range funcMap {
|
|
this.funcMap[name] = function
|
|
}
|
|
}
|
|
|
|
documents[name] = document
|
|
return document, nil
|
|
}
|
|
|
|
// GetFS returns the effective filesystem that the environment uses.
|
|
func (this *Environment) GetFS () fs.FS {
|
|
if this.FS == nil {
|
|
return os.DirFS(".")
|
|
}
|
|
return this.FS
|
|
}
|