315 lines
7.8 KiB
Go
315 lines
7.8 KiB
Go
|
package blog
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"git.gutmet.org/goutil.git"
|
||
|
. "git.gutmet.org/wombat.git/templatestructures"
|
||
|
"math"
|
||
|
"os"
|
||
|
"path"
|
||
|
"path/filepath"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"text/template"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const postsPerPage int = 5
|
||
|
|
||
|
var blogname string
|
||
|
var folder string
|
||
|
var style string
|
||
|
var templateString string
|
||
|
var t *template.Template
|
||
|
var categories map[string][]post
|
||
|
|
||
|
type metadata struct {
|
||
|
Title string
|
||
|
Date time.Time
|
||
|
Categories []string
|
||
|
}
|
||
|
|
||
|
type post struct {
|
||
|
Content string
|
||
|
Meta metadata
|
||
|
Filename string
|
||
|
Link string
|
||
|
}
|
||
|
|
||
|
type categoryListing struct {
|
||
|
Name string
|
||
|
ASCIIName string
|
||
|
Posts []post
|
||
|
}
|
||
|
|
||
|
func linkToPost(filename string) string {
|
||
|
filename = filepath.Base(filename)
|
||
|
split := strings.Split(filename, "-")
|
||
|
if len(split) < 4 {
|
||
|
return ""
|
||
|
} else {
|
||
|
year := split[0]
|
||
|
month := split[1]
|
||
|
day := split[2]
|
||
|
name := ConvASCII(strings.Join(split[3:], "-"))
|
||
|
return fmt.Sprintf("/%s/%s/%s/%s/%s", folder, year, month, day, name)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func linkToCategory(name string) string {
|
||
|
return fmt.Sprintf("/%s/categories.html#%s", folder, ConvASCII(strings.ToLower(name)))
|
||
|
}
|
||
|
|
||
|
func addToCategory(category string, post post) {
|
||
|
c := strings.ToLower(category)
|
||
|
if len(c) > 0 {
|
||
|
categories[c] = append(categories[c], post)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func keys(m map[string][]post) []string {
|
||
|
keys := make([]string, 0)
|
||
|
for k := range m {
|
||
|
keys = append(keys, k)
|
||
|
}
|
||
|
return keys
|
||
|
}
|
||
|
|
||
|
func emitPostPage(post post) string {
|
||
|
filename := strings.TrimPrefix(strings.TrimPrefix(post.Filename, "stage1/"), "stage1\\")
|
||
|
link := post.Link
|
||
|
os.Remove(filepath.Join("stage2", filename))
|
||
|
base := path.Base(link)
|
||
|
folders := strings.TrimSuffix("stage2"+link, base)
|
||
|
os.MkdirAll(folders, 0755)
|
||
|
page := Page{}
|
||
|
page.Blogname = blogname
|
||
|
page.Title = post.Meta.Title
|
||
|
page.Style = style
|
||
|
page.Content = post.Content
|
||
|
page.Time = post.Meta.Date.Format(TimeFormat)
|
||
|
page.Blogpost = true
|
||
|
for _, categoryName := range post.Meta.Categories {
|
||
|
page.Categories = append(page.Categories, Category{categoryName, linkToCategory(categoryName)})
|
||
|
addToCategory(categoryName, post)
|
||
|
}
|
||
|
buf := new(bytes.Buffer)
|
||
|
t.Execute(buf, page)
|
||
|
goutil.WriteFile(filepath.Join("stage2", link), buf.String())
|
||
|
return link
|
||
|
}
|
||
|
|
||
|
func indexFilename(i int) string {
|
||
|
return filepath.Join("stage2", folder, fmt.Sprintf("%d.html", i))
|
||
|
}
|
||
|
|
||
|
func indexPageExists(i int, total int) bool {
|
||
|
max := int(math.Ceil(float64(total)/float64(postsPerPage))) - 1
|
||
|
return (i >= 0) && (i <= max)
|
||
|
}
|
||
|
|
||
|
type byDateDesc []post
|
||
|
|
||
|
func (p byDateDesc) Len() int { return len(p) }
|
||
|
func (p byDateDesc) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||
|
func (p byDateDesc) Less(i, j int) bool { return p[i].Meta.Date.After(p[j].Meta.Date) }
|
||
|
|
||
|
func emitIndexPage(i int, posts []post, total int) {
|
||
|
file := indexFilename(i)
|
||
|
postCollection := make([]Blogpost, 0)
|
||
|
sort.Sort(byDateDesc(posts))
|
||
|
for _, post := range posts {
|
||
|
link := emitPostPage(post)
|
||
|
p := Blogpost{}
|
||
|
p.Title = post.Meta.Title
|
||
|
p.Link = link
|
||
|
p.Content = post.Content
|
||
|
postCollection = append(postCollection, p)
|
||
|
}
|
||
|
buf := new(bytes.Buffer)
|
||
|
page := Page{}
|
||
|
page.Title = blogname
|
||
|
page.Style = style
|
||
|
page.Blogdir = folder
|
||
|
page.Blogpost = false
|
||
|
page.PostCollection = postCollection
|
||
|
if indexPageExists(i-1, total) {
|
||
|
page.Previous = strconv.Itoa(i-1) + ".html"
|
||
|
}
|
||
|
if indexPageExists(i+1, total) {
|
||
|
page.Later = strconv.Itoa(i+1) + ".html"
|
||
|
}
|
||
|
t.Execute(buf, page)
|
||
|
goutil.WriteFile(file, buf.String())
|
||
|
}
|
||
|
|
||
|
func getMetadata(file string) (metadata, error) {
|
||
|
metaAll, err := goutil.ReadFile(goutil.TrimExt(file) + ".desc")
|
||
|
ret := metadata{}
|
||
|
if err == nil {
|
||
|
lines := strings.Split(metaAll, "\n")
|
||
|
for _, line := range lines {
|
||
|
if strings.HasPrefix(line, "title:") {
|
||
|
ret.Title = strings.Trim(strings.TrimPrefix(line, "title:"), "\" ")
|
||
|
} else if strings.HasPrefix(line, "date:") {
|
||
|
date, err2 := time.Parse(TimeFormat, strings.Trim(strings.TrimPrefix(line, "date:"), "\" "))
|
||
|
if err2 == nil {
|
||
|
ret.Date = date
|
||
|
} else {
|
||
|
fmt.Printf("Unrecognized date format in %s", file)
|
||
|
}
|
||
|
} else if strings.HasPrefix(line, "categories:") {
|
||
|
categories := strings.Trim(strings.TrimPrefix(line, "categories:"), "\"[] ")
|
||
|
splitcategories := strings.Split(categories, ",")
|
||
|
for _, category := range splitcategories {
|
||
|
c := strings.TrimSpace(category)
|
||
|
if len(c) > 0 {
|
||
|
ret.Categories = append(ret.Categories, c)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return ret, nil
|
||
|
} else {
|
||
|
return ret, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getPost(file string) (post, error) {
|
||
|
tmp, err := goutil.ReadFile(file)
|
||
|
if err == nil {
|
||
|
meta, err2 := getMetadata(file)
|
||
|
if err2 == nil {
|
||
|
p := post{}
|
||
|
p.Content = tmp
|
||
|
p.Meta = meta
|
||
|
p.Filename = file
|
||
|
filename := strings.TrimPrefix(file, "stage1")
|
||
|
p.Link = linkToPost(filename)
|
||
|
return p, nil
|
||
|
} else {
|
||
|
return post{}, err2
|
||
|
}
|
||
|
} else {
|
||
|
return post{}, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func makeCategories() {
|
||
|
ct := template.Must(template.New("Categories").Parse(categoryTemplate))
|
||
|
page := Page{}
|
||
|
page.Title = blogname + ": Categories"
|
||
|
page.Style = style
|
||
|
keySet := keys(categories)
|
||
|
sort.Strings(keySet)
|
||
|
page.Content += "<dl style=\"padding-bottom:150em\">"
|
||
|
for _, k := range keySet {
|
||
|
listing := categoryListing{}
|
||
|
listing.Name = k
|
||
|
listing.ASCIIName = ConvASCII(k)
|
||
|
sort.Sort(byDate(categories[k]))
|
||
|
listing.Posts = categories[k]
|
||
|
buf := new(bytes.Buffer)
|
||
|
ct.Execute(buf, listing)
|
||
|
page.Content += buf.String()
|
||
|
}
|
||
|
page.Content += "</dl>"
|
||
|
buf := new(bytes.Buffer)
|
||
|
t.Execute(buf, page)
|
||
|
goutil.WriteFile(filepath.Join("stage2", folder, "categories.html"), buf.String())
|
||
|
}
|
||
|
|
||
|
func makeTimeline(posts []post) {
|
||
|
sort.Sort(byDateDesc(posts))
|
||
|
page := Page{}
|
||
|
page.Title = blogname + ": Timeline"
|
||
|
page.Style = style
|
||
|
page.Content += "<dl>"
|
||
|
year := -1
|
||
|
month := -1
|
||
|
for _, p := range posts {
|
||
|
if year != p.Meta.Date.Year() {
|
||
|
year = p.Meta.Date.Year()
|
||
|
page.Content += "</dl>\n\n" + "<h3>" + strconv.Itoa(year) + "</h3>\n<dl>\n"
|
||
|
}
|
||
|
if month != int(p.Meta.Date.Month()) {
|
||
|
month = int(p.Meta.Date.Month())
|
||
|
page.Content += fmt.Sprintf("<dt>%02d</dt>", month)
|
||
|
}
|
||
|
page.Content += fmt.Sprintf("<dd><a href=\"%s\">%s</a></dd>\n", p.Link, p.Meta.Title)
|
||
|
}
|
||
|
page.Content += "</dl>"
|
||
|
buf := new(bytes.Buffer)
|
||
|
t.Execute(buf, page)
|
||
|
goutil.WriteFile(filepath.Join("stage2", folder, "timeline.html"), buf.String())
|
||
|
}
|
||
|
|
||
|
type byDate []post
|
||
|
|
||
|
func (p byDate) Len() int { return len(p) }
|
||
|
func (p byDate) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||
|
func (p byDate) Less(i, j int) bool { return p[i].Meta.Date.Before(p[j].Meta.Date) }
|
||
|
|
||
|
func getPosts() []post {
|
||
|
files := goutil.ListFilesExt(filepath.Join("stage1", folder), ".html")
|
||
|
posts := make([]post, 0)
|
||
|
for _, file := range files {
|
||
|
tmp, err := getPost(file)
|
||
|
if err == nil {
|
||
|
posts = append(posts, tmp)
|
||
|
}
|
||
|
}
|
||
|
return posts
|
||
|
}
|
||
|
|
||
|
func makeBlog() {
|
||
|
posts := getPosts()
|
||
|
sort.Sort(byDate(posts))
|
||
|
total := len(posts)
|
||
|
s := make([]post, 0)
|
||
|
n := 0
|
||
|
for i, tmp := range posts {
|
||
|
if (i / postsPerPage) != n {
|
||
|
emitIndexPage(n, s, total)
|
||
|
n = i / postsPerPage
|
||
|
s = make([]post, 0)
|
||
|
}
|
||
|
s = append(s, tmp)
|
||
|
}
|
||
|
if len(s) != 0 {
|
||
|
emitIndexPage(n, s, total)
|
||
|
}
|
||
|
os.Link(indexFilename(n), filepath.Join("stage2", folder, "index.html"))
|
||
|
makeCategories()
|
||
|
makeTimeline(posts)
|
||
|
}
|
||
|
|
||
|
func Generate(dir string) {
|
||
|
folder = dir
|
||
|
blogname, _ = goutil.ReadFile(filepath.Join("stage0", folder, ".name"))
|
||
|
if blogname == "" {
|
||
|
blogname = "blog"
|
||
|
}
|
||
|
blogname = strings.TrimSpace(blogname)
|
||
|
categories = make(map[string][]post)
|
||
|
var err error
|
||
|
var err2 error
|
||
|
style, err = goutil.ReadFile("style.css")
|
||
|
templateString, err2 = goutil.ReadFile("template")
|
||
|
t = template.Must(template.New("page").Parse(templateString))
|
||
|
if err == nil && err2 == nil {
|
||
|
makeBlog()
|
||
|
} else {
|
||
|
fmt.Println(err)
|
||
|
fmt.Println(err2)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const categoryTemplate string = `
|
||
|
<dt><a name="{{.ASCIIName}}" class="categoryListing">{{.Name}}</a></dt>
|
||
|
{{range $index, $post := .Posts}}<dd><a href="{{$post.Link}}">{{$post.Meta.Title}}</a></dd>
|
||
|
{{end}}
|
||
|
`
|