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
|
|
}
|