diff --git a/example/pages/sitemap.html b/example/pages/sitemap.html
index 2238074..b1517d9 100644
--- a/example/pages/sitemap.html
+++ b/example/pages/sitemap.html
@@ -2,7 +2,7 @@
{{range $index, $page := .Pages}}
- - {{$index}}: {{$page}}
+ - {{$page}}
{{ else }}
Nothing Here
{{end}}
diff --git a/main.go b/main.go
index e3b9a88..873497a 100644
--- a/main.go
+++ b/main.go
@@ -1,402 +1,414 @@
package main
import (
- "bytes"
- "flag"
- "fmt"
- "github.com/fsnotify/fsnotify"
- "github.com/yuin/goldmark"
- "io"
- "io/fs"
- "lcthw.dev/go/ssgod/config"
- "log"
- "os"
- "path/filepath"
- "strings"
- "text/template"
- "time"
- _ "embed"
+ "bytes"
+ "flag"
+ "fmt"
+ "github.com/fsnotify/fsnotify"
+ "github.com/yuin/goldmark"
+ "io"
+ "io/fs"
+ "lcthw.dev/go/ssgod/config"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+ "time"
+ _ "embed"
)
//go:embed example/.ssgod.json
var DEFAULT_CONFIG string
+type PageMetaData struct {
+ pages []string
+}
+
+func (meta *PageMetaData) AddPage(path string) {
+ path = RePrefixPath(path, "/")
+ path = filepath.ToSlash(path)
+ meta.pages = append(meta.pages, path)
+}
+
func Fatal(err error, format string, v ...any) {
- err_format := fmt.Sprintf("ERROR: %v; %s", err, format)
- log.Fatalf(err_format, v...)
+ err_format := fmt.Sprintf("ERROR: %v; %s", err, format)
+ panic(fmt.Sprintf(err_format, v...))
}
func Fail(err error, format string, v ...any) error {
- err_format := fmt.Sprintf("ERROR: %v; %s", err, format)
- log.Printf(err_format, v...)
- return err
+ err_format := fmt.Sprintf("ERROR: %v; %s", err, format)
+ panic(fmt.Sprintf(err_format, v...))
+ return err
}
func RenderTemplate(out io.Writer, embed string, variables any) error {
- layout_path := config.Settings.Layout
+ layout_path := config.Settings.Layout
- layout_main, err := os.ReadFile(layout_path)
+ layout_main, err := os.ReadFile(layout_path)
- if err != nil {
- return Fail(err, "can't read your layout file: %s", layout_path)
- }
+ if err != nil {
+ return Fail(err, "can't read your layout file: %s", layout_path)
+ }
- tmpl := template.New(layout_path)
+ tmpl := template.New(layout_path)
- callbacks := template.FuncMap{
- "embed": func() string {
- tmpl, err = tmpl.Parse(embed)
- if err != nil { Fatal(err, "error in your template") }
- out := bytes.NewBuffer(make([]byte, 0, 100))
- tmpl.Execute(out, variables)
- return out.String()
- },
- }
- tmpl.Funcs(callbacks)
+ callbacks := template.FuncMap{
+ "embed": func() string {
+ tmpl, err = tmpl.Parse(embed)
+ if err != nil { Fatal(err, "error in your template") }
+ out := bytes.NewBuffer(make([]byte, 0, 100))
+ tmpl.Execute(out, variables)
+ return out.String()
+ },
+ }
+ tmpl.Funcs(callbacks)
- tmpl, err = tmpl.Parse(string(layout_main))
- if err != nil {
- return Fail(err, "can't parse %s", layout_path)
- }
+ tmpl, err = tmpl.Parse(string(layout_main))
+ if err != nil {
+ return Fail(err, "can't parse %s", layout_path)
+ }
- err = tmpl.Execute(out, variables)
+ err = tmpl.Execute(out, variables)
- return err
+ return err
}
-func RenderMetaFiles(pages []string) {
- fmt.Println("PAGES: ", pages)
+func RenderMetaFiles(meta *PageMetaData) {
+ fmt.Println("PAGES: ", meta.pages)
- for _, path := range config.Settings.MetaFiles {
- fmt.Println(">>> META >>>>", path)
+ for _, path := range config.Settings.MetaFiles {
+ fmt.Println(">>> META >>>>", path)
- ext := filepath.Ext(path)
- target_path := RePrefixPath(path, config.Settings.Target)
+ ext := filepath.Ext(path)
+ target_path := RePrefixPath(path, config.Settings.Target)
- if ext == ".md" {
- RenderMarkdown(path, target_path,
- map[string]any{"Pages": pages})
- } else {
- RenderHTML(path, target_path,
- map[string]any{"Pages": pages})
- }
- }
+ if ext == ".md" {
+ RenderMarkdown(path, target_path,
+ map[string]any{"Pages": meta.pages})
+ } else if ext == ".html" {
+ RenderHTML(path, target_path,
+ map[string]any{"Pages": meta.pages})
+ }
+ }
}
func RenderMarkdown(path string, target_path string, vars map[string]any) error {
- log.Printf("MARKDOWN: %s -> %s", path, target_path)
+ log.Printf("MARKDOWN: %s -> %s", path, target_path)
- out, err := os.Create(target_path)
- defer out.Close()
+ out, err := os.Create(target_path)
+ defer out.Close()
- if err != nil {
- return Fail(err, "writing file %s", target_path)
- }
+ if err != nil {
+ return Fail(err, "writing file %s", target_path)
+ }
- input_data, err := os.ReadFile(path)
- var md_out bytes.Buffer
+ input_data, err := os.ReadFile(path)
+ var md_out bytes.Buffer
- err = goldmark.Convert(input_data, &md_out)
- if err != nil {
- return Fail(err, "failed converting markdown %s", path)
- }
+ err = goldmark.Convert(input_data, &md_out)
+ if err != nil {
+ return Fail(err, "failed converting markdown %s", path)
+ }
- err = RenderTemplate(out, md_out.String(), vars)
+ err = RenderTemplate(out, md_out.String(), vars)
- if err != nil {
- return Fail(err, "failed to render template %s->%s", path, target_path)
- }
+ if err != nil {
+ return Fail(err, "failed to render template %s->%s", path, target_path)
+ }
- return err
+ return err
}
func RenderHTML(source_path string, target_path string, vars map[string]any) error {
- log.Printf("RENDER: %s -> %s", source_path, target_path)
+ log.Printf("RENDER: %s -> %s", source_path, target_path)
- out, err := os.Create(target_path)
- defer out.Close()
+ out, err := os.Create(target_path)
+ defer out.Close()
- content, err := os.ReadFile(source_path)
- if err != nil {
- return Fail(err, "cannot open input %s", source_path)
- }
+ content, err := os.ReadFile(source_path)
+ if err != nil {
+ return Fail(err, "cannot open input %s", source_path)
+ }
- err = RenderTemplate(out, string(content), vars)
+ err = RenderTemplate(out, string(content), vars)
- if err != nil {
- return Fail(err, "writing file %s", target_path)
- }
+ if err != nil {
+ return Fail(err, "writing file %s", target_path)
+ }
- return err
+ return err
}
func MkdirPath(target_path string) error {
- target_dir := filepath.Dir(target_path)
- _, err := os.Stat(target_dir)
+ target_dir := filepath.Dir(target_path)
+ _, err := os.Stat(target_dir)
- if os.IsNotExist(err) {
- log.Println("MAKING: ", target_dir)
- err = os.MkdirAll(target_dir, 0750)
+ if os.IsNotExist(err) {
+ log.Println("MAKING: ", target_dir)
+ err = os.MkdirAll(target_dir, 0750)
- if err != nil {
- return Fail(err, "making path to %s", target_dir)
- }
- }
+ if err != nil {
+ return Fail(err, "making path to %s", target_dir)
+ }
+ }
- return nil
+ return nil
+}
+
+func UnfuckedPathSplit(path string) []string {
+ path = filepath.ToSlash(path)
+ // WARN: have to use strings.Split because fsnotify uses /, even on windows
+ return strings.Split(path, "/")[1:]
}
func SplitPathExt(path string) (string, string, bool) {
- split_path := strings.Split(path, string(os.PathSeparator))[1:]
- source_name := strings.Join(split_path, "/") // Render wants / even on windows
+ split_path := UnfuckedPathSplit(path)
+ source_name := strings.Join(split_path, "/") // Render wants / even on windows
- ext := filepath.Ext(source_name)
- source_name, found := strings.CutSuffix(source_name, ext)
- return source_name, ext, found
+ ext := filepath.Ext(source_name)
+ source_name, found := strings.CutSuffix(source_name, ext)
+ return source_name, ext, found
}
func RePrefixPath(path string, new_prefix string) string {
- split_path := strings.Split(path, string(os.PathSeparator))[1:]
+ split_path := UnfuckedPathSplit(path)
- prefixed_path := append([]string{new_prefix}, split_path...)
- return filepath.Join(prefixed_path...)
+ prefixed_path := append([]string{new_prefix}, split_path...)
+
+ res := filepath.Join(prefixed_path...)
+ return res
}
func SamePath(a string, b string) bool {
- return filepath.ToSlash(a) == filepath.ToSlash(b)
+ return filepath.ToSlash(a) == filepath.ToSlash(b)
}
-func ProcessDirEntry(path string, d fs.DirEntry, err error) error {
- settings := config.Settings
+func ProcessDirEntry(path string, d fs.DirEntry, meta *PageMetaData) error {
+ settings := config.Settings
+ var err error = nil
+
+ if !d.IsDir() && !SamePath(path, config.Settings.Layout) {
+ source_name, ext, found := SplitPathExt(path)
- if !d.IsDir() && !SamePath(path, config.Settings.Layout) {
- if err != nil {
- return Fail(err, "path: %s", path)
- }
+ if found && path != settings.Layout {
+ target_path := RePrefixPath(path, settings.Target)
- source_name, ext, found := SplitPathExt(path)
+ err = MkdirPath(target_path)
+ if err != nil {
+ return Fail(err, "making target path: %s", target_path)
+ }
- if found && path != settings.Layout {
- target_path := RePrefixPath(path, settings.Target)
+ // generate a data-testid for all pages based on template name
+ page_id := strings.ReplaceAll(source_name, "/", "-") + "-page"
- err = MkdirPath(target_path)
- if err != nil {
- return Fail(err, "making target path: %s", target_path)
- }
+ if ext == ".html" {
+ err = RenderHTML(path, target_path,
+ map[string]any{"PageId": page_id})
- // generate a data-testid for all pages based on template name
- page_id := strings.ReplaceAll(source_name, "/", "-") + "-page"
+ if err != nil {
+ return Fail(err, "failed to render %s", path)
+ }
- // BUG: make it so we can have a list of extensions
- if ext == ".html" || ext == ".xml" || ext == ".rss" {
- err = RenderHTML(path, target_path,
- map[string]any{"PageId": page_id})
+ meta.AddPage(target_path)
+ } else if ext == ".md" {
+ // need to strip the .md and replace with .html
+ html_name, _ := strings.CutSuffix(target_path, ext)
+ html_name = fmt.Sprintf("%s.html", html_name)
- if err != nil {
- return Fail(err, "failed to render %s", path)
- }
- } else if ext == ".md" {
- // need to strip the .md and replace with .html
- html_name, _ := strings.CutSuffix(target_path, ext)
- html_name = fmt.Sprintf("%s.html", html_name)
+ RenderMarkdown(path, html_name,
+ map[string]any{"PageId": page_id})
- RenderMarkdown(path, html_name,
- map[string]any{"PageId": page_id})
+ if err != nil {
+ return Fail(err, "failed to render markdown %s", path)
+ }
- if err != nil {
- return Fail(err, "failed to render markdown %s", path)
- }
- }
- }
- }
+ meta.AddPage(html_name)
+ }
+ }
+ }
- return nil
+ return nil
}
func SyncStaticDir() {
- target := config.Settings.Target
- sync_dir := config.Settings.SyncDir
- if sync_dir == "" {
- return
- }
-
- log.Printf("removing target directory: %s", target)
-
- err := os.RemoveAll(target)
- if err != nil {
- Fatal(err, "can't remove target directory: %s", target)
- }
-
- err = os.MkdirAll(target, 0750)
- if err != nil {
- Fatal(err, "can't recreate target directory: %s", target)
- }
-
- source := os.DirFS(sync_dir)
-
- log.Printf("SYNC %s -> %s", sync_dir, target)
- err = os.CopyFS(target, source)
- if err != nil {
- Fatal(err, "can't sync %s to target directory: %s", sync_dir, target)
- }
+ target := config.Settings.Target
+ sync_dir := config.Settings.SyncDir
+ if sync_dir == "" {
+ return
+ }
+
+ log.Printf("removing target directory: %s", target)
+
+ err := os.RemoveAll(target)
+ if err != nil {
+ Fatal(err, "can't remove target directory: %s", target)
+ }
+
+ err = os.MkdirAll(target, 0750)
+ if err != nil {
+ Fatal(err, "can't recreate target directory: %s", target)
+ }
+
+ source := os.DirFS(sync_dir)
+
+ log.Printf("SYNC %s -> %s", sync_dir, target)
+ err = os.CopyFS(target, source)
+ if err != nil {
+ Fatal(err, "can't sync %s to target directory: %s", sync_dir, target)
+ }
}
func RenderPages() {
- pages := make([]string, 0, 100)
+ meta := new(PageMetaData)
- err := filepath.WalkDir(config.Settings.Views,
- func(path string, d fs.DirEntry, err error) error {
- if err != nil { return err }
+ err := filepath.WalkDir(config.Settings.Views,
+ func(path string, d fs.DirEntry, err error) error {
+ if err != nil { return err }
- create an exclusion/inclusion system that knows if a file
- is a target, and only process it and add to pages if it matches
+ err = ProcessDirEntry(path, d, meta)
+ if err != nil { return Fail(err, "failed to process %s", path) }
- err = ProcessDirEntry(path, d, err)
- if err == nil {
- // no errors, add to the list
- pages = append(pages, path)
- }
-
- return err
- })
+ return err
+ })
- if err != nil {
- Fatal(err, "can't walk content")
- }
+ if err != nil {
+ Fatal(err, "can't walk content")
+ }
- RenderMetaFiles(pages)
+ RenderMetaFiles(meta)
}
func WatchMatches(name string) bool {
- is_static := strings.Index(name, config.Settings.SyncDir) == 0
- return is_static || filepath.Ext(name) == ".html" || filepath.Ext(name) == ".md"
+ is_static := strings.Index(name, config.Settings.SyncDir) == 0
+ return is_static || filepath.Ext(name) == ".html" || filepath.Ext(name) == ".md"
}
func AddWatchDir(watcher *fsnotify.Watcher, name string) error {
- return filepath.WalkDir(name,
- func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- log.Printf("WATCH ERROR! walking=%s path=%s: err=%v", name, path, err)
- return err
- }
-
- if d.IsDir() {
- log.Println("WATCHING: ", path)
- err = watcher.Add(path)
- if err != nil {
- log.Printf("failed to watch %s", path)
- return err
- }
- }
-
- return nil
- })
+ return filepath.WalkDir(name,
+ func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ log.Printf("WATCH ERROR! walking=%s path=%s: err=%v", name, path, err)
+ return err
+ }
+
+ if d.IsDir() {
+ log.Println("WATCHING: ", path)
+ err = watcher.Add(path)
+ if err != nil {
+ log.Printf("failed to watch %s", path)
+ return err
+ }
+ }
+
+ return nil
+ })
}
func WatchDir() {
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- Fatal(err, "can't create new watcher")
- }
-
- defer watcher.Close()
-
- wait_time, err := time.ParseDuration(config.Settings.WatchDelay)
- if err != nil {
- Fatal(err,
- "can't parse watch_delay setting: %s",
- config.Settings.WatchDelay)
- }
-
- doit := time.NewTimer(wait_time)
- doit.Stop()
-
- go func() {
- for {
- select {
- case event, ok := <-watcher.Events:
- if !ok {
- return
- }
-
- if event.Has(fsnotify.Create) {
- log.Println("---> CREATE IS:", event.Name)
- AddWatchDir(watcher, event.Name)
- } else {
- log.Println("event: ", event)
- }
-
- if WatchMatches(event.Name) {
- log.Println("modified file: ", event.Name)
- doit.Reset(wait_time)
- }
- case <-doit.C:
- SyncStaticDir()
- RenderPages()
- case err, ok := <-watcher.Errors:
- if !ok {
- return
- }
- log.Println("error: ", err)
- }
- }
- }()
-
- err = AddWatchDir(watcher, config.Settings.Views)
- if err != nil {
- Fatal(err, "failed to watch %s", config.Settings.Views)
- }
-
- err = AddWatchDir(watcher, filepath.Dir(config.Settings.Layout))
- if err != nil {
- Fatal(err, "failed to watch %s", filepath.Dir(config.Settings.Layout))
- }
-
- if config.Settings.SyncDir != "" {
- err = AddWatchDir(watcher, config.Settings.SyncDir)
- if err != nil {
- Fatal(err, "failed to watch %s", config.Settings.SyncDir)
- }
- }
-
- <-make(chan struct{})
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ Fatal(err, "can't create new watcher")
+ }
+
+ defer watcher.Close()
+
+ wait_time, err := time.ParseDuration(config.Settings.WatchDelay)
+ if err != nil {
+ Fatal(err,
+ "can't parse watch_delay setting: %s",
+ config.Settings.WatchDelay)
+ }
+
+ doit := time.NewTimer(wait_time)
+ doit.Stop()
+
+ go func() {
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+
+ if event.Has(fsnotify.Create) {
+ log.Println("---> CREATE IS:", event.Name)
+ AddWatchDir(watcher, event.Name)
+ } else {
+ log.Println("event: ", event)
+ }
+
+ if WatchMatches(event.Name) {
+ log.Println("modified file: ", event.Name)
+ doit.Reset(wait_time)
+ }
+ case <-doit.C:
+ SyncStaticDir()
+ RenderPages()
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ log.Println("error: ", err)
+ }
+ }
+ }()
+
+ err = AddWatchDir(watcher, config.Settings.Views)
+ if err != nil {
+ Fatal(err, "failed to watch %s", config.Settings.Views)
+ }
+
+ err = AddWatchDir(watcher, filepath.Dir(config.Settings.Layout))
+ if err != nil {
+ Fatal(err, "failed to watch %s", filepath.Dir(config.Settings.Layout))
+ }
+
+ if config.Settings.SyncDir != "" {
+ err = AddWatchDir(watcher, config.Settings.SyncDir)
+ if err != nil {
+ Fatal(err, "failed to watch %s", config.Settings.SyncDir)
+ }
+ }
+
+ <-make(chan struct{})
}
func InitConfig(config_file string) {
- _, err := os.Stat(config_file)
-
- if os.IsNotExist(err) {
- out, err := os.Create(config_file)
- if err != nil {
- Fatal(err, "error opening %s", config_file)
- }
- defer out.Close()
-
- out.WriteString(DEFAULT_CONFIG)
- fmt.Println("new config written to:", config_file)
- } else {
- Fatal(err, "there's already a %s file here", config_file)
- }
+ _, err := os.Stat(config_file)
+
+ if os.IsNotExist(err) {
+ out, err := os.Create(config_file)
+ if err != nil {
+ Fatal(err, "error opening %s", config_file)
+ }
+ defer out.Close()
+
+ out.WriteString(DEFAULT_CONFIG)
+ fmt.Println("new config written to:", config_file)
+ } else {
+ Fatal(err, "there's already a %s file here", config_file)
+ }
}
func main() {
- var config_file string
-
- flag.StringVar(&config_file, "config", ".ssgod.json", ".json config file to use")
- flag.Parse()
- command := flag.Arg(0)
-
- switch command {
- case "watch":
- config.Load(config_file)
- SyncStaticDir()
- RenderPages()
- WatchDir()
- case "init":
- InitConfig(config_file)
- default:
- config.Load(config_file)
- SyncStaticDir()
- RenderPages()
- }
+ var config_file string
+
+ flag.StringVar(&config_file, "config", ".ssgod.json", ".json config file to use")
+ flag.Parse()
+ command := flag.Arg(0)
+
+ switch command {
+ case "watch":
+ config.Load(config_file)
+ SyncStaticDir()
+ RenderPages()
+ WatchDir()
+ case "init":
+ InitConfig(config_file)
+ default:
+ config.Load(config_file)
+ SyncStaticDir()
+ RenderPages()
+ }
}