//go:generate go run sketchground.dk/www --generate
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"sort"
"strings"
"time"
"github.com/gorilla/feeds"
"github.com/russross/blackfriday"
)
var tmpl = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sketchground</title>
<meta name="description" content="Sketchground">
<meta name="flattr:id" content="zl0d9e">
<link rel="stylesheet" type="text/css" href="/hack.css">
<link href="/blog/atom.xml" type="application/atom+xml" rel="alternate" title="Sketchground.dk blog" />
<link href="/blog/rss.xml" type="application/rss+xml" rel="alternate" title="Sketchground.dk blog" />
</head>
<body class="hack">
<div class="menu">
{{if .HasBack}}
<a href="/"><<< HOME</a>
|
{{end}}
<a href="/blog">journal</a>
-
<a href="/toots">toots</a>
</div>
<hr/>
{{.Content}}
<hr/>
<div style="text-align:center;">
<a href="https://www.twitter.com/sketchground">twitter</a>
<a rel="me" href="https://mastodon.technology/@jzs">mastodon</a>
<a href="https://www.github.com/jzs">github</a>
<a href="https://git.sr.ht/~jzs">sourcehut</a>
</div>
</body>
</html>
`
type BlogIndex struct {
Entries []BlogEntry
}
type BlogEntry struct {
Title string
Link string
Filepath string
Created time.Time
}
func main() {
gen := flag.Bool("generate", false, "generate static html")
flag.Parse()
if *gen {
os.MkdirAll("dist", 0700)
log.Println("Generating static pages")
dir, err := os.Open(".")
if err != nil {
panic(err)
}
t, err := template.New("tmpl").Parse(tmpl)
if err != nil {
panic(err)
}
processFolder(dir, "", t)
copyFile("hack.css", "dist/hack.css")
copyFile("profile.jpeg", "dist/profile.jpeg")
log.Println("Generating rss/atom feeds")
atom, rss, err := GenerateRSSBlogFeed(GenerateBlogIndex())
if err != nil {
panic(err)
}
if err := ioutil.WriteFile("dist/blog/rss.xml", []byte(rss), 0644); err != nil {
panic(err)
}
if err := ioutil.WriteFile("dist/blog/atom.xml", []byte(atom), 0644); err != nil {
panic(err)
}
return
}
port := "localhost:10000"
log.Printf("Serving on %v", port)
if err := http.ListenAndServe(port, &Server{fs: http.FileServer(http.Dir("."))}); err != nil {
panic(err)
}
}
func processFolder(dir *os.File, basePath string, t *template.Template) {
log.Printf("Processing %v %v", basePath, dir.Name())
files, err := dir.Readdir(-1)
if err != nil {
panic(err)
}
for _, f := range files {
if strings.HasPrefix(f.Name(), ".") {
continue
}
fullPath := path.Join(basePath, f.Name())
file, err := os.Open(fullPath)
if err != nil {
panic(err)
}
if f.IsDir() {
processFolder(file, fullPath, t)
file.Close()
continue
}
if strings.HasSuffix(f.Name(), ".md") || strings.HasSuffix(f.Name(), ".tmpl") {
os.MkdirAll(path.Join("dist", basePath), 0700)
fc, err := ioutil.ReadAll(file)
if err != nil {
panic(err)
}
fname := path.Join("dist", strings.Replace(file.Name(), ".tmpl", "", 1))
fname = strings.Replace(fname, ".md", ".html", 1)
log.Printf("Generated %v", fname)
if strings.HasSuffix(f.Name(), ".tmpl") {
log.Printf("Generating template for: %v", file.Name())
var index interface{}
switch true {
case strings.HasPrefix(file.Name(), "blog/"):
index = GenerateBlogIndex()
case strings.HasPrefix(file.Name(), "toots/"):
index = GenerateTootList()
default:
index = GenerateFrontpageIndex()
}
tmpl, err := template.New("tmpl").Parse(string(fc))
if err != nil {
panic(err)
}
buf := &bytes.Buffer{}
if err := tmpl.Execute(buf, index); err != nil {
panic(err)
}
fc = buf.Bytes()
}
// Create the file. Skip this on serve...
file, err := os.Create(fname)
if err != nil {
panic(err)
}
data := MarkdownToHtml(t, fc, basePath != "")
if _, err := file.Write(data); err != nil {
log.Println(err)
}
}
file.Close()
}
}
func MarkdownToHtml(t *template.Template, fc []byte, hasBack bool) []byte {
buf := &bytes.Buffer{}
if err := t.Execute(buf, struct {
HasBack bool
Content template.HTML
}{
HasBack: hasBack,
Content: template.HTML(blackfriday.Run(fc)),
}); err != nil {
log.Println(err)
}
return buf.Bytes()
}
type FrontpageEntry struct {
Title string
Link string
Type string
Created time.Time
}
func GenerateFrontpageIndex() []FrontpageEntry {
blogs := GenerateBlogIndex()
toots := GenerateTootList()
entries := make([]FrontpageEntry, len(blogs.Entries)+len(toots))
for i, b := range blogs.Entries {
entries[i] = FrontpageEntry{
Title: b.Title,
Link: b.Link,
Created: b.Created,
Type: "blog",
}
}
for i, t := range toots {
entries[i+len(blogs.Entries)] = FrontpageEntry{
Title: t.Message,
Created: t.Time,
Type: "toot",
}
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Created.After(entries[j].Created)
})
return entries
}
func GenerateBlogIndex() BlogIndex {
index := BlogIndex{
Entries: []BlogEntry{},
}
dir, err := os.Open("blog")
if err != nil {
panic(err)
}
defer dir.Close()
files, err := dir.Readdirnames(-1)
if err != nil {
panic(err)
}
sort.Slice(files, func(i, j int) bool {
return files[j] < files[i]
})
for _, f := range files {
if strings.HasSuffix(f, ".md") {
link := strings.TrimSuffix(f, ".md")
link = fmt.Sprintf("%v.html", link)
elems := strings.SplitN(strings.TrimPrefix(link, "/blog/"), "_", 2)
dateStr := elems[0]
t, err := time.Parse("2006-01-02", dateStr)
if err != nil {
panic(err)
}
title := strings.Replace(elems[1], "_", " ", -1)
title = strings.TrimSuffix(title, ".html")
index.Entries = append(index.Entries, BlogEntry{
Link: fmt.Sprintf("/blog/%v", link),
Title: fmt.Sprintf(title),
Filepath: path.Join(dir.Name(), f),
Created: t,
})
}
}
return index
}
type Toot struct {
Time time.Time
Message string
}
func GenerateTootList() []Toot {
toots, err := os.Open("toots/toots.list")
if err != nil {
panic(err)
}
defer toots.Close()
r := bufio.NewReader(toots)
res := []Toot{}
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
elems := strings.SplitN(line, " ", 2)
t, err := time.Parse("2006-01-02T15:04:05", elems[0])
if err != nil {
panic(err)
}
res = append(res, Toot{Time: t, Message: strings.TrimSpace(elems[1])})
}
sort.Slice(res, func(i, j int) bool {
return res[i].Time.After(res[j].Time)
})
return res
}
func GenerateRSSBlogFeed(index BlogIndex) (atom, rss string, err error) {
now := time.Now()
feed := &feeds.Feed{
Title: "Sketchground.dk blog",
Link: &feeds.Link{Href: "https://www.sketchground.dk/blog"},
Description: "Personal blog about this and that",
Author: &feeds.Author{Name: "Jens Zeilund"},
Created: now,
Items: make([]*feeds.Item, len(index.Entries)),
}
// Populate items. Based on blogindex?
for i, entry := range index.Entries {
data, err := ioutil.ReadFile(entry.Filepath)
if err != nil {
return "", "", err
}
feed.Items[i] = &feeds.Item{
Title: entry.Title,
Link: &feeds.Link{Href: entry.Link},
Author: &feeds.Author{Name: "Jens Zeilund"},
Created: entry.Created,
Description: string(blackfriday.Run(data)),
}
}
if atom, err = feed.ToAtom(); err != nil {
return "", "", err
}
if rss, err = feed.ToRss(); err != nil {
return "", "", err
}
return
}
func copyFile(file string, path string) {
f, err := os.Open(file)
if err != nil {
panic(err)
}
defer f.Close()
nf, err := os.Create(path)
if err != nil {
panic(err)
}
defer nf.Close()
if _, err := io.Copy(nf, f); err != nil {
panic(err)
}
}
type Server struct {
fs http.Handler
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t, err := template.New("tmpl").Parse(tmpl)
if err != nil {
panic(err)
}
if r.URL.Path == "/" {
r.URL.Path = "/index.md"
}
if strings.HasSuffix(r.URL.Path, "/") {
r.URL.Path = fmt.Sprintf("%vindex.md", r.URL.Path)
}
if r.URL.Path == "/profile.jpeg" {
http.ServeFile(w, r, "profile.jpeg")
return
}
if r.URL.Path == "/blog/rss.xml" {
w.Header().Set("Content-Type", "application/rss+xml")
index := GenerateBlogIndex()
_, rss, err := GenerateRSSBlogFeed(index)
if err != nil {
panic(err)
}
w.Write([]byte(rss))
return
}
if r.URL.Path == "/blog/atom.xml" {
w.Header().Set("Content-Type", "application/atom+xml")
index := GenerateBlogIndex()
atom, _, err := GenerateRSSBlogFeed(index)
if err != nil {
panic(err)
}
w.Write([]byte(atom))
return
}
if strings.HasSuffix(r.URL.Path, ".md") {
path := strings.TrimLeft(r.URL.Path, "/")
file, err := ioutil.ReadFile(path)
if err != nil {
file, err = ioutil.ReadFile(fmt.Sprintf("%v.tmpl", path))
if err != nil {
panic(err)
}
var index interface{}
switch true {
case strings.Contains(r.URL.Path, "/blog/"):
index = GenerateBlogIndex()
case strings.Contains(r.URL.Path, "/toots/"):
index = GenerateTootList()
default:
index = GenerateFrontpageIndex()
}
tmpl, err := template.New("tmpl").Parse(string(file))
if err != nil {
panic(err)
}
buf := &bytes.Buffer{}
if err := tmpl.Execute(buf, index); err != nil {
panic(err)
}
file = buf.Bytes()
}
w.Write(MarkdownToHtml(t, file, r.URL.Path != "/index.md"))
return
}
if strings.HasSuffix(r.URL.Path, ".html") {
path := strings.TrimLeft(r.URL.Path, "/")
path = strings.Replace(path, ".html", ".md", 1)
file, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
w.Write(MarkdownToHtml(t, file, r.URL.Path != "/index.md"))
return
}
s.fs.ServeHTTP(w, r)
}