2020-09-22 23:46:30 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-09-29 14:57:15 +00:00
|
|
|
"bytes"
|
2020-09-29 18:10:49 +00:00
|
|
|
"fmt"
|
|
|
|
"html"
|
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-29 18:10:49 +00:00
|
|
|
|
|
|
|
"git.sr.ht/~adnano/gmi"
|
2020-09-22 23:46:30 +00:00
|
|
|
)
|
|
|
|
|
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 {
|
2020-09-29 15:33:17 +00:00
|
|
|
path := filepath.Join(dir.Permalink, "index.gmi")
|
2020-09-29 14:57:15 +00:00
|
|
|
var b bytes.Buffer
|
2020-09-23 18:12:07 +00:00
|
|
|
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
|
2020-09-29 18:10:49 +00:00
|
|
|
dirPath := filepath.Join(dstDir, d.Permalink)
|
2020-09-23 01:11:56 +00:00
|
|
|
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"
|
2020-09-29 18:10:49 +00:00
|
|
|
const meta = `<!DOCTYPE html>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
<link rel="stylesheet" href="/style.css">
|
|
|
|
<title>%s</title>
|
|
|
|
|
|
|
|
`
|
|
|
|
|
2020-09-29 14:57:15 +00:00
|
|
|
path = filepath.Join(p.Permalink, indexPath)
|
2020-09-29 18:10:49 +00:00
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
fmt.Fprintf(&b, meta, p.Title)
|
|
|
|
|
|
|
|
var pre bool
|
|
|
|
var list bool
|
2020-09-29 14:57:15 +00:00
|
|
|
r := bytes.NewReader(p.content)
|
2020-09-29 18:10:49 +00:00
|
|
|
text := gmi.Parse(r)
|
|
|
|
for _, l := range text {
|
|
|
|
if _, ok := l.(gmi.LineListItem); ok {
|
|
|
|
if !list {
|
|
|
|
list = true
|
|
|
|
fmt.Fprint(&b, "<ul>\n")
|
|
|
|
}
|
|
|
|
} else if list {
|
|
|
|
list = false
|
|
|
|
fmt.Fprint(&b, "</ul>\n")
|
|
|
|
}
|
|
|
|
switch l.(type) {
|
|
|
|
case gmi.LineLink:
|
|
|
|
link := l.(gmi.LineLink)
|
|
|
|
url := html.EscapeString(link.URL)
|
|
|
|
name := html.EscapeString(link.Name)
|
|
|
|
if name == "" {
|
|
|
|
name = url
|
|
|
|
}
|
|
|
|
fmt.Fprintf(&b, "<p><a href='%s'>%s</a></p>\n", url, name)
|
|
|
|
case gmi.LinePreformattingToggle:
|
|
|
|
pre = !pre
|
|
|
|
if pre {
|
|
|
|
fmt.Fprint(&b, "<pre>\n")
|
|
|
|
} else {
|
|
|
|
fmt.Fprint(&b, "</pre>\n")
|
|
|
|
}
|
|
|
|
case gmi.LinePreformattedText:
|
|
|
|
text := string(l.(gmi.LinePreformattedText))
|
|
|
|
fmt.Fprintf(&b, "%s\n", html.EscapeString(text))
|
|
|
|
case gmi.LineHeading1:
|
|
|
|
text := string(l.(gmi.LineHeading1))
|
|
|
|
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(text))
|
|
|
|
case gmi.LineHeading2:
|
|
|
|
text := string(l.(gmi.LineHeading2))
|
|
|
|
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(text))
|
|
|
|
case gmi.LineHeading3:
|
|
|
|
text := string(l.(gmi.LineHeading3))
|
|
|
|
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(text))
|
|
|
|
case gmi.LineListItem:
|
|
|
|
text := string(l.(gmi.LineListItem))
|
|
|
|
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(text))
|
|
|
|
case gmi.LineQuote:
|
|
|
|
text := string(l.(gmi.LineQuote))
|
|
|
|
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(text))
|
|
|
|
case gmi.LineText:
|
|
|
|
text := string(l.(gmi.LineText))
|
|
|
|
if text == "" {
|
|
|
|
fmt.Fprint(&b, "<br>\n")
|
|
|
|
} else {
|
|
|
|
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if pre {
|
|
|
|
fmt.Fprint(&b, "</pre>\n")
|
|
|
|
}
|
|
|
|
if list {
|
|
|
|
fmt.Fprint(&b, "</ul>\n")
|
|
|
|
}
|
|
|
|
content = b.Bytes()
|
2020-09-29 14:57:15 +00:00
|
|
|
return
|
|
|
|
}
|