mirror of
https://git.sr.ht/~adnano/kiln
synced 2024-12-04 19:08:22 +01:00
354 lines
7.6 KiB
Go
354 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
pathpkg "path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Dir represents a directory.
|
|
type Dir struct {
|
|
Path string
|
|
Pages []*Page
|
|
Dirs []*Dir
|
|
index *Page // The index page.
|
|
feed []byte // Atom feed.
|
|
}
|
|
|
|
// Page represents a page.
|
|
type Page struct {
|
|
Title string
|
|
Date time.Time
|
|
Weight int
|
|
Path string `yaml:"-"`
|
|
FilePath string `yaml:"-"`
|
|
Content string `yaml:"-"`
|
|
Params map[string]interface{}
|
|
Prev *Page `yaml:"-"`
|
|
Next *Page `yaml:"-"`
|
|
}
|
|
|
|
// read reads from a directory and indexes the files and directories within it.
|
|
func (d *Dir) read(srcDir string, task *Task, cfg *Site) error {
|
|
return d._read(srcDir, "", task, cfg)
|
|
}
|
|
|
|
func (d *Dir) _read(srcDir, path string, task *Task, cfg *Site) error {
|
|
entries, err := ioutil.ReadDir(pathpkg.Join(srcDir, path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
path := pathpkg.Join(path, name)
|
|
if entry.IsDir() {
|
|
// Ignore directories beginning with "_"
|
|
if strings.HasPrefix(name, "_") {
|
|
continue
|
|
}
|
|
// Gather directory data
|
|
dir := &Dir{Path: "/" + path + "/"}
|
|
if err := dir._read(srcDir, path, task, cfg); err != nil {
|
|
return err
|
|
}
|
|
d.Dirs = append(d.Dirs, dir)
|
|
} else if ext := pathpkg.Ext(name); task.Match(ext) {
|
|
// Ignore pages beginning with "_" with the exception of _index pages
|
|
namePrefix := strings.TrimSuffix(name, ext)
|
|
if strings.HasPrefix(name, "_") && namePrefix != "_index" {
|
|
continue
|
|
}
|
|
|
|
srcPath := pathpkg.Join(srcDir, path)
|
|
content, err := ioutil.ReadFile(srcPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
page := &Page{}
|
|
|
|
// Try to parse the date from the page filename
|
|
const layout = "2006-01-02"
|
|
base := pathpkg.Base(path)
|
|
if len(base) >= len(layout) {
|
|
dateStr := base[:len(layout)]
|
|
if time, err := time.Parse(layout, dateStr); err == nil {
|
|
page.Date = time
|
|
// Remove the date from the path
|
|
base = base[len(layout):]
|
|
if len(base) > 0 {
|
|
// Remove a leading dash
|
|
if base[0] == '-' {
|
|
base = base[1:]
|
|
}
|
|
if len(base) > 0 {
|
|
dir := pathpkg.Dir(path)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
path = pathpkg.Join(dir, base)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract frontmatter from content
|
|
frontmatter, content := extractFrontmatter(content)
|
|
if len(frontmatter) != 0 {
|
|
if err := yaml.Unmarshal(frontmatter, page); err != nil {
|
|
log.Printf("failed to parse frontmatter for %q: %v", path, err)
|
|
}
|
|
|
|
// Trim leading newlines from content
|
|
content = bytes.TrimLeft(content, "\r\n")
|
|
}
|
|
|
|
if cmd, ok := task.Preprocess[strings.TrimPrefix(ext, ".")]; ok {
|
|
content = process(cmd, bytes.NewReader(content))
|
|
}
|
|
page.Content = string(content)
|
|
|
|
page.FilePath = path
|
|
|
|
if namePrefix == "_index" {
|
|
page.Path = d.Path
|
|
d.index = page
|
|
} else {
|
|
if namePrefix == "index" {
|
|
path = "/" + strings.TrimSuffix(path, name)
|
|
} else {
|
|
path = "/" + strings.TrimSuffix(path, ext)
|
|
if task.UglyURLs {
|
|
path += task.OutputExt
|
|
} else {
|
|
path += "/"
|
|
}
|
|
}
|
|
page.Path = path
|
|
if permalink, ok := cfg.permalinks[d.Path]; ok {
|
|
var b strings.Builder
|
|
permalink.Execute(&b, page)
|
|
page.Path = b.String()
|
|
}
|
|
d.Pages = append(d.Pages, page)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// process processes the directory's contents.
|
|
func (d *Dir) process(cfg *Site, task *Task) error {
|
|
if task.TemplateExt != "" {
|
|
// Create index
|
|
if d.index != nil {
|
|
tmpl, ok := cfg.templates.FindTemplate(d.Path, "index"+task.TemplateExt)
|
|
if ok {
|
|
var b strings.Builder
|
|
if err := tmpl.Execute(&b, d); err != nil {
|
|
return err
|
|
}
|
|
d.index.Content = b.String()
|
|
}
|
|
}
|
|
|
|
// Process pages
|
|
for i := range d.Pages {
|
|
var b strings.Builder
|
|
tmpl, ok := cfg.templates.FindTemplate(d.Path, "page"+task.TemplateExt)
|
|
if ok {
|
|
if err := tmpl.Execute(&b, d.Pages[i]); err != nil {
|
|
return err
|
|
}
|
|
d.Pages[i].Content = b.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Feed represents a feed.
|
|
type Feed struct {
|
|
Title string // Feed title.
|
|
Path string // Feed path.
|
|
Updated time.Time // Last updated time.
|
|
Entries []*Page // Feed entries.
|
|
}
|
|
|
|
// Create feeds
|
|
if title, ok := cfg.Feeds[d.Path]; ok {
|
|
var b bytes.Buffer
|
|
feed := &Feed{
|
|
Title: title,
|
|
Path: d.Path,
|
|
Updated: time.Now(),
|
|
Entries: d.Pages,
|
|
}
|
|
tmpl, ok := cfg.templates.FindTemplate(d.Path, "atom.xml")
|
|
if ok {
|
|
if err := tmpl.Execute(&b, feed); err != nil {
|
|
return err
|
|
}
|
|
d.feed = b.Bytes()
|
|
} else {
|
|
fmt.Printf("Warning: failed to generate feed %q: missing template \"atom.xml\"\n", title)
|
|
}
|
|
}
|
|
|
|
// Process subdirectories
|
|
for _, d := range d.Dirs {
|
|
if err := d.process(cfg, task); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// write writes the directory's contents to the provided destination path.
|
|
func (d *Dir) write(dstDir string, task *Task) error {
|
|
dirPath := pathpkg.Join(dstDir, d.Path)
|
|
|
|
// Write pages
|
|
pages := d.Pages
|
|
if d.index != nil {
|
|
pages = append(pages, d.index)
|
|
}
|
|
for _, page := range pages {
|
|
path := page.Path
|
|
if !task.UglyURLs || page == d.index {
|
|
path = pathpkg.Join(path, "index"+task.OutputExt)
|
|
}
|
|
var content []byte
|
|
if cmd := task.Postprocess; cmd != "" {
|
|
content = process(cmd, strings.NewReader(page.Content))
|
|
} else {
|
|
content = []byte(page.Content)
|
|
}
|
|
|
|
dstPath := pathpkg.Join(dstDir, path)
|
|
dir := pathpkg.Dir(dstPath)
|
|
os.MkdirAll(dir, 0755)
|
|
if err := os.WriteFile(dstPath, content, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Write the atom feed
|
|
if d.feed != nil {
|
|
const path = "atom.xml"
|
|
dstPath := pathpkg.Join(dirPath, path)
|
|
os.MkdirAll(dirPath, 0755)
|
|
if err := os.WriteFile(dstPath, d.feed, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Write subdirectories
|
|
for _, dir := range d.Dirs {
|
|
dir.write(dstDir, task)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sort sorts the directory's pages by weight, then date, then filepath.
|
|
func (d *Dir) sort() {
|
|
sort.Slice(d.Pages, func(i, j int) bool {
|
|
pi, pj := d.Pages[i], d.Pages[j]
|
|
return pi.FilePath < pj.FilePath
|
|
})
|
|
|
|
sort.SliceStable(d.Pages, func(i, j int) bool {
|
|
pi, pj := d.Pages[i], d.Pages[j]
|
|
return pi.Date.After(pj.Date)
|
|
})
|
|
|
|
sort.SliceStable(d.Pages, func(i, j int) bool {
|
|
pi, pj := d.Pages[i], d.Pages[j]
|
|
return pi.Weight < pj.Weight
|
|
})
|
|
|
|
for i := range d.Pages {
|
|
if i-1 >= 0 {
|
|
d.Pages[i].Prev = d.Pages[i-1]
|
|
}
|
|
if i+1 < len(d.Pages) {
|
|
d.Pages[i].Next = d.Pages[i+1]
|
|
}
|
|
}
|
|
|
|
// Sort subdirectories
|
|
for _, d := range d.Dirs {
|
|
d.sort()
|
|
}
|
|
}
|
|
|
|
// process runs a process command.
|
|
func process(command string, input io.Reader) []byte {
|
|
split := strings.Split(command, " ")
|
|
cmd := exec.Command(split[0], split[1:]...)
|
|
cmd.Stdin = input
|
|
cmd.Stderr = os.Stderr
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return output
|
|
}
|
|
|
|
func (d *Dir) Title() string {
|
|
return d.index.Title
|
|
}
|
|
|
|
func (d *Dir) Date() time.Time {
|
|
return d.index.Date
|
|
}
|
|
|
|
func (d *Dir) Content() string {
|
|
return d.index.Content
|
|
}
|
|
|
|
func (d *Dir) Params() map[string]interface{} {
|
|
return d.index.Params
|
|
}
|
|
|
|
func (d *Dir) getDir(path string) *Dir {
|
|
// XXX: This is inefficient
|
|
if d.Path == path {
|
|
return d
|
|
}
|
|
for _, dir := range d.Dirs {
|
|
if dir.Path == path {
|
|
return dir
|
|
}
|
|
}
|
|
for i := range d.Dirs {
|
|
if dir := d.Dirs[i].getDir(path); dir != nil {
|
|
return dir
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Dir) getPage(path string) *Page {
|
|
// XXX: This is inefficient
|
|
for _, page := range d.Pages {
|
|
if page.FilePath == path {
|
|
return page
|
|
}
|
|
}
|
|
for _, dir := range d.Dirs {
|
|
if page := dir.getPage(path); page != nil {
|
|
return page
|
|
}
|
|
}
|
|
return nil
|
|
}
|