This commit is contained in:
adnano 2020-09-29 21:30:50 -04:00
parent 3f38235d4b
commit 399e543b4a
5 changed files with 124 additions and 132 deletions

2
go.mod
View file

@ -2,4 +2,4 @@ module kiln
go 1.15 go 1.15
require git.sr.ht/~adnano/gmi v0.0.1-alpha require git.sr.ht/~adnano/gmi v0.0.1-alpha.1

4
go.sum
View file

@ -1,5 +1,5 @@
git.sr.ht/~adnano/gmi v0.0.1-alpha h1:6SRpwsKRowrKfExhyYQaQ0SBYcMIhHG4hUaWF67iP4k= git.sr.ht/~adnano/gmi v0.0.1-alpha.1 h1:zXcPeenDDt3opygDCunf52x3IJdVFEc1V0wf8aHshzc=
git.sr.ht/~adnano/gmi v0.0.1-alpha/go.mod h1:4MWQDsleal4HRi/LuxxM6ymWJQikP3Gh7xZindVCHzg= git.sr.ht/~adnano/gmi v0.0.1-alpha.1/go.mod h1:4MWQDsleal4HRi/LuxxM6ymWJQikP3Gh7xZindVCHzg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

213
kiln.go
View file

@ -18,29 +18,42 @@ import (
// Site represents a kiln site. // Site represents a kiln site.
type Site struct { type Site struct {
URL string // Site URL Title string // Site title.
Directory *Directory // Site directory URL string // Site URL.
Templates *template.Template // Templates dir *Dir // Site directory.
tmpl *template.Template // Templates.
feeds bool // Whether or not to generate feeds.
} }
// Load loads the Site from the specified source directory. // load loads the Site from the current directory.
func (s *Site) Load(srcDir string) error { func (s *Site) load() error {
s.Directory = NewDirectory("") // Load content
const srcDir = "src"
tmpl, err := template.New("templates").ParseGlob("templates/*.gmi") s.dir = NewDir("")
if err != nil { if err := site.dir.Read(srcDir, ""); err != nil {
return err return err
} }
s.Templates = tmpl // Load templates
const tmplDir = "templates"
if err := site.Directory.Read(""); err != nil { 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 err
} }
return nil return nil
} }
// Write writes the contents of the Index to the provided destination directory. // write writes the contents of the Index to the provided destination directory.
func (s *Site) Write(dstDir string, format OutputFormat) error { func (s *Site) write(dstDir string, format OutputFormat) error {
// Empty the destination directory // Empty the destination directory
if err := os.RemoveAll(dstDir); err != nil { if err := os.RemoveAll(dstDir); err != nil {
return err return err
@ -49,105 +62,88 @@ func (s *Site) Write(dstDir string, format OutputFormat) error {
if err := os.MkdirAll(dstDir, 0755); err != nil { if err := os.MkdirAll(dstDir, 0755); err != nil {
return err return err
} }
// Write the directory // Write the directory
return s.Directory.Write(dstDir, format) return s.dir.Write(dstDir, format)
} }
// Manipulate processes and manipulates the site's content. // manipulate processes and manipulates the site's content.
func (s *Site) Manipulate(dir *Directory) error { func (s *Site) manipulate() error {
// FIXME: manipulate doesn't descend into subdirectories
// Write the directory index file, if it doesn't exist // Write the directory index file, if it doesn't exist
if dir.Index == nil { const indexPath = "index.gmi"
path := filepath.Join(dir.Permalink, "index.gmi") if s.dir.index == nil {
var b bytes.Buffer var b bytes.Buffer
tmpl := s.Templates.Lookup("index.gmi") tmpl := s.tmpl.Lookup(indexPath)
if tmpl == nil { if err := tmpl.Execute(&b, s.dir); err != nil {
tmpl = indexTmpl
}
if err := tmpl.Execute(&b, dir); err != nil {
return err return err
} }
content := b.Bytes() s.dir.index = &Page{
permalink := filepath.Dir(path) Permalink: s.dir.Permalink,
if permalink == "." { content: b.Bytes(),
permalink = ""
} }
page := &Page{
Permalink: "/" + permalink,
content: content,
}
dir.Index = page
} }
// Manipulate pages // Manipulate pages
for i := range dir.Pages { const pagePath = "page.gmi"
for i := range s.dir.Pages {
var b bytes.Buffer var b bytes.Buffer
tmpl := s.Templates.Lookup("page.gmi") tmpl := s.tmpl.Lookup(pagePath)
if tmpl == nil { if err := tmpl.Execute(&b, s.dir.Pages[i]); err != nil {
tmpl = pageTmpl
}
if err := tmpl.Execute(&b, dir.Pages[i]); err != nil {
return err return err
} }
dir.Pages[i].content = b.Bytes() s.dir.Pages[i].content = b.Bytes()
} }
return nil return nil
} }
// Sort sorts the site's pages by date. // sort sorts the site's pages by date.
func (s *Site) Sort() { func (s *Site) sort() {
s.Directory.Sort() s.dir.Sort()
} }
// Feed represents a feed.
type Feed struct { type Feed struct {
Title string Title string // Feed title.
Path string Path string // Feed path.
SiteURL string URL string // Site URL.
Updated time.Time Updated time.Time // Last updated time.
Entries []*Page Entries []*Page // Feed entries.
} }
// CreateFeeds creates Atom feeds. // createFeeds creates Atom feeds.
func (s *Site) CreateFeeds() error { func (s *Site) createFeeds() error {
const atomPath = "atom.xml" const atomPath = "atom.xml"
var b bytes.Buffer var b bytes.Buffer
tmpl := s.Templates.Lookup(atomPath) tmpl := s.tmpl.Lookup(atomPath)
if tmpl == nil {
tmpl = atomTmpl
}
feed := &Feed{ feed := &Feed{
Title: "Example Feed", Title: s.Title,
Path: filepath.Join(s.Directory.Permalink, atomPath), Path: filepath.Join(s.dir.Permalink, atomPath),
SiteURL: s.URL, URL: s.URL,
Updated: time.Now(), Updated: time.Now(),
Entries: s.Directory.Pages, Entries: s.dir.Pages,
} }
if err := tmpl.Execute(&b, feed); err != nil { if err := tmpl.Execute(&b, feed); err != nil {
return err return err
} }
path := filepath.Join(s.Directory.Permalink, atomPath) path := filepath.Join(s.dir.Permalink, atomPath)
s.Directory.Static[path] = b.Bytes() s.dir.Files[path] = b.Bytes()
return nil return nil
} }
// Page represents a page. // Page represents a page.
type Page struct { type Page struct {
// The permalink to this page. Title string // The title of this page.
Permalink string Permalink string // The permalink to this page.
// The title of this page, parsed from the Gemini contents. Date time.Time // The date of the page.
Title string content []byte // The content of this page.
// 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 []byte
} }
// Content returns the page content as a string.
// Used in templates.
func (p *Page) Content() string { func (p *Page) Content() string {
return string(p.content) return string(p.content)
} }
// Regexp to parse title from Gemini files
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?") var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?")
// NewPage returns a new Page with the given path and content. // NewPage returns a new Page with the given path and content.
@ -196,82 +192,77 @@ func NewPage(path string, content []byte) *Page {
} }
} }
// Directory represents a directory of pages. // Dir represents a directory.
type Directory struct { type Dir struct {
// The permalink to this directory. Permalink string // Permalink to this directory.
Permalink string Pages []*Page // Pages in this directory.
// The pages in this directory. Dirs []*Dir // Subdirectories.
Pages []*Page Files map[string][]byte // Static files.
// The subdirectories of this directory. index *Page // The index file (index.gmi).
Directories []*Directory
// The index file (index.gmi).
Index *Page
// Static files
Static map[string][]byte
} }
// NewDirectory returns a new Directory with the given path. // NewDir returns a new Dir with the given path.
func NewDirectory(path string) *Directory { func NewDir(path string) *Dir {
var permalink string var permalink string
if path == "" { if path == "" {
permalink = "/" permalink = "/"
} else { } else {
permalink = "/" + path + "/" permalink = "/" + path + "/"
} }
return &Directory{ return &Dir{
Permalink: permalink, Permalink: permalink,
Static: map[string][]byte{}, Files: map[string][]byte{},
} }
} }
// Read reads from a directory and indexes the files and directories within it. // Read reads from a directory and indexes the files and directories within it.
func (d *Directory) Read(path string) error { func (d *Dir) Read(srcDir string, path string) error {
entries, err := ioutil.ReadDir(filepath.Join("src", path)) entries, err := ioutil.ReadDir(filepath.Join(srcDir, path))
if err != nil { if err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range entries {
name := entry.Name() name := entry.Name()
// Ignore names that start with '_'
if strings.HasPrefix(name, "_") {
continue
}
path := filepath.Join(path, name)
if entry.IsDir() { if entry.IsDir() {
// Gather directory data // Gather directory data
path := filepath.Join(path, name) dir := NewDir(path)
dir := NewDirectory(path) if err := dir.Read(srcDir, path); err != nil {
if err := dir.Read(path); err != nil {
return err return err
} }
d.Directories = append(d.Directories, dir) d.Dirs = append(d.Dirs, dir)
} else { } else {
srcPath := filepath.Join("src", path, name) srcPath := filepath.Join(srcDir, path)
content, err := ioutil.ReadFile(srcPath) content, err := ioutil.ReadFile(srcPath)
if err != nil { if err != nil {
return err return err
} }
switch filepath.Ext(name) { switch filepath.Ext(name) {
case ".gmi", ".gemini": case ".gmi":
path := filepath.Join(path, name)
// Gather page data // Gather page data
page := NewPage(path, content) page := NewPage(path, content)
if name == "index.gmi" { if name == "index.gmi" {
d.Index = page d.index = page
} else { } else {
d.Pages = append(d.Pages, page) d.Pages = append(d.Pages, page)
} }
default: default:
// Static file // Static file
path := filepath.Join(path, name) d.Files[path] = content
d.Static[path] = content
} }
} }
} }
return nil return nil
} }
// Write writes the Directory to the provided destination path. // Write writes the Dir to the provided destination path.
func (d *Directory) Write(dstDir string, format OutputFormat) error { func (d *Dir) Write(dstDir string, format OutputFormat) error {
// Create the directory // Create the directory
dirPath := filepath.Join(dstDir, d.Permalink) dirPath := filepath.Join(dstDir, d.Permalink)
if err := os.MkdirAll(dirPath, 0755); err != nil { if err := os.MkdirAll(dirPath, 0755); err != nil {
@ -279,13 +270,13 @@ func (d *Directory) Write(dstDir string, format OutputFormat) error {
} }
// Write static files // Write static files
for path := range d.Static { for path := range d.Files {
dstPath := filepath.Join(dstDir, path) dstPath := filepath.Join(dstDir, path)
f, err := os.Create(dstPath) f, err := os.Create(dstPath)
if err != nil { if err != nil {
return err return err
} }
data := d.Static[path] data := d.Files[path]
if _, err := f.Write(data); err != nil { if _, err := f.Write(data); err != nil {
return err return err
} }
@ -307,8 +298,8 @@ func (d *Directory) Write(dstDir string, format OutputFormat) error {
} }
// Write the index file // Write the index file
if d.Index != nil { if d.index != nil {
path, content := format(d.Index) path, content := format(d.index)
dstPath := filepath.Join(dstDir, path) dstPath := filepath.Join(dstDir, path)
f, err := os.Create(dstPath) f, err := os.Create(dstPath)
if err != nil { if err != nil {
@ -320,19 +311,19 @@ func (d *Directory) Write(dstDir string, format OutputFormat) error {
} }
// Write subdirectories // Write subdirectories
for _, dir := range d.Directories { for _, dir := range d.Dirs {
dir.Write(dstDir, format) dir.Write(dstDir, format)
} }
return nil return nil
} }
// Sort sorts the directory's pages by date. // Sort sorts the directory's pages by date.
func (d *Directory) Sort() { func (d *Dir) Sort() {
sort.Slice(d.Pages, func(i, j int) bool { sort.Slice(d.Pages, func(i, j int) bool {
return d.Pages[i].Date.After(d.Pages[j].Date) return d.Pages[i].Date.After(d.Pages[j].Date)
}) })
// Sort subdirectories // Sort subdirectories
for _, d := range d.Directories { for _, d := range d.Dirs {
d.Sort() d.Sort()
} }
} }

13
main.go
View file

@ -19,6 +19,7 @@ func init() {
flag.BoolVar(&serveSite, "serve", false, "serve the site") flag.BoolVar(&serveSite, "serve", false, "serve the site")
flag.BoolVar(&toHtml, "html", false, "output HTML") flag.BoolVar(&toHtml, "html", false, "output HTML")
flag.BoolVar(&toAtom, "atom", false, "output Atom feed") flag.BoolVar(&toAtom, "atom", false, "output Atom feed")
flag.StringVar(&site.Title, "title", "", "site title")
flag.StringVar(&site.URL, "url", "", "site URL") flag.StringVar(&site.URL, "url", "", "site URL")
} }
@ -35,23 +36,23 @@ func main() {
// build the site // build the site
func build() error { func build() error {
if err := site.Load("src"); err != nil { if err := site.load(); err != nil {
return err return err
} }
site.Sort() site.sort()
if err := site.Manipulate(site.Directory); err != nil { if err := site.manipulate(); err != nil {
return err return err
} }
if toAtom { if toAtom {
if err := site.CreateFeeds(); err != nil { if err := site.createFeeds(); err != nil {
return err return err
} }
} }
if err := site.Write("dst", OutputGemini); err != nil { if err := site.write("dst", OutputGemini); err != nil {
return err return err
} }
if toHtml { if toHtml {
if err := site.Write("dst.html", OutputHTML); err != nil { if err := site.write("dst.html", OutputHTML); err != nil {
return err return err
} }
} }

View file

@ -7,28 +7,28 @@ import "text/template"
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>
<link href="{{ .SiteURL }}"/> <link href="{{ .URL }}"/>
<link rel="self" href="{{ .SiteURL }}{{ .Path }}"/> <link rel="self" href="{{ .URL }}{{ .Path }}"/>
<updated>{{ .Updated }}</updated> <updated>{{ .Updated }}</updated>
<id>{{ .SiteURL }}{{ .Path }}</id> <id>{{ .URL }}{{ .Path }}</id>
{{ $siteURL := .SiteURL }} {{- $url := .URL -}}
{{ range .Entries }} {{- range .Entries }}
<entry> <entry>
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link href="{{ $siteURL }}{{ .Permalink }}"/> <link href="{{ $url }}{{ .Permalink }}"/>
<id>{{ $siteURL }}{{ .Permalink }}</id> <id>{{ $url }}{{ .Permalink }}</id>
<updated>{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}</updated> <updated>{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}</updated>
<content src="{{ $siteURL }}{{ .Permalink }}" type="text/gemini"></content> <content src="{{ $url }}{{ .Permalink }}" type="text/gemini"></content>
</entry> </entry>
{{ end }} {{ end -}}
</feed>` </feed>`
const index_gmi = `# Index of {{ .Permalink }} const index_gmi = `# Index of {{ .Permalink }}
{{ range .Directories }}=> {{ .Permalink }} {{ range .Dirs }}=> {{ .Permalink }}
{{ end }} {{ end -}}
{{ range .Pages }}=> {{ .Permalink }} {{ range .Pages }}=> {{ .Permalink }}
{{ end }}` {{ end -}}`
const page_gmi = `# {{ .Title }} const page_gmi = `# {{ .Title }}