mirror of
https://git.sr.ht/~adnano/kiln
synced 2024-10-30 01:13:08 +00:00
Refactor
This commit is contained in:
parent
65ff96bffa
commit
919cdd7556
76
html.go
Normal file
76
html.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
|
||||||
|
"git.sr.ht/~adnano/gmi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// gmiToHTML converts the provided Gemini text to HTML.
|
||||||
|
func gmiToHTML(text gmi.Text) []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
var pre bool
|
||||||
|
var list bool
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
421
kiln.go
421
kiln.go
|
@ -1,421 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.sr.ht/~adnano/gmi"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Site represents a kiln site.
|
|
||||||
type Site struct {
|
|
||||||
Title string // Site title.
|
|
||||||
URL string // Site URL.
|
|
||||||
dir *Dir // Site directory.
|
|
||||||
tmpl *template.Template // Templates.
|
|
||||||
feeds bool // Whether or not to generate feeds.
|
|
||||||
}
|
|
||||||
|
|
||||||
// load loads the Site from the current directory.
|
|
||||||
func (s *Site) load() error {
|
|
||||||
// Load content
|
|
||||||
const srcDir = "src"
|
|
||||||
s.dir = NewDir("")
|
|
||||||
if err := site.dir.Read(srcDir, ""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Load templates
|
|
||||||
const tmplDir = "templates"
|
|
||||||
s.tmpl = template.New(tmplDir)
|
|
||||||
// Add default templates
|
|
||||||
s.tmpl.AddParseTree("atom.xml", atomTmpl.Tree)
|
|
||||||
s.tmpl.AddParseTree("index.gmi", indexTmpl.Tree)
|
|
||||||
s.tmpl.AddParseTree("page.gmi", pageTmpl.Tree)
|
|
||||||
// Add function to get site
|
|
||||||
s.tmpl = s.tmpl.Funcs(template.FuncMap{
|
|
||||||
"site": func() *Site { return s },
|
|
||||||
})
|
|
||||||
var err error
|
|
||||||
s.tmpl, err = s.tmpl.ParseGlob(filepath.Join(tmplDir, "*.gmi"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// write writes the contents of the Index to the provided destination directory.
|
|
||||||
func (s *Site) write(dstDir string, format OutputFormat) error {
|
|
||||||
// Empty the destination directory
|
|
||||||
if err := os.RemoveAll(dstDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Create the destination directory
|
|
||||||
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Write the directory
|
|
||||||
return s.dir.Write(dstDir, format)
|
|
||||||
}
|
|
||||||
|
|
||||||
// manipulate processes and manipulates the site's content.
|
|
||||||
func (s *Site) manipulate() error {
|
|
||||||
// FIXME: manipulate doesn't descend into subdirectories
|
|
||||||
// Write the directory index file, if it doesn't exist
|
|
||||||
const indexPath = "index.gmi"
|
|
||||||
if s.dir.index == nil {
|
|
||||||
var b bytes.Buffer
|
|
||||||
tmpl := s.tmpl.Lookup(indexPath)
|
|
||||||
if err := tmpl.Execute(&b, s.dir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.dir.index = &Page{
|
|
||||||
Permalink: s.dir.Permalink,
|
|
||||||
content: b.Bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Manipulate pages
|
|
||||||
const pagePath = "page.gmi"
|
|
||||||
for i := range s.dir.Pages {
|
|
||||||
var b bytes.Buffer
|
|
||||||
tmpl := s.tmpl.Lookup(pagePath)
|
|
||||||
if err := tmpl.Execute(&b, s.dir.Pages[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.dir.Pages[i].content = b.Bytes()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort sorts the site's pages by date.
|
|
||||||
func (s *Site) sort() {
|
|
||||||
s.dir.Sort()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feed represents a feed.
|
|
||||||
type Feed struct {
|
|
||||||
Title string // Feed title.
|
|
||||||
Path string // Feed path.
|
|
||||||
URL string // Site URL.
|
|
||||||
Updated time.Time // Last updated time.
|
|
||||||
Entries []*Page // Feed entries.
|
|
||||||
}
|
|
||||||
|
|
||||||
// createFeeds creates Atom feeds.
|
|
||||||
func (s *Site) createFeeds() error {
|
|
||||||
const atomPath = "atom.xml"
|
|
||||||
var b bytes.Buffer
|
|
||||||
tmpl := s.tmpl.Lookup(atomPath)
|
|
||||||
feed := &Feed{
|
|
||||||
Title: s.Title,
|
|
||||||
Path: filepath.Join(s.dir.Permalink, atomPath),
|
|
||||||
URL: s.URL,
|
|
||||||
Updated: time.Now(),
|
|
||||||
Entries: s.dir.Pages,
|
|
||||||
}
|
|
||||||
if err := tmpl.Execute(&b, feed); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
path := filepath.Join(s.dir.Permalink, atomPath)
|
|
||||||
s.dir.Files[path] = b.Bytes()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page represents a page.
|
|
||||||
type Page struct {
|
|
||||||
Title string // The title of this page.
|
|
||||||
Permalink string // The permalink to this page.
|
|
||||||
Date time.Time // The date of the page.
|
|
||||||
content []byte // The content of this page.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content returns the page content as a string.
|
|
||||||
// Used in templates.
|
|
||||||
func (p *Page) Content() string {
|
|
||||||
return string(p.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regexp to parse title from Gemini files
|
|
||||||
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?")
|
|
||||||
|
|
||||||
// NewPage returns a new Page with the given path and content.
|
|
||||||
func NewPage(path string, content []byte) *Page {
|
|
||||||
// Try to parse the date from the page filename
|
|
||||||
var date time.Time
|
|
||||||
const layout = "2006-01-02"
|
|
||||||
base := filepath.Base(path)
|
|
||||||
if len(base) >= len(layout) {
|
|
||||||
dateStr := base[:len(layout)]
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the title from the contents
|
|
||||||
var title string
|
|
||||||
if submatches := titleRE.FindSubmatch(content); submatches != nil {
|
|
||||||
title = string(submatches[1])
|
|
||||||
// Remove the title from the contents
|
|
||||||
content = content[len(submatches[0]):]
|
|
||||||
}
|
|
||||||
|
|
||||||
permalink := strings.TrimSuffix(path, ".gmi")
|
|
||||||
|
|
||||||
return &Page{
|
|
||||||
Permalink: "/" + permalink + "/",
|
|
||||||
Title: title,
|
|
||||||
Date: date,
|
|
||||||
content: content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dir represents a directory.
|
|
||||||
type Dir struct {
|
|
||||||
Permalink string // Permalink to this directory.
|
|
||||||
Pages []*Page // Pages in this directory.
|
|
||||||
Dirs []*Dir // Subdirectories.
|
|
||||||
Files map[string][]byte // Static files.
|
|
||||||
index *Page // The index file (index.gmi).
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDir returns a new Dir with the given path.
|
|
||||||
func NewDir(path string) *Dir {
|
|
||||||
var permalink string
|
|
||||||
if path == "" {
|
|
||||||
permalink = "/"
|
|
||||||
} else {
|
|
||||||
permalink = "/" + path + "/"
|
|
||||||
}
|
|
||||||
return &Dir{
|
|
||||||
Permalink: permalink,
|
|
||||||
Files: map[string][]byte{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads from a directory and indexes the files and directories within it.
|
|
||||||
func (d *Dir) Read(srcDir string, path string) error {
|
|
||||||
entries, err := ioutil.ReadDir(filepath.Join(srcDir, path))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
name := entry.Name()
|
|
||||||
// Ignore names that start with '_'
|
|
||||||
if strings.HasPrefix(name, "_") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := filepath.Join(path, name)
|
|
||||||
if entry.IsDir() {
|
|
||||||
// Gather directory data
|
|
||||||
dir := NewDir(path)
|
|
||||||
if err := dir.Read(srcDir, path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.Dirs = append(d.Dirs, dir)
|
|
||||||
} else {
|
|
||||||
srcPath := filepath.Join(srcDir, path)
|
|
||||||
content, err := ioutil.ReadFile(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch filepath.Ext(name) {
|
|
||||||
case ".gmi":
|
|
||||||
// Gather page data
|
|
||||||
page := NewPage(path, content)
|
|
||||||
if name == "index.gmi" {
|
|
||||||
d.index = page
|
|
||||||
} else {
|
|
||||||
d.Pages = append(d.Pages, page)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Static file
|
|
||||||
d.Files[path] = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes the Dir to the provided destination path.
|
|
||||||
func (d *Dir) Write(dstDir string, format OutputFormat) error {
|
|
||||||
// Create the directory
|
|
||||||
dirPath := filepath.Join(dstDir, d.Permalink)
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write static files
|
|
||||||
for path := range d.Files {
|
|
||||||
dstPath := filepath.Join(dstDir, path)
|
|
||||||
f, err := os.Create(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data := d.Files[path]
|
|
||||||
if _, err := f.Write(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the files
|
|
||||||
for _, page := range d.Pages {
|
|
||||||
path, content := format(page)
|
|
||||||
dstPath := filepath.Join(dstDir, path)
|
|
||||||
dir := filepath.Dir(dstPath)
|
|
||||||
os.MkdirAll(dir, 0755)
|
|
||||||
f, err := os.Create(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := f.Write(content); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the index file
|
|
||||||
if d.index != nil {
|
|
||||||
path, content := format(d.index)
|
|
||||||
dstPath := filepath.Join(dstDir, path)
|
|
||||||
f, err := os.Create(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := f.Write(content); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write subdirectories
|
|
||||||
for _, dir := range d.Dirs {
|
|
||||||
dir.Write(dstDir, format)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort sorts the directory's pages by date.
|
|
||||||
func (d *Dir) 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.Dirs {
|
|
||||||
d.Sort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
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>
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
path = filepath.Join(p.Permalink, indexPath)
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
fmt.Fprintf(&b, meta, html.EscapeString(p.Title))
|
|
||||||
|
|
||||||
var pre bool
|
|
||||||
var list bool
|
|
||||||
r := bytes.NewReader(p.content)
|
|
||||||
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()
|
|
||||||
return
|
|
||||||
}
|
|
421
main.go
421
main.go
|
@ -1,58 +1,132 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~adnano/gmi"
|
"git.sr.ht/~adnano/gmi"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
serveSite bool
|
srcDir = "src" // site source
|
||||||
toHtml bool
|
dstDir = "dst" // site destination
|
||||||
toAtom bool
|
htmlDst = "dst.html" // html destination
|
||||||
site Site
|
indexPath = "index.gmi" // path used for index files
|
||||||
|
atomPath = "atom.xml" // path used for atom feeds
|
||||||
|
tmplDir = "templates/" // templates directory
|
||||||
|
atomTmpl = "atom.xml" // path to atom template
|
||||||
|
indexTmpl = "index.gmi" // path to index template
|
||||||
|
pageTmpl = "page.gmi" // path to page template
|
||||||
|
htmlTmpl = "output.html" // path to html template
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// site config
|
||||||
|
var cfg struct {
|
||||||
|
title string // site title
|
||||||
|
url string // site URL
|
||||||
|
toAtom bool // output Atom
|
||||||
|
toHTML bool // output HTML
|
||||||
|
serveSite bool // serve the site
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&serveSite, "serve", false, "serve the site")
|
flag.StringVar(&cfg.title, "title", "", "site title")
|
||||||
flag.BoolVar(&toHtml, "html", false, "output HTML")
|
flag.StringVar(&cfg.url, "url", "", "site URL")
|
||||||
flag.BoolVar(&toAtom, "atom", false, "output Atom feed")
|
flag.BoolVar(&cfg.toAtom, "atom", false, "output Atom feed")
|
||||||
flag.StringVar(&site.Title, "title", "", "site title")
|
flag.BoolVar(&cfg.toHTML, "html", false, "output HTML")
|
||||||
flag.StringVar(&site.URL, "url", "", "site URL")
|
flag.BoolVar(&cfg.serveSite, "serve", false, "serve the site")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if err := build(); err != nil {
|
if err := run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if cfg.serveSite {
|
||||||
if serveSite {
|
|
||||||
serve()
|
serve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the site
|
// site metadata passed to templates
|
||||||
func build() error {
|
type site struct {
|
||||||
if err := site.load(); err != nil {
|
Title string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// map of paths to templates
|
||||||
|
var templates = map[string]*template.Template{}
|
||||||
|
|
||||||
|
// template functions
|
||||||
|
var funcMap = template.FuncMap{
|
||||||
|
"site": func() site {
|
||||||
|
return site{
|
||||||
|
Title: cfg.title,
|
||||||
|
URL: cfg.url,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTemplate loads a template from the provided path and content.
|
||||||
|
func loadTemplate(path string, content string) {
|
||||||
|
tmpl := template.New(path)
|
||||||
|
tmpl.Funcs(funcMap)
|
||||||
|
template.Must(tmpl.Parse(content))
|
||||||
|
templates[path] = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
// Load default templates
|
||||||
|
loadTemplate(atomTmpl, atom_xml)
|
||||||
|
loadTemplate(indexTmpl, index_gmi)
|
||||||
|
loadTemplate(pageTmpl, page_gmi)
|
||||||
|
loadTemplate(htmlTmpl, output_html)
|
||||||
|
// Load templates
|
||||||
|
filepath.Walk(tmplDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Mode().IsRegular() {
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Remove "templates/" from beginning of path
|
||||||
|
path = strings.TrimPrefix(path, tmplDir)
|
||||||
|
loadTemplate(path, string(b))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// Load content
|
||||||
|
dir := NewDir("")
|
||||||
|
if err := dir.read(srcDir, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
site.sort()
|
// Sort content
|
||||||
if err := site.manipulate(); err != nil {
|
dir.sort()
|
||||||
|
// Manipulate content
|
||||||
|
if err := manipulate(dir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if toAtom {
|
if cfg.toAtom {
|
||||||
if err := site.createFeeds(); err != nil {
|
if err := createFeeds(dir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := site.write("dst", OutputGemini); err != nil {
|
// Write content
|
||||||
|
if err := write(dir, dstDir, outputGemini); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if toHtml {
|
if cfg.toHTML {
|
||||||
if err := site.write("dst.html", OutputHTML); err != nil {
|
if err := write(dir, htmlDst, outputHTML); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,3 +144,304 @@ func serve() error {
|
||||||
server.Handler = gmi.FileServer(gmi.Dir("dst"))
|
server.Handler = gmi.FileServer(gmi.Dir("dst"))
|
||||||
return server.ListenAndServe()
|
return server.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write writes the contents of the Index to the provided destination directory.
|
||||||
|
func write(dir *Dir, dstDir string, format outputFormat) error {
|
||||||
|
// Empty the destination directory
|
||||||
|
if err := os.RemoveAll(dstDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Create the destination directory
|
||||||
|
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Write the directory
|
||||||
|
return dir.write(dstDir, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manipulate processes and manipulates the site's content.
|
||||||
|
func manipulate(dir *Dir) error {
|
||||||
|
// Write the directory index file, if it doesn't exist
|
||||||
|
if dir.index == nil {
|
||||||
|
var b bytes.Buffer
|
||||||
|
tmpl := templates[indexTmpl]
|
||||||
|
if err := tmpl.Execute(&b, dir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dir.index = &Page{
|
||||||
|
Permalink: dir.Permalink,
|
||||||
|
content: b.Bytes(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Manipulate pages
|
||||||
|
for i := range dir.Pages {
|
||||||
|
var b bytes.Buffer
|
||||||
|
tmpl := templates[pageTmpl]
|
||||||
|
if err := tmpl.Execute(&b, dir.Pages[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dir.Pages[i].content = b.Bytes()
|
||||||
|
}
|
||||||
|
// Manipulate subdirectories
|
||||||
|
for _, d := range dir.Dirs {
|
||||||
|
if err := manipulate(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// feed represents an Atom feed.
|
||||||
|
type feed struct {
|
||||||
|
Title string // Feed title.
|
||||||
|
Path string // Feed path.
|
||||||
|
URL string // Site URL.
|
||||||
|
Updated time.Time // Last updated time.
|
||||||
|
Entries []*Page // Feed entries.
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFeeds creates Atom feeds.
|
||||||
|
func createFeeds(dir *Dir) error {
|
||||||
|
var b bytes.Buffer
|
||||||
|
tmpl := templates[atomTmpl]
|
||||||
|
feed := &feed{
|
||||||
|
Title: cfg.title,
|
||||||
|
Path: filepath.Join(dir.Permalink, atomPath),
|
||||||
|
URL: cfg.url,
|
||||||
|
Updated: time.Now(),
|
||||||
|
Entries: dir.Pages,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(&b, feed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir.Permalink, atomPath)
|
||||||
|
dir.Files[path] = b.Bytes()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page represents a page.
|
||||||
|
type Page struct {
|
||||||
|
Title string // The title of this page.
|
||||||
|
Permalink string // The permalink to this page.
|
||||||
|
Date time.Time // The date of the page.
|
||||||
|
content []byte // The content of this page.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content returns the page content as a string.
|
||||||
|
// Used in templates.
|
||||||
|
func (p *Page) Content() string {
|
||||||
|
return string(p.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regexp to parse title from Gemini files
|
||||||
|
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?")
|
||||||
|
|
||||||
|
// NewPage returns a new Page with the given path and content.
|
||||||
|
func NewPage(path string, content []byte) *Page {
|
||||||
|
// Try to parse the date from the page filename
|
||||||
|
var date time.Time
|
||||||
|
const layout = "2006-01-02"
|
||||||
|
base := filepath.Base(path)
|
||||||
|
if len(base) >= len(layout) {
|
||||||
|
dateStr := base[:len(layout)]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse the title from the contents
|
||||||
|
var title string
|
||||||
|
if submatches := titleRE.FindSubmatch(content); submatches != nil {
|
||||||
|
title = string(submatches[1])
|
||||||
|
// Remove the title from the contents
|
||||||
|
content = content[len(submatches[0]):]
|
||||||
|
}
|
||||||
|
|
||||||
|
permalink := strings.TrimSuffix(path, ".gmi")
|
||||||
|
return &Page{
|
||||||
|
Permalink: "/" + permalink + "/",
|
||||||
|
Title: title,
|
||||||
|
Date: date,
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir represents a directory.
|
||||||
|
type Dir struct {
|
||||||
|
Permalink string // Permalink to this directory.
|
||||||
|
Pages []*Page // Pages in this directory.
|
||||||
|
Dirs []*Dir // Subdirectories.
|
||||||
|
Files map[string][]byte // Static files.
|
||||||
|
index *Page // The index file (index.gmi).
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDir returns a new Dir with the given path.
|
||||||
|
func NewDir(path string) *Dir {
|
||||||
|
var permalink string
|
||||||
|
if path == "" {
|
||||||
|
permalink = "/"
|
||||||
|
} else {
|
||||||
|
permalink = "/" + path + "/"
|
||||||
|
}
|
||||||
|
return &Dir{
|
||||||
|
Permalink: permalink,
|
||||||
|
Files: map[string][]byte{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read reads from a directory and indexes the files and directories within it.
|
||||||
|
func (d *Dir) read(srcDir string, path string) error {
|
||||||
|
entries, err := ioutil.ReadDir(filepath.Join(srcDir, path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
// Ignore names that start with "_"
|
||||||
|
if strings.HasPrefix(name, "_") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(path, name)
|
||||||
|
if entry.IsDir() {
|
||||||
|
// Gather directory data
|
||||||
|
dir := NewDir(path)
|
||||||
|
if err := dir.read(srcDir, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Dirs = append(d.Dirs, dir)
|
||||||
|
} else {
|
||||||
|
srcPath := filepath.Join(srcDir, path)
|
||||||
|
content, err := ioutil.ReadFile(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if filepath.Ext(name) == ".gmi" {
|
||||||
|
// Gather page data
|
||||||
|
page := NewPage(path, content)
|
||||||
|
if name == "index.gmi" {
|
||||||
|
d.index = page
|
||||||
|
} else {
|
||||||
|
d.Pages = append(d.Pages, page)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Static file
|
||||||
|
d.Files[path] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write writes the Dir to the provided destination path.
|
||||||
|
func (d *Dir) write(dstDir string, format outputFormat) error {
|
||||||
|
// Create the directory
|
||||||
|
dirPath := filepath.Join(dstDir, d.Permalink)
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write static files
|
||||||
|
for path := range d.Files {
|
||||||
|
dstPath := filepath.Join(dstDir, path)
|
||||||
|
f, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data := d.Files[path]
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the pages
|
||||||
|
for _, page := range d.Pages {
|
||||||
|
path, content := format(page)
|
||||||
|
dstPath := filepath.Join(dstDir, path)
|
||||||
|
dir := filepath.Dir(dstPath)
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
f, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the index file
|
||||||
|
if d.index != nil {
|
||||||
|
path, content := format(d.index)
|
||||||
|
dstPath := filepath.Join(dstDir, path)
|
||||||
|
f, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write subdirectories
|
||||||
|
for _, dir := range d.Dirs {
|
||||||
|
dir.write(dstDir, format)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort sorts the directory's pages by date.
|
||||||
|
func (d *Dir) 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.Dirs {
|
||||||
|
d.sort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputFormat represents an output format.
|
||||||
|
type outputFormat func(*Page) (path string, content []byte)
|
||||||
|
|
||||||
|
func outputGemini(p *Page) (path string, content []byte) {
|
||||||
|
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)
|
||||||
|
text := gmi.Parse(r)
|
||||||
|
content = gmiToHTML(text)
|
||||||
|
|
||||||
|
type tmplCtx struct {
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
tmpl := templates[htmlTmpl]
|
||||||
|
tmpl.Execute(&b, &tmplCtx{
|
||||||
|
Title: p.Title,
|
||||||
|
Content: string(content),
|
||||||
|
})
|
||||||
|
content = b.Bytes()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
19
templates.go
19
templates.go
|
@ -1,9 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "text/template"
|
// Default atom feed template
|
||||||
|
|
||||||
// Default templates
|
|
||||||
|
|
||||||
const atom_xml = `<?xml version="1.0" encoding="utf-8"?>
|
const atom_xml = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<title>{{ .Title }}</title>
|
<title>{{ .Title }}</title>
|
||||||
|
@ -23,6 +20,7 @@ const atom_xml = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
|
// Default directory index template
|
||||||
const index_gmi = `# Index of {{ .Permalink }}
|
const index_gmi = `# Index of {{ .Permalink }}
|
||||||
|
|
||||||
{{ range .Dirs }}=> {{ .Permalink }}
|
{{ range .Dirs }}=> {{ .Permalink }}
|
||||||
|
@ -30,12 +28,15 @@ const index_gmi = `# Index of {{ .Permalink }}
|
||||||
{{ range .Pages }}=> {{ .Permalink }}
|
{{ range .Pages }}=> {{ .Permalink }}
|
||||||
{{ end -}}`
|
{{ end -}}`
|
||||||
|
|
||||||
|
// Default page template
|
||||||
const page_gmi = `# {{ .Title }}
|
const page_gmi = `# {{ .Title }}
|
||||||
|
|
||||||
{{ .Content }}`
|
{{ .Content }}`
|
||||||
|
|
||||||
var (
|
// Default template for html output
|
||||||
atomTmpl = template.Must(template.New("atom.xml").Parse(atom_xml))
|
const output_html = `<!DOCTYPE html>
|
||||||
indexTmpl = template.Must(template.New("index.gmi").Parse(index_gmi))
|
<meta charset="utf-8">
|
||||||
pageTmpl = template.Must(template.New("page.gmi").Parse(page_gmi))
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
)
|
<title>{{ .Title }}</title>
|
||||||
|
|
||||||
|
{{ .Content }}`
|
||||||
|
|
Loading…
Reference in a new issue