Simple static site generator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
5.0 KiB

package main
import (
"bytes"
"fmt"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
"html/template"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
type Article struct {
Path string
SectionPath string
IsSection bool
Content template.HTML
Meta map[string]interface{}
SubArticles []*Article
}
func (article *Article) Title() string {
if article.Meta["title"] != nil {
if title, ok := (article.Meta["title"]).(string); ok {
return title
}
}
return ""
}
type parseResult struct {
Article *Article
Err error
}
var fsMutex = &sync.Mutex{}
func main() {
fmt.Println("Start generation:", time.Now().String())
articles, err := parse()
if err != nil {
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Finished parsing:", time.Now().String())
fmt.Println(len(articles), "articles")
process(&articles)
fmt.Println("Finished processing:", time.Now().String())
err = generate(&articles)
if err != nil {
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Finished generating:", time.Now().String())
}
func parse() (map[string]*Article, error) {
// Parse files (meta and markdown
var files []string
_ = filepath.Walk("source", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
files = append(files, path)
}
return nil
})
var results []parseResult
resultsMutex := &sync.Mutex{}
var waitGroup sync.WaitGroup
for _, file := range files {
file := file
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
// Read file
fsMutex.Lock()
byteArray, err := ioutil.ReadFile(file)
fsMutex.Unlock()
if err != nil {
results = append(results, parseResult{Err: err})
return
}
// Parse markdown and meta data
content, metaData, err := convert(byteArray)
if err != nil {
results = append(results, parseResult{Err: err})
return
}
// Compute article path
articlePath := path.Clean(strings.TrimPrefix(file, "source/"))
if strings.HasSuffix(articlePath, "index.md") {
articlePath = path.Dir(articlePath)
} else {
articlePath = strings.TrimSuffix(articlePath, ".md")
}
// Save article
resultsMutex.Lock()
results = append(results, parseResult{Article: &Article{
Path: articlePath,
Content: template.HTML(content),
Meta: metaData,
}})
resultsMutex.Unlock()
}()
}
waitGroup.Wait()
articles := map[string]*Article{}
for _, result := range results {
if result.Err != nil {
return nil, result.Err
} else {
articles[result.Article.Path] = result.Article
}
}
return articles, nil
}
func process(articles *map[string]*Article) {
for _, article := range *articles {
pathSegments := strings.Split(article.Path, "/")
// Set section
if len(pathSegments) > 1 && (*articles)[pathSegments[0]] != nil {
section := (*articles)[pathSegments[0]]
section.IsSection = true
article.SectionPath = section.Path
}
// Get cascaded meta
joinedSegments := ""
for _, segment := range pathSegments {
joinedSegments = path.Join(joinedSegments, segment)
if (*articles)[joinedSegments] != nil {
joinedSegmentsArticle := (*articles)[joinedSegments]
if cascade, ok := (joinedSegmentsArticle.Meta["cascade"]).(map[string]interface{}); ok {
for key, value := range cascade {
if article.Meta[key] == nil {
article.Meta[key] = value
}
}
}
if joinedSegments != article.Path {
joinedSegmentsArticle.SubArticles = append(joinedSegmentsArticle.SubArticles, article)
sort.Slice(joinedSegmentsArticle.SubArticles, func(i, j int) bool {
return joinedSegmentsArticle.SubArticles[i].Title() < joinedSegmentsArticle.SubArticles[j].Title()
})
}
}
}
}
return
}
func generate(articles *map[string]*Article) error {
// Delete old files
err := os.RemoveAll("output")
if err != nil {
return err
}
// Generate new files
var wg sync.WaitGroup
templates := template.Must(template.New("").ParseFiles("templates/single.gohtml"))
minifier := minify.New()
minifier.AddFunc("text/html", html.Minify)
minifier.AddFunc("text/css", css.Minify)
for _, article := range *articles {
article := article
wg.Add(1)
go func() {
defer wg.Done()
// Execute template
var result bytes.Buffer
err = templates.ExecuteTemplate(&result, "single.gohtml", struct {
Article *Article
AllArticles *map[string]*Article
}{
Article: article,
AllArticles: articles,
})
if err != nil {
panic(err)
}
// Minify
minified, err := minifier.Bytes("text/html", result.Bytes())
if err != nil {
panic(err)
}
// Create directories
err = os.MkdirAll(path.Join("output", article.Path), os.ModePerm)
if err != nil {
panic(err)
}
// Write result
fsMutex.Lock()
err = ioutil.WriteFile(path.Join("output", article.Path, "index.html"), minified, os.ModePerm)
fsMutex.Unlock()
if err != nil {
panic(err)
}
}()
}
wg.Wait()
return nil
}