2021-07-10 16:27:58 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-07-10 19:09:35 +03:00
|
|
|
"bytes"
|
2021-07-10 16:27:58 +03:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
2021-07-10 19:09:35 +03:00
|
|
|
"os/exec"
|
2021-07-10 16:27:58 +03:00
|
|
|
"path"
|
|
|
|
"path/filepath"
|
2021-07-10 17:44:25 +03:00
|
|
|
"strings"
|
2021-07-10 16:27:58 +03:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gohugoio/hugo/config"
|
2021-07-10 19:46:01 +03:00
|
|
|
"github.com/n0x1m/hugoext/hugov0492"
|
2021-07-10 16:27:58 +03:00
|
|
|
"github.com/spf13/afero"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2021-07-10 20:29:02 +03:00
|
|
|
defaultExt = "md"
|
|
|
|
defaultProcessor = ""
|
2021-07-10 16:27:58 +03:00
|
|
|
defaultSource = "content"
|
|
|
|
defaultDestination = "public"
|
|
|
|
defaultConfigPath = "config.toml"
|
|
|
|
defaultUglyURLs = false
|
|
|
|
|
|
|
|
defaultPermalinkFormat = "/:year/:month/:title/"
|
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
2021-07-10 20:18:52 +03:00
|
|
|
var ext, processor, source, destination, cfgPath string
|
|
|
|
var uglyURLs, noSectionList, withDrafts bool
|
2021-07-10 16:27:58 +03:00
|
|
|
|
|
|
|
flag.StringVar(&ext, "ext", defaultExt, "ext to look for templates in ./layout")
|
|
|
|
flag.StringVar(&processor, "proc", defaultProcessor, "processor to pipe markdown content through")
|
|
|
|
flag.StringVar(&source, "source", defaultSource, "source directory")
|
|
|
|
flag.StringVar(&destination, "destination", defaultDestination, "output directory")
|
2021-07-10 20:18:52 +03:00
|
|
|
flag.StringVar(&cfgPath, "config", defaultConfigPath, "hugo config path")
|
|
|
|
flag.BoolVar(&uglyURLs, "ugly-urls", false, "use directories with index or .ext files")
|
|
|
|
flag.BoolVar(&noSectionList, "no-section-list", false, "disable auto append of section content lists")
|
|
|
|
flag.BoolVar(&withDrafts, "enable-withDrafts", false, "include withDrafts in processing and output")
|
2021-07-10 16:27:58 +03:00
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
osfs := afero.NewOsFs()
|
|
|
|
cfg, err := config.FromFile(osfs, "config.toml")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("config from file", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
permalinks := cfg.GetStringMapString("permalinks")
|
2021-07-10 19:32:21 +03:00
|
|
|
if permalinks == nil {
|
|
|
|
log.Println("no permalinks from config loaded, using default: ", defaultPermalinkFormat)
|
|
|
|
}
|
2021-07-10 16:27:58 +03:00
|
|
|
|
2021-07-10 19:32:21 +03:00
|
|
|
linkpattern := func(subdir string) string {
|
2021-07-10 16:27:58 +03:00
|
|
|
format, ok := permalinks[subdir]
|
|
|
|
if ok {
|
|
|
|
return format
|
|
|
|
}
|
|
|
|
return defaultPermalinkFormat
|
|
|
|
}
|
2021-07-10 17:44:25 +03:00
|
|
|
|
2021-07-10 19:09:35 +03:00
|
|
|
// iterate through file tree source
|
2021-07-10 17:44:25 +03:00
|
|
|
files := make(chan File)
|
|
|
|
go collectFiles(source, files)
|
2021-07-10 19:09:35 +03:00
|
|
|
|
|
|
|
// for each file, get destination path, switch file extension, remove underscore for index
|
|
|
|
var tree FileTree
|
2021-07-10 17:44:25 +03:00
|
|
|
for file := range files {
|
2021-07-10 19:32:21 +03:00
|
|
|
pattern := linkpattern(file.Parent)
|
2021-07-10 17:44:25 +03:00
|
|
|
err := destinationPath(&file, pattern)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err, file)
|
|
|
|
continue
|
|
|
|
}
|
2021-07-10 19:09:35 +03:00
|
|
|
//fmt.Printf("%s -> %s (%d)\n", file.Source, file.Destination, len(file.Body))
|
2021-07-10 20:18:52 +03:00
|
|
|
if file.Draft && !withDrafts {
|
|
|
|
fmt.Printf("skipping draft %s (%dbytes)\n", file.Source, len(file.Body))
|
|
|
|
continue
|
|
|
|
}
|
2021-07-10 19:09:35 +03:00
|
|
|
tree.Files = append(tree.Files, file)
|
|
|
|
}
|
|
|
|
|
|
|
|
// call proc and pipe content through it, catch output of proc
|
|
|
|
for i, file := range tree.Files {
|
|
|
|
// fmt.Printf("%s -> %s (%d)\n", file.Source, file.Destination, len(file.Body))
|
|
|
|
extpipe := exec.Command(processor)
|
|
|
|
buf := bytes.NewReader(file.Body)
|
|
|
|
extpipe.Stdin = buf
|
|
|
|
|
|
|
|
var procout bytes.Buffer
|
|
|
|
extpipe.Stdout = &procout
|
|
|
|
|
|
|
|
extpipe.Start()
|
|
|
|
extpipe.Wait()
|
|
|
|
|
|
|
|
// write to source
|
|
|
|
tree.Files[i].NewBody = procout.Bytes()
|
2021-07-10 19:32:21 +03:00
|
|
|
fmt.Printf("processed %s (%dbytes)\n", file.Source, len(tree.Files[i].Body))
|
2021-07-10 19:09:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
newpath := filepath.Join(".", "public")
|
|
|
|
err = os.MkdirAll(newpath, os.ModePerm)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// write to destination
|
|
|
|
for _, file := range tree.Files {
|
2021-07-10 19:32:21 +03:00
|
|
|
//fmt.Printf("%s -> %s (%d)\n", file.Source, file.Destination, len(file.Body))
|
2021-07-10 19:09:35 +03:00
|
|
|
|
|
|
|
outfile := "index." + ext
|
|
|
|
outdir := filepath.Join(destination, file.Destination)
|
|
|
|
if file.Destination != "index" {
|
|
|
|
newpath := filepath.Join(destination, file.Destination)
|
|
|
|
if err = os.MkdirAll(newpath, os.ModePerm); err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
continue
|
|
|
|
}
|
2021-07-10 19:32:21 +03:00
|
|
|
// TODO: change dir^
|
|
|
|
// TODO: consider links
|
2021-07-10 20:18:52 +03:00
|
|
|
if uglyURLs {
|
2021-07-10 19:32:21 +03:00
|
|
|
outdir += ".gmi"
|
|
|
|
outfile = ""
|
|
|
|
}
|
2021-07-10 19:09:35 +03:00
|
|
|
} else {
|
|
|
|
outdir = destination
|
|
|
|
}
|
|
|
|
|
|
|
|
fullpath := filepath.Join(outdir, outfile)
|
|
|
|
newfile, err := os.Create(fullpath)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
continue
|
|
|
|
}
|
2021-07-10 19:32:21 +03:00
|
|
|
|
|
|
|
n, _ := newfile.Write(file.NewBody)
|
2021-07-10 19:09:35 +03:00
|
|
|
newfile.Close()
|
2021-07-10 19:32:21 +03:00
|
|
|
|
|
|
|
fmt.Printf("written %s (%dbytes)\n", fullpath, n)
|
2021-07-10 17:44:25 +03:00
|
|
|
}
|
|
|
|
|
2021-07-10 20:18:52 +03:00
|
|
|
// TODO
|
|
|
|
// ugly urls
|
|
|
|
//
|
|
|
|
// append section listings
|
|
|
|
// => link title line
|
|
|
|
// date - summary block
|
|
|
|
|
2021-07-10 19:32:21 +03:00
|
|
|
// TODO
|
|
|
|
// check/replace links
|
2021-07-10 17:44:25 +03:00
|
|
|
// write rss?
|
|
|
|
// write listings from template?
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
|
2021-07-10 17:44:25 +03:00
|
|
|
type FileTree struct {
|
2021-07-10 19:09:35 +03:00
|
|
|
Files []File
|
2021-07-10 17:44:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type File struct {
|
|
|
|
Root string
|
|
|
|
Source string
|
|
|
|
Destination string
|
|
|
|
Parent string
|
|
|
|
Name string
|
|
|
|
Extension string
|
|
|
|
|
2021-07-10 20:18:52 +03:00
|
|
|
Draft bool
|
|
|
|
|
2021-07-10 19:09:35 +03:00
|
|
|
Body []byte
|
|
|
|
NewBody []byte
|
2021-07-10 17:44:25 +03:00
|
|
|
}
|
|
|
|
|
2021-07-10 19:46:01 +03:00
|
|
|
func parse(fullpath string) ([]byte, *hugov0492.Content, error) {
|
2021-07-10 16:27:58 +03:00
|
|
|
file, err := os.Open(fullpath)
|
|
|
|
if err != nil {
|
2021-07-10 17:44:25 +03:00
|
|
|
return nil, nil, err
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
2021-07-10 19:09:35 +03:00
|
|
|
defer file.Close()
|
|
|
|
|
2021-07-10 19:46:01 +03:00
|
|
|
page, err := hugov0492.ReadFrom(file)
|
2021-07-10 16:27:58 +03:00
|
|
|
if err != nil {
|
2021-07-10 17:44:25 +03:00
|
|
|
return nil, nil, err
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
meta, err := page.Metadata()
|
|
|
|
if err != nil {
|
2021-07-10 17:44:25 +03:00
|
|
|
return nil, nil, err
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
c := NewContentFromMeta(meta)
|
2021-07-10 19:09:35 +03:00
|
|
|
body := page.FrontMatter()
|
|
|
|
body = append(body, '\n')
|
|
|
|
body = append(body, page.Content()...)
|
|
|
|
|
|
|
|
return body, c, nil
|
2021-07-10 17:44:25 +03:00
|
|
|
}
|
2021-07-10 16:27:58 +03:00
|
|
|
|
2021-07-10 17:44:25 +03:00
|
|
|
func destinationPath(file *File, pattern string) error {
|
|
|
|
body, c, err := parse(file.Source)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
c.Filepath = file.Name
|
|
|
|
file.Body = body
|
2021-07-10 20:18:52 +03:00
|
|
|
file.Draft = c.Draft
|
2021-07-10 17:44:25 +03:00
|
|
|
|
|
|
|
if file.Parent != "." {
|
2021-07-10 19:46:01 +03:00
|
|
|
link, err := hugov0492.PathPattern(pattern).Expand(c)
|
2021-07-10 17:44:25 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
file.Destination = link
|
|
|
|
} else {
|
|
|
|
file.Destination = strings.TrimLeft(file.Name, "_")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
|
2021-07-10 17:44:25 +03:00
|
|
|
func collectFiles(fullpath string, filechan chan File) error {
|
|
|
|
defer close(filechan)
|
2021-07-10 16:27:58 +03:00
|
|
|
return filepath.Walk(fullpath,
|
|
|
|
func(p string, info os.FileInfo, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if info.IsDir() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
rel, err := filepath.Rel(fullpath, p)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := info.Name()
|
|
|
|
ext := path.Ext(filename)
|
|
|
|
name := filename[0 : len(filename)-len(ext)]
|
|
|
|
parent := filepath.Dir(rel)
|
2021-07-10 17:44:25 +03:00
|
|
|
|
|
|
|
filechan <- File{
|
|
|
|
Root: fullpath,
|
|
|
|
Source: p,
|
|
|
|
Name: name,
|
|
|
|
Extension: ext,
|
|
|
|
Parent: parent,
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-10 19:46:01 +03:00
|
|
|
func NewContentFromMeta(meta map[string]interface{}) *hugov0492.Content {
|
|
|
|
return &hugov0492.Content{
|
2021-07-10 19:32:21 +03:00
|
|
|
Title: stringFromInterface(meta["title"]),
|
|
|
|
Slug: stringFromInterface(meta["slug"]),
|
|
|
|
Summary: stringFromInterface(meta["summary"]),
|
|
|
|
Categories: stringArrayFromInterface(meta["categories"]),
|
|
|
|
Tags: stringArrayFromInterface(meta["tags"]),
|
|
|
|
Date: dateFromInterface(meta["date"]),
|
2021-07-10 20:18:52 +03:00
|
|
|
Draft: boolFromInterface(meta["draft"]),
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-10 19:32:21 +03:00
|
|
|
func stringFromInterface(input interface{}) string {
|
2021-07-10 20:18:52 +03:00
|
|
|
str, _ := input.(string)
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
func boolFromInterface(input interface{}) bool {
|
|
|
|
v, _ := input.(bool)
|
|
|
|
return v
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
|
2021-07-10 19:32:21 +03:00
|
|
|
func dateFromInterface(input interface{}) time.Time {
|
2021-07-10 16:27:58 +03:00
|
|
|
str, ok := input.(string)
|
|
|
|
if !ok {
|
|
|
|
return time.Now()
|
|
|
|
|
|
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339, str)
|
|
|
|
if err != nil {
|
|
|
|
// try just date, or give up
|
|
|
|
t, err := time.Parse("2006-01-02", str)
|
|
|
|
if err != nil {
|
|
|
|
return time.Now()
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
2021-07-10 19:32:21 +03:00
|
|
|
func stringArrayFromInterface(input interface{}) []string {
|
|
|
|
strarr, ok := input.([]interface{})
|
2021-07-10 16:27:58 +03:00
|
|
|
if ok {
|
2021-07-10 19:32:21 +03:00
|
|
|
var out []string
|
|
|
|
for _, str := range strarr {
|
|
|
|
out = append(out, stringFromInterface(str))
|
|
|
|
}
|
|
|
|
return out
|
2021-07-10 16:27:58 +03:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|