140 lines
3.3 KiB
Go
140 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/pelletier/go-toml"
|
|
"github.com/yuin/goldmark"
|
|
)
|
|
|
|
const (
|
|
tomlStart = "+++\n"
|
|
tomlEnd = "\n+++\n"
|
|
)
|
|
|
|
type (
|
|
CatalogOptions struct {
|
|
WithUnpublished bool // render unpublished documents?
|
|
DefaultTemplate string // the default template name
|
|
}
|
|
|
|
Catalog struct {
|
|
Categories map[string]Category
|
|
Content map[string]*Content
|
|
}
|
|
|
|
Category struct {
|
|
Content map[string]*Content
|
|
}
|
|
|
|
Content struct {
|
|
// Metadata
|
|
Path string
|
|
Title string // the Title of this post
|
|
Author string // the list of authors that worked on the post
|
|
Published bool // Is the content published?
|
|
Time time.Time // the Time this post was published
|
|
Categories []string // The different Categories the content falls into
|
|
Template string // Template to use to render
|
|
Content template.HTML // the rendered Content
|
|
}
|
|
|
|
Responses map[string]*Response
|
|
|
|
Response struct {
|
|
Header http.Header
|
|
Modified time.Time
|
|
Content []byte
|
|
}
|
|
)
|
|
|
|
func NewCatalog(files fs.FS, cfg CatalogOptions) (*Catalog, error) {
|
|
if cfg.DefaultTemplate == "" {
|
|
return nil, fmt.Errorf("no default template set")
|
|
}
|
|
cat := &Catalog{
|
|
Content: map[string]*Content{},
|
|
}
|
|
|
|
if err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
raw, err := fs.ReadFile(files, path)
|
|
if err != nil {
|
|
return fmt.Errorf("could not read file '%s': %w", path, err)
|
|
}
|
|
cont := &Content{Path: path}
|
|
err = parseContent(raw, cont)
|
|
if err != nil {
|
|
return fmt.Errorf("file '%s' could not be parsed: %w", path, err)
|
|
}
|
|
if !cont.Published && !cfg.WithUnpublished {
|
|
return nil
|
|
}
|
|
if cont.Template == "" {
|
|
cont.Template = cfg.DefaultTemplate
|
|
}
|
|
cat.Content[cont.Path] = cont
|
|
return nil
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("error occured when parsing content files: %w", err)
|
|
}
|
|
return cat, nil
|
|
}
|
|
|
|
func (c *Catalog) Render(tmpl *template.Template) (Responses, error) {
|
|
pages := map[string]*Response{}
|
|
buf := &bytes.Buffer{}
|
|
for path, content := range c.Content {
|
|
buf.Reset()
|
|
err := tmpl.ExecuteTemplate(buf, content.Template, content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rendering content '%s' failed: %w", path, err)
|
|
}
|
|
pages[path] = &Response{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"text/html"},
|
|
},
|
|
Modified: content.Time,
|
|
Content: buf.Bytes(),
|
|
}
|
|
}
|
|
return (Responses)(pages), nil
|
|
}
|
|
|
|
func parseContent(raw []byte, c *Content) error {
|
|
if !bytes.HasPrefix(raw, []byte(tomlStart)) {
|
|
return fmt.Errorf("no metadata header found")
|
|
}
|
|
end := bytes.Index(raw, []byte(tomlEnd))
|
|
if end < 0 {
|
|
return fmt.Errorf("incomplete header")
|
|
}
|
|
header, err := toml.LoadBytes(raw[len(tomlStart) : end+1])
|
|
if err != nil {
|
|
return fmt.Errorf("could not load header: %w", err)
|
|
}
|
|
if err := header.Unmarshal(c); err != nil {
|
|
return fmt.Errorf("could not parse header: %w", err)
|
|
}
|
|
raw = raw[end+len(tomlEnd):]
|
|
|
|
md := goldmark.New()
|
|
var buf bytes.Buffer
|
|
if err := md.Convert(raw, &buf); err != nil {
|
|
return fmt.Errorf("could not render content: %w", err)
|
|
}
|
|
c.Content = template.HTML(buf.String())
|
|
return nil
|
|
}
|
|
|
|
func (c *Content) String() string {
|
|
return fmt.Sprintf("file '%s' - title '%s'", c.Path, c.Title)
|
|
}
|