mirror of
https://git.sr.ht/~adnano/kiln
synced 2024-10-30 01:13:08 +00:00
213 lines
4.5 KiB
Go
213 lines
4.5 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"text/template"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type Site struct {
|
||
|
Files map[string][]byte
|
||
|
Dirs map[string][]string
|
||
|
Pages map[string]*Page
|
||
|
Templates *template.Template
|
||
|
}
|
||
|
|
||
|
func LoadSite(srcDir string) (*Site, error) {
|
||
|
tmpl, err := template.New("templates").ParseGlob("templates/*.gmi")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
site := &Site{
|
||
|
Files: map[string][]byte{},
|
||
|
Dirs: map[string][]string{},
|
||
|
Pages: map[string]*Page{},
|
||
|
Templates: tmpl,
|
||
|
}
|
||
|
|
||
|
if err := site.ReadDir(srcDir, ""); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return site, nil
|
||
|
}
|
||
|
|
||
|
// ReadDir reads a directory and indexes the files and directories within it.
|
||
|
func (s *Site) ReadDir(srcDir string, dstDir string) error {
|
||
|
entries, err := ioutil.ReadDir(srcDir)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, entry := range entries {
|
||
|
name := entry.Name()
|
||
|
srcPath := filepath.Join(srcDir, name)
|
||
|
dstPath := filepath.Join(dstDir, name)
|
||
|
|
||
|
if entry.IsDir() {
|
||
|
s.Dirs[dstPath] = []string{}
|
||
|
s.ReadDir(srcPath, dstPath)
|
||
|
} else {
|
||
|
content, err := ioutil.ReadFile(srcPath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
s.Files[dstPath] = content
|
||
|
s.Dirs[dstDir] = append(s.Dirs[dstDir], dstPath)
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Write writes the contents of the Index to the provided destination directory.
|
||
|
func (s *Site) Write(dstDir string) error {
|
||
|
// Empty the destination directory
|
||
|
if err := os.RemoveAll(dstDir); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for path := range s.Files {
|
||
|
// 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
|
||
|
}
|
||
|
data := s.Files[path]
|
||
|
if _, err := f.Write(data); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Manipulate processes and manipulates the site's content.
|
||
|
func (s *Site) Manipulate() error {
|
||
|
for path := range s.Files {
|
||
|
switch filepath.Ext(path) {
|
||
|
case ".gmi":
|
||
|
// Gather page data
|
||
|
content := string(s.Files[path])
|
||
|
page := NewPage(path, content)
|
||
|
|
||
|
builder := &strings.Builder{}
|
||
|
tmpl := s.Templates.Lookup("page.gmi")
|
||
|
if err := tmpl.Execute(builder, page); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
page.Content = builder.String()
|
||
|
s.Pages[path] = page
|
||
|
s.Files[path] = []byte(page.Content)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for dir := range s.Dirs {
|
||
|
// Gather section data
|
||
|
section := NewSection(dir)
|
||
|
for _, path := range s.Dirs[dir] {
|
||
|
switch filepath.Ext(path) {
|
||
|
case ".gmi":
|
||
|
section.Pages = append(section.Pages, s.Pages[path])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
dstPath := filepath.Join(dir, "index.gmi")
|
||
|
builder := &strings.Builder{}
|
||
|
tmpl := s.Templates.Lookup("section.gmi")
|
||
|
if err := tmpl.Execute(builder, section); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
s.Files[dstPath] = []byte(builder.String())
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Page represents a page.
|
||
|
type Page struct {
|
||
|
// The path to this page.
|
||
|
Path string
|
||
|
// 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.
|
||
|
Content string
|
||
|
}
|
||
|
|
||
|
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)")
|
||
|
|
||
|
func NewPage(path string, content string) *Page {
|
||
|
page := &Page{
|
||
|
Path: path,
|
||
|
Permalink: "/" + path,
|
||
|
Content: content,
|
||
|
}
|
||
|
|
||
|
// Try to parse the date from the page filename
|
||
|
const layout = "2006-01-02"
|
||
|
base := filepath.Base(path)
|
||
|
if len(base) >= len(layout) {
|
||
|
dateStr := base[:len(layout)]
|
||
|
if date, err := time.Parse(layout, dateStr); err == nil {
|
||
|
page.Date = date
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Try to parse the title from the contents
|
||
|
if submatches := titleRE.FindStringSubmatch(content); submatches != nil {
|
||
|
page.Title = submatches[1]
|
||
|
}
|
||
|
|
||
|
return page
|
||
|
}
|
||
|
|
||
|
// Section represents a section (i.e., a directory).
|
||
|
type Section struct {
|
||
|
// The path to this section.
|
||
|
Path string
|
||
|
// The permalink to this section.
|
||
|
Permalink string
|
||
|
// The pages in this section.
|
||
|
Pages []*Page
|
||
|
}
|
||
|
|
||
|
// NewSection returns a new Section with the given path.
|
||
|
func NewSection(path string) *Section {
|
||
|
var permalink string
|
||
|
if path == "" {
|
||
|
permalink = "/"
|
||
|
} else {
|
||
|
permalink = "/" + path + "/"
|
||
|
}
|
||
|
return &Section{
|
||
|
Path: path,
|
||
|
Permalink: permalink,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Sort sorts pages by date.
|
||
|
func (s *Section) Sort() {
|
||
|
// TODO: Implement this
|
||
|
}
|