package step import "os" import "io" import "time" import "strings" import "path/filepath" import "html/template" // Document represents a STEP file. type Document struct { Author string Title string Extends string Meta Meta // WORM: environment *Environment name string parseTime time.Time template *template.Template } // Execute executes this document and writes the result to the output. This // method is safe for concurrent use by multiple goroutines. func (this *Document) Execute (output io.Writer, data ExecutionData) error { if this.Extends == "" { // no parent return this.template.Execute(output, data) } // execute into string builder outputBuilder := strings.Builder { } // TODO catch template errors here and offset their row number by the // number of rows taken up by the front matter err := this.template.Execute(&outputBuilder, data) // ignore ErrExecutionCanceled, because it's meant to gracefully stop // the template execution if err != nil { return err } // execute parent with this document's result parent, err := this.environment.LoadRelative(this.Extends, this) if err != nil { return err } return parent.Execute(output, ExecutionData { Data: data.Data, Child: &ExecutionResult { Author: this.Author, Title: this.Title, Extends: this.Extends, Meta: this.Meta, Body: template.HTML(outputBuilder.String()), }, }) } // Environment returns the environment this document is in. func (this *Document) Environment () *Environment { return this.environment } // Abs returns an absolute representation of the given path. If the path is an // absolute path already, it is returned as-is. If the path is a relative path, // it is treated as relative to the current working directory. If the path is a // relative path beginning with "." or "..", it is treated as relative to this // document. The result is cleaned. func (this *Document) Abs (name string) (string, error) { if filepath.IsAbs(name) { return filepath.Clean(name), nil } if strings.HasPrefix(name, ".") { directory := this.dir() return filepath.Abs(filepath.Join(directory, name)) } return filepath.Abs(name) } // Rel returns the given path relative to the current working directory. As a // special case, any relative path beginning with "." or ".." is treated as // being relative to this document before it is made relative to the current // working directory. The result is cleaned. func (this *Document) Rel (name string) (string, error) { if filepath.IsAbs(name) { cwd, err := os.Getwd() if err != nil { return "", err } return filepath.Rel(cwd, name) } if strings.HasPrefix(name, ".") { directory := this.dir() return filepath.Join(directory, name), nil } return name, nil } // Name returns the document's name, which is a path relative to its // environment. func (this *Document) Name () string { return this.name } func (this *Document) dir () string { directory := this.name ext := filepath.Ext(directory) if ext != "" { directory = filepath.Dir(directory) } return directory } // ExecutionData is data made available to documents as they are being exeucted. type ExecutionData struct { Data any // Custom data Child *ExecutionResult // The child document's result, if applicable } // ExecutionResult is the result of executing a document. type ExecutionResult struct { Author string Title string Extends string Meta Meta Body template.HTML }