diff --git a/example/ssgod.toml b/example/ssgod.toml index 4571fea..34438b9 100644 --- a/example/ssgod.toml +++ b/example/ssgod.toml @@ -1,4 +1,3 @@ - views = "pages" layout = "pages/layouts/main.html" target = "public" diff --git a/main.go b/main.go index 0752c51..5f06db8 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,20 @@ package main import ( - "log" - "fmt" - "bytes" - "strings" - "io/fs" - "io" - "path/filepath" - "os" - "flag" - "time" - "text/template" - "lcthw.dev/go/ssgod/config" - "github.com/yuin/goldmark" - "github.com/fsnotify/fsnotify" + "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" ) var DEFAULT_CONFIG = ` @@ -27,289 +27,337 @@ watch_delay = "500ms" ` 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) + log.Fatalf(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) + log.Printf(err_format, v...) + return err } -func RenderTemplate(out io.Writer, embed string, variables any) (error) { - layout_path := config.Settings.Layout +func RenderTemplate(out io.Writer, embed string, variables any) error { + 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 { return embed }, - } - tmpl.Funcs(callbacks) + callbacks := template.FuncMap{ + "embed": func() string { return embed }, + } + 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 RenderMarkdown(path string, target_path string, page_id string) error { - log.Printf("MARKDOWN: %s -> %s", path, target_path) + log.Printf("MARKDOWN: %s -> %s", path, target_path) - out, err := os.OpenFile(target_path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - defer out.Close() + out, err := os.OpenFile(target_path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + 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(), - map[string]string{"PageId": page_id}) + err = RenderTemplate(out, md_out.String(), + map[string]string{"PageId": page_id}) - 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, page_id string) error { - log.Printf("RENDER: %s -> %s", source_path, target_path) + log.Printf("RENDER: %s -> %s", source_path, target_path) - out, err := os.OpenFile(target_path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - defer out.Close() + out, err := os.OpenFile(target_path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + defer out.Close() - html_content, err := os.ReadFile(source_path) - if err != nil { return Fail(err, "cannot open html input %s", source_path) } + html_content, err := os.ReadFile(source_path) + if err != nil { + return Fail(err, "cannot open html input %s", source_path) + } - err = RenderTemplate(out, string(html_content), - map[string]string{"PageId": page_id}) + err = RenderTemplate(out, string(html_content), + map[string]string{"PageId": page_id}) - 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 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 := strings.Split(path, string(os.PathSeparator))[1:] + 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 := strings.Split(path, string(os.PathSeparator))[1:] - prefixed_path := append([]string{new_prefix}, split_path...) - return filepath.Join(prefixed_path...) + prefixed_path := append([]string{new_prefix}, split_path...) + return filepath.Join(prefixed_path...) } 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, err error) error { + settings := config.Settings - if !d.IsDir() && !SamePath(path, config.Settings.Layout) { - if err != nil { return Fail(err, "path: %s", path); } + if !d.IsDir() && !SamePath(path, config.Settings.Layout) { + if err != nil { + return Fail(err, "path: %s", path) + } - source_name, ext, found := SplitPathExt(path) + source_name, ext, found := SplitPathExt(path) - if found && path != settings.Layout { - target_path := RePrefixPath(path, settings.Target) + if found && path != settings.Layout { + target_path := RePrefixPath(path, settings.Target) - err = MkdirPath(target_path) - if err != nil { return Fail(err, "making target path: %s", target_path) } + err = MkdirPath(target_path) + if err != nil { + return Fail(err, "making target path: %s", target_path) + } - // generate a data-testid for all pages based on template name - page_id := strings.ReplaceAll(source_name, "/", "-") + "-page" + // generate a data-testid for all pages based on template name + page_id := strings.ReplaceAll(source_name, "/", "-") + "-page" - if ext == ".html" { - err = RenderHTML(path, target_path, page_id) + if ext == ".html" { + err = RenderHTML(path, target_path, page_id) - 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) + 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, page_id) + RenderMarkdown(path, html_name, 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) + } + } + } + } - 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() { - err := filepath.WalkDir(config.Settings.Views, - func (path string, d fs.DirEntry, err error) error { - return ProcessDirEntry(path, d, err) - }) - - if err != nil { log.Fatalf("can't walk content") } + err := filepath.WalkDir(config.Settings.Views, + func(path string, d fs.DirEntry, err error) error { + return ProcessDirEntry(path, d, err) + }) + + if err != nil { + log.Fatalf("can't walk content") + } } 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.Fatalf("WATCH ERROR! path=%s: err=%v", path, err) } - - if d.IsDir() { - log.Println("WATCHING: ", path) - err = watcher.Add(path) - if err != nil { return Fail(err, "failed to watch %s", path) } - } - - 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 { log.Fatal(err) } - - defer watcher.Close() - - wait_time, err := time.ParseDuration(config.Settings.WatchDelay) - if err != nil { log.Fatal(err) } - - doit := time.NewTimer(wait_time) - doit.Stop() - - go func() { - for { - select { - case event, ok := <-watcher.Events: - if !ok { return } - 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)) - } - - 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 { + log.Fatal(err) + } + + defer watcher.Close() + + wait_time, err := time.ParseDuration(config.Settings.WatchDelay) + if err != nil { + log.Fatal(err) + } + + 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.OpenFile(config_file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { log.Fatalf("error opening %s", config_file) } - defer out.Close() - - out.WriteString(DEFAULT_CONFIG) - } else { - log.Fatalf("there's already a %s file here", config_file); - } + _, err := os.Stat(config_file) + + if os.IsNotExist(err) { + out, err := os.OpenFile(config_file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatalf("error opening %s", config_file) + } + defer out.Close() + + out.WriteString(DEFAULT_CONFIG) + } else { + log.Fatalf("there's already a %s file here", config_file) + } } func main() { - var config_file string - - flag.StringVar(&config_file, "config", "ssgod.toml", ".toml 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.toml", ".toml 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() + } }