0
0
Fork 0
zblog/main.go

218 lines
5.4 KiB
Go
Raw Permalink Normal View History

2022-03-25 14:41:22 +01:00
package main
import (
"bytes"
"flag"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
"path"
"path/filepath"
"time"
"github.com/russross/blackfriday/v2"
)
var (
contentDir = flag.String("content-dir", "content", "path to the content directory")
staticDir = flag.String("static-dir", "static", "path to the static files")
templateDir = flag.String("template-dir", "templates", "path to the template directory")
outputDir = flag.String("output-dir", "", "path to output all files from the render process")
listen = flag.String("listen", "", "When provided with a listen port, start serving the content")
)
type (
Metadata struct {
URLPath string
FilePath string
Template string
Title string
Date time.Time
Author string
Draft bool
}
)
func main() {
flag.Parse()
var err error
tmplDirFS := os.DirFS(*templateDir)
templates := template.New("")
templates = templates.Funcs(template.FuncMap(
map[string]interface{}{
"formatTime": func(t time.Time) string {
return t.Format("2006-01-02")
},
},
))
templates, err = templates.ParseFS(tmplDirFS, "*")
if err != nil {
log.Fatalf("could not parse template files: %s", err)
}
content := []Metadata{}
if err := filepath.Walk(*contentDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
m, err := MetadataFromFile(*contentDir, path)
if err != nil {
return fmt.Errorf("could not parse metadata from '%s': %w", path, err)
}
content = append(content, m)
return nil
}); err != nil {
log.Fatalf("could not read content: %s", err)
}
if *outputDir != "" {
for _, metadata := range content {
p := *outputDir + metadata.URLPath
if p[len(p)-1] == '/' {
p = path.Join(p, "index.html")
}
// create directory
if _, err := os.Stat(path.Dir(p)); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(p), 0755); err != nil {
log.Fatalf("could not create directory '%s': %s", path.Dir(p), err)
}
}
f, err := os.Create(p)
if err != nil {
log.Fatalf("could not create new file '%s': %s", p, err)
}
defer f.Close()
if err := metadata.Render(f, templates); err != nil {
log.Fatalf("could not render '%s': %s", metadata.FilePath, err)
}
f.Close()
}
}
if *listen != "" {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(*staticDir))))
for _, metadata := range content {
func(m Metadata) {
http.HandleFunc(m.URLPath, func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s -> %s", r.URL, m.URLPath)
w.Header()["Content-Type"] = []string{"text/html"}
if err := m.Render(w, templates); err != nil {
log.Printf("could not render '%s': %s", m.FilePath, err)
}
})
}(metadata)
}
log.Fatalf("stopped listening: %s", http.ListenAndServe(*listen, nil))
}
if *outputDir == "" && *listen == "" {
log.Printf("neither output-dir nor listen are requested - doing nothing")
}
}
var (
metadataStart = []byte("+++\n")
metadataEnd = []byte("\n+++\n")
headerTitle = "title"
headerDate = "date"
headerAuthor = "author"
headerURLPath = "url"
headerDraft = "draft"
headerTemplate = "template"
)
// ContentFromFile reads the header of the file to create the metadata.
//
// basePath is stripped from the path when generating the default URL path.
func MetadataFromFile(basePath string, path string) (Metadata, error) {
m := Metadata{
FilePath: path,
URLPath: path[len(basePath):],
Template: "content.html",
}
raw, err := os.ReadFile(m.FilePath)
if err != nil {
return m, err
}
if !bytes.HasPrefix(raw, metadataStart) {
return m, fmt.Errorf("missing metadata header, must start with +++")
}
last := bytes.Index(raw, metadataEnd)
if last == -1 {
return m, fmt.Errorf("missing metadata header, must end with +++ on a single line")
}
rawHeader := raw[len(metadataStart):last]
lineNum := 0
for _, headerLine := range bytes.Split(rawHeader, []byte("\n")) {
if len(headerLine) == 0 {
continue
}
line := bytes.SplitN(headerLine, []byte("="), 2)
if len(line) != 2 {
return m, fmt.Errorf("line %d: format must be 'key = value'", lineNum)
}
key := string(bytes.Trim(line[0], " "))
val := string(bytes.Trim(line[1], ` "'`))
switch string(key) {
case headerTitle:
m.Title = val
case headerAuthor:
m.Author = val
case headerDraft:
if headerDraft == "true" {
m.Draft = true
}
case headerTemplate:
m.Template = val
case headerDate:
m.Date, err = time.Parse(time.RFC3339, val)
if err != nil {
log.Printf("line %d: date must match RFC3339 format", lineNum)
}
case headerURLPath:
m.URLPath = val
default:
log.Printf("line %d: unknown header %s found in %s", lineNum, key, path)
}
lineNum += 1
}
return m, nil
}
func (m Metadata) Content() template.HTML {
result := ""
raw, err := os.ReadFile(m.FilePath)
if err != nil {
log.Printf("error reading file: %w", err)
return template.HTML("")
}
end := bytes.Index(raw, metadataEnd)
if end == -1 {
log.Printf("could not find metadata end")
return template.HTML("")
}
result = string(blackfriday.Run(raw[end+len(metadataEnd):]))
return template.HTML(result)
}
func (m Metadata) Render(w io.Writer, tmpl *template.Template) error {
if err := tmpl.ExecuteTemplate(w, m.Template, m); err != nil {
return fmt.Errorf("could not render content path '%s': %w", m.FilePath, err)
}
return nil
}