kiln/kiln.go

326 lines
7.1 KiB
Go
Raw Normal View History

2020-09-22 23:46:30 +00:00
package main
import (
2020-09-29 14:57:15 +00:00
"bytes"
2020-09-22 23:46:30 +00:00
"io/ioutil"
"os"
"path/filepath"
"regexp"
2020-09-29 15:22:54 +00:00
"sort"
2020-09-22 23:46:30 +00:00
"strings"
"text/template"
"time"
)
2020-09-23 17:50:25 +00:00
// Site represents a kiln site.
2020-09-22 23:46:30 +00:00
type Site struct {
2020-09-23 01:11:56 +00:00
Static map[string][]byte // Static files
Directory *Directory // Site directory
Templates *template.Template // Templates
2020-09-22 23:46:30 +00:00
}
2020-09-23 01:11:56 +00:00
// LoadSite loads and returns a Site.
// It reads site content from the specified source directory.
2020-09-22 23:46:30 +00:00
func LoadSite(srcDir string) (*Site, error) {
tmpl, err := template.New("templates").ParseGlob("templates/*.gmi")
if err != nil {
return nil, err
}
site := &Site{
2020-09-23 01:11:56 +00:00
Static: map[string][]byte{},
Directory: NewDirectory(""),
2020-09-22 23:46:30 +00:00
Templates: tmpl,
}
2020-09-23 01:11:56 +00:00
if err := site.Directory.Read(site, srcDir, ""); err != nil {
2020-09-22 23:46:30 +00:00
return nil, err
}
return site, nil
}
// Write writes the contents of the Index to the provided destination directory.
2020-09-29 15:22:54 +00:00
func (s *Site) Write(dstDir string, format OutputFormat) error {
2020-09-22 23:46:30 +00:00
// Empty the destination directory
if err := os.RemoveAll(dstDir); err != nil {
return err
}
2020-09-23 01:11:56 +00:00
// Create the destination directory
2020-09-22 23:46:30 +00:00
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
2020-09-23 01:11:56 +00:00
// Write the static files
for path := range s.Static {
2020-09-22 23:46:30 +00:00
// Create any parent directories
if dir := filepath.Dir(path); dir != "." {
dstPath := filepath.Join(dstDir, dir)
if err := os.MkdirAll(dstPath, 0755); err != nil {
return err
}
}
// Write the file
dstPath := filepath.Join(dstDir, path)
f, err := os.Create(dstPath)
if err != nil {
return err
}
2020-09-23 01:11:56 +00:00
data := s.Static[path]
2020-09-22 23:46:30 +00:00
if _, err := f.Write(data); err != nil {
return err
}
}
2020-09-23 01:11:56 +00:00
// Write the directory
2020-09-29 15:22:54 +00:00
return s.Directory.Write(dstDir, format)
2020-09-22 23:46:30 +00:00
}
// Manipulate processes and manipulates the site's content.
2020-09-23 01:11:56 +00:00
func (s *Site) Manipulate(dir *Directory) error {
// Write the directory index file, if it doesn't exist
if dir.Index == nil {
path := filepath.Join(dir.Permalink, "index.gmi")
2020-09-29 14:57:15 +00:00
var b bytes.Buffer
tmpl := s.Templates.Lookup("index.gmi")
2020-09-28 23:54:48 +00:00
if tmpl != nil {
2020-09-29 14:57:15 +00:00
if err := tmpl.Execute(&b, dir); err != nil {
2020-09-28 23:54:48 +00:00
return err
}
2020-09-29 14:57:15 +00:00
content := b.Bytes()
2020-09-28 23:54:48 +00:00
permalink := filepath.Dir(path)
if permalink == "." {
permalink = ""
}
page := &Page{
Permalink: "/" + permalink,
2020-09-29 14:57:15 +00:00
content: content,
2020-09-28 23:54:48 +00:00
}
dir.Index = page
2020-09-23 01:11:56 +00:00
}
}
2020-09-22 23:46:30 +00:00
2020-09-23 01:11:56 +00:00
// Manipulate pages
for i := range dir.Pages {
2020-09-29 14:57:15 +00:00
var b bytes.Buffer
2020-09-23 01:11:56 +00:00
tmpl := s.Templates.Lookup("page.gmi")
2020-09-29 14:57:15 +00:00
if err := tmpl.Execute(&b, dir.Pages[i]); err != nil {
2020-09-22 23:46:30 +00:00
return err
}
2020-09-29 14:57:15 +00:00
dir.Pages[i].content = b.Bytes()
2020-09-22 23:46:30 +00:00
}
return nil
}
2020-09-29 15:22:54 +00:00
// Sort sorts the site's pages by date.
func (s *Site) Sort() {
s.Directory.Sort()
}
2020-09-22 23:46:30 +00:00
// Page represents a page.
type Page struct {
// The permalink to this page.
Permalink string
// The title of this page, parsed from the Gemini contents.
Title string
// The date of the page. Dates are specified in the filename.
// Ex: 2020-09-22-hello-world.gmi
Date time.Time
// The content of this page.
2020-09-29 14:57:15 +00:00
content []byte
}
func (p *Page) Content() string {
return string(p.content)
2020-09-22 23:46:30 +00:00
}
2020-09-23 01:15:03 +00:00
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?")
2020-09-22 23:46:30 +00:00
2020-09-23 17:50:25 +00:00
// NewPage returns a new Page with the given path and content.
2020-09-29 14:57:15 +00:00
func NewPage(path string, content []byte) *Page {
2020-09-22 23:46:30 +00:00
// Try to parse the date from the page filename
2020-09-23 01:11:56 +00:00
var date time.Time
2020-09-22 23:46:30 +00:00
const layout = "2006-01-02"
base := filepath.Base(path)
if len(base) >= len(layout) {
dateStr := base[:len(layout)]
2020-09-23 01:11:56 +00:00
if time, err := time.Parse(layout, dateStr); err == nil {
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 := filepath.Dir(path)
if dir == "." {
dir = ""
}
path = filepath.Join(dir, base)
}
2020-09-22 23:46:30 +00:00
}
}
// Try to parse the title from the contents
2020-09-23 01:11:56 +00:00
var title string
2020-09-29 14:57:15 +00:00
if submatches := titleRE.FindSubmatch(content); submatches != nil {
title = string(submatches[1])
2020-09-23 01:11:56 +00:00
// Remove the title from the contents
content = content[len(submatches[0]):]
2020-09-22 23:46:30 +00:00
}
2020-09-29 14:57:15 +00:00
permalink := strings.TrimSuffix(path, ".gmi")
2020-09-23 01:11:56 +00:00
return &Page{
2020-09-29 14:57:15 +00:00
Permalink: "/" + permalink + "/",
2020-09-23 01:11:56 +00:00
Title: title,
Date: date,
2020-09-29 14:57:15 +00:00
content: content,
2020-09-23 01:11:56 +00:00
}
2020-09-22 23:46:30 +00:00
}
2020-09-23 01:11:56 +00:00
// Directory represents a directory of pages.
type Directory struct {
// The permalink to this directory.
2020-09-22 23:46:30 +00:00
Permalink string
2020-09-23 01:11:56 +00:00
// The pages in this directory.
2020-09-22 23:46:30 +00:00
Pages []*Page
2020-09-23 01:11:56 +00:00
// The subdirectories of this directory.
Directories []*Directory
// The index file (index.gmi).
Index *Page
2020-09-22 23:46:30 +00:00
}
2020-09-23 17:50:25 +00:00
// NewDirectory returns a new Directory with the given path.
2020-09-23 01:11:56 +00:00
func NewDirectory(path string) *Directory {
2020-09-22 23:46:30 +00:00
var permalink string
if path == "" {
permalink = "/"
} else {
permalink = "/" + path + "/"
}
2020-09-23 01:11:56 +00:00
return &Directory{
2020-09-22 23:46:30 +00:00
Permalink: permalink,
}
}
2020-09-23 01:11:56 +00:00
// Read reads from a directory and indexes the files and directories within it.
func (d *Directory) Read(site *Site, srcDir string, path string) error {
entries, err := ioutil.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
name := entry.Name()
path := filepath.Join(path, name)
srcPath := filepath.Join(srcDir, path)
if entry.IsDir() {
// Gather directory data
dir := NewDirectory(path)
dir.Read(site, srcPath, path)
d.Directories = append(d.Directories, dir)
} else {
content, err := ioutil.ReadFile(srcPath)
if err != nil {
return err
}
switch filepath.Ext(name) {
case ".gmi", ".gemini":
// Gather page data
page := NewPage(path, content)
if name == "index.gmi" {
d.Index = page
} else {
d.Pages = append(d.Pages, page)
}
default:
// Static file
site.Static[path] = content
}
}
}
return nil
}
// Write writes the Directory to the provided destination path.
2020-09-29 14:57:15 +00:00
func (d *Directory) Write(dstDir string, format OutputFormat) error {
2020-09-23 01:11:56 +00:00
// Create the directory
dirPath := filepath.Join(dstDir, d.Path)
if err := os.MkdirAll(dirPath, 0755); err != nil {
return err
}
// Write the files
for _, page := range d.Pages {
2020-09-29 14:57:15 +00:00
path, content := format(page)
dstPath := filepath.Join(dstDir, path)
dir := filepath.Dir(dstPath)
os.MkdirAll(dir, 0755)
2020-09-23 01:11:56 +00:00
f, err := os.Create(dstPath)
if err != nil {
return err
}
2020-09-29 14:57:15 +00:00
if _, err := f.Write(content); err != nil {
2020-09-23 01:11:56 +00:00
return err
}
}
// Write the index file
if d.Index != nil {
2020-09-29 14:57:15 +00:00
path, content := format(d.Index)
dstPath := filepath.Join(dstDir, path)
2020-09-23 01:11:56 +00:00
f, err := os.Create(dstPath)
if err != nil {
return err
}
2020-09-29 14:57:15 +00:00
if _, err := f.Write(content); err != nil {
2020-09-23 01:11:56 +00:00
return err
}
}
// Write subdirectories
for _, dir := range d.Directories {
2020-09-29 14:57:15 +00:00
dir.Write(dstDir, format)
2020-09-23 01:11:56 +00:00
}
return nil
2020-09-22 23:46:30 +00:00
}
2020-09-29 14:57:15 +00:00
2020-09-29 15:22:54 +00:00
// Sort sorts the directory's pages by date.
func (d *Directory) Sort() {
sort.Slice(d.Pages, func(i, j int) bool {
return d.Pages[i].Date.After(d.Pages[j].Date)
})
// Sort subdirectories
for _, d := range d.Directories {
d.Sort()
}
}
2020-09-29 14:57:15 +00:00
// OutputFormat represents an output format.
type OutputFormat func(*Page) (path string, content []byte)
func OutputGemini(p *Page) (path string, content []byte) {
const indexPath = "index.gmi"
path = filepath.Join(p.Permalink, indexPath)
content = p.content
return
}
func OutputHTML(p *Page) (path string, content []byte) {
const indexPath = "index.html"
path = filepath.Join(p.Permalink, indexPath)
r := bytes.NewReader(p.content)
content = GeminiToHTML(r)
return
}