Add initial implementation of build tasks

This commit is contained in:
Adnan Maolood 2021-03-20 23:17:58 -04:00
parent 0f13954449
commit ace33d8b1e
7 changed files with 135 additions and 79 deletions

View file

@ -1,7 +1,9 @@
package main
import (
"fmt"
"os"
"path"
"text/template"
"github.com/pelletier/go-toml"
@ -12,9 +14,27 @@ type Config struct {
Title string `toml:"title"` // site title
URLs []string `toml:"urls"` // site URLs
Feeds map[string]string `toml:"feeds"` // site feeds
Tasks map[string]*Task `toml:"task"` // site tasks
Templates *Templates `toml:"-"` // site templates
}
// Task represents a site build task.
type Task struct {
Input string `toml:"input"` // input file extension
Output string `toml:"output"` // output file extension
Template string `toml:"template"` // template file extension
PostProcess string `toml:"postprocess"` // postprocess directive
Destination string `toml:"destination"` // destination directory
postProcess Format
}
func (t Task) Format(p *Page) (string, []byte) {
if t.postProcess == nil {
return path.Join(p.Path, "index"+t.Output), []byte(p.Content)
}
return t.postProcess.Format(p)
}
// LoadConfig loads the configuration from the provided path.
func LoadConfig(path string) (*Config, error) {
c := new(Config)
@ -28,6 +48,28 @@ func LoadConfig(path string) (*Config, error) {
return nil, err
}
// Add default task
// The toml library overwrites the map, so add default values after parsing
if _, ok := c.Tasks["gemini"]; !ok {
c.Tasks["gemini"] = &Task{
Input: ".gmi",
Output: ".gmi",
Template: ".gmi",
Destination: "public",
}
}
for _, task := range c.Tasks {
switch task.PostProcess {
case "":
continue
case "geminiToHTML":
task.postProcess = GeminiToHTML(c)
default:
return nil, fmt.Errorf("unrecognized postprocess directive %q", task.PostProcess)
}
}
return c, nil
}

51
dir.go
View file

@ -19,6 +19,10 @@ type Dir struct {
Dirs []*Dir // Subdirectories.
files map[string][]byte // Static files.
index *Page // The index page.
inputExt string // input file extension
outputExt string // output file extension
templateExt string // template file extension
}
// NewDir returns a new Dir with the given path.
@ -50,6 +54,9 @@ func (d *Dir) read(srcDir string, path string) error {
if entry.IsDir() {
// Gather directory data
dir := NewDir(path)
dir.inputExt = d.inputExt
dir.outputExt = d.outputExt
dir.templateExt = d.templateExt
if err := dir.read(srcDir, path); err != nil {
return err
}
@ -60,9 +67,9 @@ func (d *Dir) read(srcDir string, path string) error {
if err != nil {
return err
}
if pathpkg.Ext(name) == ".gmi" {
if ext := pathpkg.Ext(name); ext == d.inputExt {
// Gather page data
if name == "index.gmi" {
if strings.TrimSuffix(name, ext) == "index" {
d.index = NewPage(d.Path, content)
d.Title = d.index.Title
d.Content = d.index.Content
@ -80,24 +87,26 @@ func (d *Dir) read(srcDir string, path string) error {
// manipulate processes and manipulates the directory's contents.
func (d *Dir) manipulate(cfg *Config) error {
// Create index
if d.index != nil {
var b strings.Builder
tmpl := cfg.Templates.FindTemplate(d.Path, "index.gmi")
if err := tmpl.Execute(&b, d); err != nil {
return err
if d.templateExt != "" {
// Create index
if d.index != nil {
var b strings.Builder
tmpl := cfg.Templates.FindTemplate(d.Path, "index"+d.templateExt)
if err := tmpl.Execute(&b, d); err != nil {
return err
}
d.index.Content = b.String()
}
d.index.Content = b.String()
}
// Manipulate pages
for i := range d.Pages {
var b strings.Builder
tmpl := cfg.Templates.FindTemplate(d.Path, "page.gmi")
if err := tmpl.Execute(&b, d.Pages[i]); err != nil {
return err
// Manipulate pages
for i := range d.Pages {
var b strings.Builder
tmpl := cfg.Templates.FindTemplate(d.Path, "page"+d.templateExt)
if err := tmpl.Execute(&b, d.Pages[i]); err != nil {
return err
}
d.Pages[i].Content = b.String()
}
d.Pages[i].Content = b.String()
}
// Feed represents a feed.
@ -134,7 +143,7 @@ func (d *Dir) manipulate(cfg *Config) error {
}
// write writes the Dir to the provided destination path.
func (d *Dir) write(dstDir string, format Format, cfg *Config) error {
func (d *Dir) write(dstDir string, format Format) error {
// Create the directory
dirPath := pathpkg.Join(dstDir, d.Path)
if err := os.MkdirAll(dirPath, 0755); err != nil {
@ -156,7 +165,7 @@ func (d *Dir) write(dstDir string, format Format, cfg *Config) error {
// Write pages
for _, page := range d.Pages {
path, content := format.Format(page, cfg)
path, content := format.Format(page)
dstPath := pathpkg.Join(dstDir, path)
dir := pathpkg.Dir(dstPath)
os.MkdirAll(dir, 0755)
@ -171,7 +180,7 @@ func (d *Dir) write(dstDir string, format Format, cfg *Config) error {
// Write the index file
if d.index != nil {
path, content := format.Format(d.index, cfg)
path, content := format.Format(d.index)
dstPath := pathpkg.Join(dstDir, path)
dir := pathpkg.Dir(dstPath)
os.MkdirAll(dir, 0755)
@ -186,7 +195,7 @@ func (d *Dir) write(dstDir string, format Format, cfg *Config) error {
// Write subdirectories
for _, dir := range d.Dirs {
dir.write(dstDir, format, cfg)
dir.write(dstDir, format)
}
return nil
}

1
example/.gitignore vendored
View file

@ -1 +1,2 @@
/public
/public.html

View file

@ -3,3 +3,16 @@ urls = ["gemini://example.com", "https://example.com"]
[feeds]
"/blog/" = "Example Feed"
[task.gemini]
input = ".gmi"
output = ".gmi"
template = ".gmi"
destination = "public"
[task.geminiToHTML]
input = ".gmi"
output = ".html"
template = ".gmi"
postprocess = "geminiToHTML"
destination = "public.html"

View file

@ -8,43 +8,38 @@ import (
// Format represents an output format.
type Format interface {
Format(*Page, *Config) (path string, content []byte)
Format(*Page) (path string, content []byte)
}
type FormatFunc func(*Page, *Config) (string, []byte)
type FormatFunc func(*Page) (string, []byte)
func (f FormatFunc) Format(p *Page, cfg *Config) (string, []byte) {
return f(p, cfg)
func (f FormatFunc) Format(p *Page) (string, []byte) {
return f(p)
}
// FormatGemini formats the page as Gemini text.
func FormatGemini(p *Page, cfg *Config) (path string, content []byte) {
path = pathpkg.Join(p.Path, "index.gmi")
content = []byte(p.Content)
return
}
// GeminiToHTML returns an output format that converts Gemini text to HTML.
func GeminiToHTML(cfg *Config) Format {
return FormatFunc(func(p *Page) (path string, content []byte) {
path = pathpkg.Join(p.Path, "index.html")
// FormatHTML formats the page as HTML.
func FormatHTML(p *Page, cfg *Config) (path string, content []byte) {
path = pathpkg.Join(p.Path, "index.html")
r := strings.NewReader(p.Content)
content = textToHTML(r)
r := strings.NewReader(p.Content)
content = textToHTML(r)
// html template context
type htmlCtx struct {
Title string // page title
Content string // page HTML contents
}
// html template context
type htmlCtx struct {
Title string // page title
Content string // page HTML contents
}
var b bytes.Buffer
// clean path to remove trailing slash
dir := pathpkg.Dir(pathpkg.Clean(p.Path))
tmpl := cfg.Templates.FindTemplate(dir, "output.html")
tmpl.Execute(&b, &htmlCtx{
Title: p.Title,
Content: string(content),
var b bytes.Buffer
// clean path to remove trailing slash
dir := pathpkg.Dir(pathpkg.Clean(p.Path))
tmpl := cfg.Templates.FindTemplate(dir, "output.html")
tmpl.Execute(&b, &htmlCtx{
Title: p.Title,
Content: string(content),
})
content = b.Bytes()
return
})
content = b.Bytes()
return
}

45
main.go
View file

@ -1,7 +1,6 @@
package main
import (
"flag"
"log"
)
@ -12,10 +11,6 @@ func main() {
}
func run() error {
var format string
flag.StringVar(&format, "format", "gemini", "output format to use. Supported formats include gemini and html")
flag.Parse()
// Load config
cfg, err := LoadConfig("config.toml")
if err != nil {
@ -25,29 +20,25 @@ func run() error {
return err
}
var output Format
switch format {
case "gemini":
output = FormatFunc(FormatGemini)
case "html":
output = FormatFunc(FormatHTML)
default:
log.Fatalf("unknown output format %q", format)
for _, task := range cfg.Tasks {
// Load content
dir := NewDir("")
dir.inputExt = task.Input
dir.outputExt = task.Output
dir.templateExt = task.Template
if err := dir.read("content", ""); err != nil {
return err
}
dir.sort()
// Manipulate content
if err := dir.manipulate(cfg); err != nil {
return err
}
// Write content
if err := dir.write(task.Destination, task); err != nil {
return err
}
}
// Load content
dir := NewDir("")
if err := dir.read("content", ""); err != nil {
return err
}
dir.sort()
// Manipulate content
if err := dir.manipulate(cfg); err != nil {
return err
}
// Write content
if err := dir.write("public", output, cfg); err != nil {
return err
}
return nil
}

View file

@ -3,6 +3,7 @@ package main
import (
"embed"
"io/ioutil"
"log"
"os"
pathpkg "path"
"path/filepath"
@ -82,5 +83,9 @@ func (t *Templates) FindTemplate(path string, tmpl string) *template.Template {
if t, ok := t.tmpls[tmplPath]; ok {
return t
}
return t.tmpls[pathpkg.Join("/_default", tmpl)]
if t, ok := t.tmpls[pathpkg.Join("/_default", tmpl)]; ok {
return t
}
log.Fatalf("failed to find template %q", tmpl)
return nil
}