boring/main.go

423 lines
9.0 KiB
Go
Raw Permalink Normal View History

2017-02-03 13:29:15 -07:00
package main
import (
"bufio"
"flag"
2017-02-03 13:29:15 -07:00
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
2017-02-03 13:29:15 -07:00
"os"
"os/exec"
2017-02-03 13:46:00 -07:00
"path"
2017-02-03 13:29:15 -07:00
"regexp"
"sort"
"strings"
"time"
"github.com/fsnotify/fsnotify"
2020-06-05 17:52:54 -06:00
"github.com/gorilla/feeds"
2023-06-27 13:36:37 -06:00
"github.com/russross/blackfriday/v2"
2017-02-03 13:29:15 -07:00
)
var templ *template.Template
var funcMap = template.FuncMap{
2017-02-04 19:25:41 -07:00
"hasTitle": func(s string) bool {
ret := false
if s != "" {
ret = true
}
return ret
},
2017-02-03 13:29:15 -07:00
"formatDate": func(t time.Time) string {
return t.Format(time.RFC1123)
},
"shortDate": func(t time.Time) string {
return t.Format("January _2, 2006")
},
"printByte": func(b []byte) string {
return string(b)
},
"lop": func(p Posts, start, end int) Posts {
if len(p) < end {
return p
}
2017-02-03 13:29:15 -07:00
return p[start:end]
},
"joinTags": func(ts Tags) template.HTML {
var s []string
for _, t := range ts {
s = append(s, fmt.Sprintf(`%s`, t.Name))
}
return template.HTML(strings.Join(s, ", "))
},
"printHTML": func(b []byte) template.HTML {
return template.HTML(string(b))
},
}
2017-02-04 19:25:41 -07:00
type content struct {
Posts Posts
Title string
Author User
2017-02-03 13:29:15 -07:00
}
// AuthorRE is a regex to grab our Authors
var AuthorRE = regexp.MustCompile(`^author:\s(.*)$`)
// TitleRE matches our article title
var TitleRE = regexp.MustCompile(`^title:\s(.*)$`)
// DateRE matches our article date
var DateRE = regexp.MustCompile(`^date:\s(.*)$`)
// TagRE matches the tags for a given post
var TagRE = regexp.MustCompile(`^tags:\s(.*)$`)
// DescRE matches the descriptoin for a given post
var DescRE = regexp.MustCompile(`^description:\s(.*)$`)
// Tag represents a specific tag for an article
type Tag struct {
ID int
Created time.Time
Name string
}
// Tags are a collection of Tag
type Tags []*Tag
// Join returns a concat'd string of Tag names
func (t *Tags) Join() []string {
var s []string
for _, t := range *t {
s = append(s, t.Name)
}
return s
}
func (t *Tags) String() string {
return strings.Join(t.Join(), ", ")
}
// User represents an author of an article
type User struct {
LName string
FName string
Email string
Pubkey []byte
User string
}
var userLineRE = regexp.MustCompile(`^(.*)\s(.*)\s<(.*)>$`)
// Parse takes a 'First Last <user@email.com>' style string and creates a User
func (u *User) Parse(s string) {
u.FName = userLineRE.ReplaceAllString(s, "$1")
u.LName = userLineRE.ReplaceAllString(s, "$2")
u.Email = userLineRE.ReplaceAllString(s, "$3")
}
// Combine concatenates FName, LName and Email into one line
func (u *User) Combine() string {
2017-02-04 19:25:41 -07:00
return fmt.Sprintf("%s %s", u.FName, u.LName)
2017-02-03 13:29:15 -07:00
}
// Post is the base type for all posts
type Post struct {
Title string
Description string
Date time.Time
Body []byte
Author User
Signed bool
Signature []byte
Tags Tags
URL string
}
// HTML returns converted MD to HTML
func (p *Post) HTML() {
2019-04-17 17:17:13 -06:00
p.Body = blackfriday.Run(p.Body)
2017-02-03 13:29:15 -07:00
}
// LoadFromFile takes the File of a given page and loads the markdown for rendering
func (p *Post) LoadFromFile(f string) error {
file, err := os.Open(f)
if err != nil {
return err
}
scanner := bufio.NewScanner(file)
if err != nil {
return err
}
for scanner.Scan() {
var line = scanner.Bytes()
useLine := true
if AuthorRE.Match(line) {
aline := AuthorRE.ReplaceAllString(string(line), "$1")
p.Author.Parse(aline)
fmt.Printf("Author: %s %s (%s)\n", p.Author.FName, p.Author.LName, p.Author.Email)
useLine = false
}
if TitleRE.Match(line) {
p.Title = TitleRE.ReplaceAllString(string(line), "$1")
fmt.Printf("Title: %s\n", p.Title)
useLine = false
}
if DateRE.Match(line) {
d := DateRE.ReplaceAllString(string(line), "$1")
2017-02-03 13:46:13 -07:00
p.Date, err = time.Parse(time.RFC1123, d)
if err != nil {
log.Printf("error in '%s'\n", f)
log.Fatal(err)
}
2017-02-03 13:29:15 -07:00
fmt.Printf("Date: %s\n", p.Date)
useLine = false
}
if TagRE.Match(line) {
ts := TagRE.ReplaceAllString(string(line), "$1")
for _, tag := range strings.Split(ts, ",") {
var t Tag
t.Name = strings.TrimSpace(tag)
p.Tags = append(p.Tags, &t)
}
fmt.Printf("Tags: %s\n", p.Tags.Join())
useLine = false
}
if DescRE.Match(line) {
p.Description = DescRE.ReplaceAllString(string(line), "$1")
fmt.Printf("Description: %s\n", p.Description)
useLine = false
}
if useLine {
p.Body = append(p.Body, line...)
p.Body = append(p.Body, 10)
}
}
if err != nil {
return err
}
return nil
}
// Posts represent a collection of a set of Post
type Posts []*Post
func (p Posts) Len() int {
return len(p)
}
func (p Posts) Less(i, j int) bool {
return p[i].Date.After(p[j].Date)
}
func (p Posts) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func renderPost(f string, path string) (Post, error) {
var err error
p := Post{}
p.LoadFromFile(f)
if err != nil {
log.Fatal(err)
}
p.HTML()
p.URL = "/" + md2html(path)
return p, nil
}
func renderTemplate(dst string, tmpl string, data interface{}) {
o, err := os.Create(dst)
defer o.Close()
if err != nil {
log.Fatal(err)
}
err = templ.ExecuteTemplate(o, tmpl, data)
if err != nil {
2017-02-04 19:25:41 -07:00
log.Println(dst)
2017-02-03 13:29:15 -07:00
log.Fatal(err)
}
}
func md2html(f string) string {
return strings.Replace(f, ".md", ".html", -1)
}
func main() {
var err error
// extrasys.Pledge("stdio wpath rpath cpath", nil)
var watch = flag.Bool("w", false, "Enable 'watch' mode. Requires 'wdir' and 'wcmd'.")
var watchDir = flag.String("wdir", "", "watch a directory for changes, run command when change happens.")
var watchCmd = flag.String("wcmd", "", "command to run when changes are detected in 'wdir'.")
var watchSrv = flag.String("wsrv", "", "Serve static files from specified directory.")
var srvPort = flag.String("port", ":8080", "Port to serve the static files on.")
2017-02-03 13:29:15 -07:00
flag.Parse()
2017-02-03 13:29:15 -07:00
if !*watch {
if len(os.Args) < 2 {
fmt.Println("Wrong number of arguments")
os.Exit(1)
}
2017-02-03 13:29:15 -07:00
src := os.Args[1]
tmpl := os.Args[2]
dst := os.Args[3]
2017-02-03 13:29:15 -07:00
templ, err = template.New("boring").Funcs(funcMap).ParseGlob(tmpl + "/*.html")
if err != nil {
log.Fatal(err)
}
log.Printf("Generating static html from %s to %s\n", src, dst)
2017-02-03 13:29:15 -07:00
files, err := ioutil.ReadDir(src)
2017-02-03 13:29:15 -07:00
if err != nil {
log.Fatal(err)
}
posts := Posts{}
for _, file := range files {
fn := file.Name()
srcFile := path.Join(src, fn)
dstFile := path.Join(dst, "/posts/", md2html(fn))
post, err := renderPost(srcFile, path.Join("posts/", fn))
fmt.Println("-----")
if err != nil {
log.Fatal(err)
}
2017-02-03 13:29:15 -07:00
renderTemplate(dstFile, "default.html", struct {
Content Post
}{
post,
})
posts = append(posts, &post)
}
2017-02-03 13:29:15 -07:00
sort.Sort(posts)
renderTemplate(path.Join(dst, "/index.html"), "index.html", content{
Title: "",
Posts: posts,
})
renderTemplate(path.Join(dst, "/about.html"), "about.html", content{
Title: "About",
Author: posts[0].Author,
})
renderTemplate(path.Join(dst, "/contact.html"), "contact.html", content{
Title: "Contact",
Author: posts[0].Author,
})
if len(posts) < 5 {
renderTemplate(path.Join(dst, "/archive.html"), "archive.html", content{
Title: "Archive",
Posts: posts,
})
} else {
renderTemplate(path.Join(dst, "/archive.html"), "archive.html", content{
Title: "Archive",
Posts: posts[5:],
})
}
2017-02-04 19:25:41 -07:00
// TODO variablize all of this and shove it in some kind of config
2017-02-04 19:25:41 -07:00
latestDate := posts[0].Date
2017-02-04 19:25:41 -07:00
2020-06-05 17:52:54 -06:00
feed := &feeds.Feed{
Title: "deftly.net - All posts",
2020-06-05 17:52:54 -06:00
Link: &feeds.Link{Href: "https://deftly.net/"},
Description: "Personal blog of Aaron Bieber",
2020-06-05 17:52:54 -06:00
Author: &feeds.Author{Name: "Aaron Bieber", Email: "aaron@bolddaemon.com"},
Created: latestDate,
Copyright: "This work is copyright © Aaron Bieber",
}
2017-02-04 19:25:41 -07:00
for _, post := range posts {
2020-06-05 17:52:54 -06:00
var i = &feeds.Item{}
i.Title = post.Title
i.Description = string(post.Body)
2020-06-05 17:52:54 -06:00
i.Link = &feeds.Link{Href: "https://deftly.net" + post.URL}
i.Author = &feeds.Author{Name: post.Author.Combine(), Email: "aaron@bolddaemon.com"}
i.Created = post.Date
2017-02-04 19:25:41 -07:00
feed.Items = append(feed.Items, i)
}
2017-02-04 19:25:41 -07:00
atomFile, err := os.Create(path.Join(dst, "atom.xml"))
if err != nil {
log.Fatal(err)
}
2017-02-04 19:25:41 -07:00
rssFile, err := os.Create(path.Join(dst, "rss.xml"))
if err != nil {
log.Fatal(err)
}
feed.WriteAtom(atomFile)
feed.WriteRss(rssFile)
} else {
// Watch mode
go func() {
// Start a http server and serve the static dir
log.Printf("serving '%s' on https://localhost%s", *watchSrv, *srvPort)
log.Fatal(
http.ListenAndServe(
*srvPort,
http.FileServer(http.Dir(*watchSrv)),
),
)
}()
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("modified file:", event.Name)
c := exec.Command(*watchCmd)
if err := c.Run(); err != nil {
fmt.Println("Error: ", err)
}
}
case err := <-watcher.Errors:
log.Fatal(err)
}
}
}()
2017-02-04 19:25:41 -07:00
err = watcher.Add(*watchDir)
if err != nil {
log.Fatal(err)
}
<-done
}
2017-02-03 13:29:15 -07:00
}