diff --git a/main.go b/main.go index 873497a..cf2b42e 100644 --- a/main.go +++ b/main.go @@ -1,414 +1,420 @@ 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" + _ "embed" + "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" ) //go:embed example/.ssgod.json var DEFAULT_CONFIG string type PageMetaData struct { - pages []string + pages []string } func (meta *PageMetaData) AddPage(path string) { - path = RePrefixPath(path, "/") - path = filepath.ToSlash(path) - meta.pages = append(meta.pages, path) + 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) - panic(fmt.Sprintf(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) - panic(fmt.Sprintf(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(meta *PageMetaData) { - fmt.Println("PAGES: ", meta.pages) - - for _, path := range config.Settings.MetaFiles { - fmt.Println(">>> META >>>>", path) - - ext := filepath.Ext(path) - target_path := RePrefixPath(path, config.Settings.Target) - - 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}) - } - } + fmt.Println("PAGES: ", meta.pages) + + for _, path := range config.Settings.MetaFiles { + fmt.Println(">>> META >>>>", path) + + ext := filepath.Ext(path) + target_path := RePrefixPath(path, config.Settings.Target) + + 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:] + 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 := UnfuckedPathSplit(path) - 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 := UnfuckedPathSplit(path) + split_path := UnfuckedPathSplit(path) - prefixed_path := append([]string{new_prefix}, split_path...) + prefixed_path := append([]string{new_prefix}, split_path...) - res := filepath.Join(prefixed_path...) - return res + 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, meta *PageMetaData) error { - settings := config.Settings - var err error = nil + 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) { + 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, - map[string]any{"PageId": page_id}) + if ext == ".html" { + err = RenderHTML(path, target_path, + map[string]any{"PageId": page_id}) - if err != nil { - return Fail(err, "failed to render %s", path) - } + if err != nil { + return Fail(err, "failed to render %s", path) + } - 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) + 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) - 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) - } - } - } + 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() { - meta := new(PageMetaData) + 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 + } - err = ProcessDirEntry(path, d, meta) - if err != nil { return Fail(err, "failed to process %s", path) } + err = ProcessDirEntry(path, d, meta) + if err != nil { + return Fail(err, "failed to process %s", path) + } - return err - }) + return err + }) - if err != nil { - Fatal(err, "can't walk content") - } + if err != nil { + Fatal(err, "can't walk content") + } - RenderMetaFiles(meta) + 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() + } }