189 lines
5.4 KiB
Go
189 lines
5.4 KiB
Go
package step
|
|
|
|
import "os"
|
|
import "io"
|
|
import "fmt"
|
|
import "time"
|
|
import "io/fs"
|
|
import "errors"
|
|
import "context"
|
|
import "path/filepath"
|
|
import "html/template"
|
|
import "git.tebibyte.media/sashakoshka/go-util/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
|
|
// Providers is a slice of objects which can provide various
|
|
// functionality to the environment. In order to be used, values in this
|
|
// slice must implement one or more of the interfaces in provider.go.
|
|
Providers []Provider
|
|
// Config specifies configuration data. It is used to configure the
|
|
// environment, as well as any providers that are loaded.
|
|
Config Meta
|
|
|
|
documents usync.Monitor[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.NewMonitor(make(map[string] *Document))
|
|
this.funcMap = make(template.FuncMap)
|
|
for _, provider := range this.Providers {
|
|
if provider, ok := provider.(ConfigProcessor); ok {
|
|
err := provider.ProcessConfig(this.Config)
|
|
if err != nil { return err }
|
|
}
|
|
}
|
|
for _, provider := range this.Providers {
|
|
if provider, ok := provider.(Configurable); ok {
|
|
err := provider.Configure(this.Config)
|
|
if err != nil { return err }
|
|
}
|
|
if provider, ok := provider.(FuncProvider); ok {
|
|
funcMap := provider.FuncMap()
|
|
if funcMap == nil { continue }
|
|
for name, function := range funcMap {
|
|
this.funcMap[name] = function
|
|
}
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
// Load loads 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 loaded, and are returned on subsequent calls that have the
|
|
// same name.
|
|
func (this *Environment) Load (name string) (*Document, error) {
|
|
return this.LoadRelative(name, nil)
|
|
}
|
|
|
|
// LoadRelative is like Load, but treats the name as a path relative to the
|
|
// given document.
|
|
func (this *Environment) LoadRelative (name string, document *Document) (*Document, error) {
|
|
if document == nil {
|
|
name = filepath.Clean(name)
|
|
} else {
|
|
nam, err := document.Rel(name)
|
|
if err != nil { return nil, err }
|
|
name = nam
|
|
}
|
|
|
|
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.load(name, info.ModTime(), input)
|
|
}
|
|
|
|
// Parse is like Load, 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) Parse (name string, input io.Reader) (*Document, error) {
|
|
name = filepath.Clean(name)
|
|
return this.load(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) load (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 }
|
|
meta, body, err := SplitMeta(string(buffer))
|
|
if err != nil { return nil, err }
|
|
|
|
// assemble the document struct
|
|
document := &Document {
|
|
Meta: meta,
|
|
environment: this,
|
|
name: name,
|
|
parseTime: time.Now(),
|
|
template: template.New(name),
|
|
}
|
|
if author := meta.Get("author"); author != "" {
|
|
document.Author = author
|
|
}
|
|
if title := meta.Get("title"); title != "" {
|
|
document.Title = title
|
|
}
|
|
if extends := meta.Get("extends"); extends != "" {
|
|
document.Extends = extends
|
|
}
|
|
|
|
// add template functions
|
|
funcMap := make(template.FuncMap)
|
|
for name, function := range this.funcMap {
|
|
funcMap[name] = function
|
|
}
|
|
for _, provider := range this.Providers {
|
|
provider, ok := provider.(FuncProviderFor)
|
|
if !ok { continue }
|
|
funcMapFor := provider.FuncMapFor(document)
|
|
if funcMapFor == nil { continue }
|
|
for name, function := range funcMapFor {
|
|
funcMap[name] = function
|
|
}
|
|
}
|
|
err = this.addFuncsTo(document, funcMap)
|
|
if err != nil { return nil, err }
|
|
|
|
// parse template from the body
|
|
// TODO catch template errors here and offset their row number by the
|
|
// number of rows taken up by the front matter
|
|
_, err = document.template.Parse(body)
|
|
if err != nil { return nil, err }
|
|
|
|
documents[name] = document
|
|
return document, nil
|
|
}
|
|
|
|
func (this *Environment) addFuncsTo (document *Document, funcs template.FuncMap) (err error) {
|
|
defer func () {
|
|
if r := recover(); r != nil {
|
|
if rerr, ok := err.(error); ok {
|
|
err = rerr
|
|
} else {
|
|
err = errors.New(fmt.Sprint(r))
|
|
}
|
|
}
|
|
} ()
|
|
document.template.Funcs(funcs)
|
|
return err
|
|
}
|
|
|
|
// 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
|
|
}
|