From 7a178720044d4fb99ef929238b937dbbe3c8b122 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 15:52:15 +0100 Subject: [PATCH 01/30] refactor: move and rename DirExist to cli and new copyFile helper --- internal/checks/checks.go | 21 ------------ internal/cli/cli.go | 4 +-- internal/cli/fileops.go | 70 +++++++++++++++++++++++++++------------ 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/internal/checks/checks.go b/internal/checks/checks.go index f1e41dc..0684ed2 100644 --- a/internal/checks/checks.go +++ b/internal/checks/checks.go @@ -34,24 +34,3 @@ func FileExist(f string) (bool, error) { return true, nil } - -// Ensure a directory exists -func DirExist(dir string) error { - if err := os.Mkdir(dir, 0o755); err == nil { - return nil - } else if os.IsExist(err) { - // check that the existing path is a directory - info, err := os.Stat(dir) - if err != nil { - return err - } - - if !info.IsDir() { - return fmt.Errorf("path exists but is not a directory") - } - - return nil - } - - return nil -} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index ef7a3ca..27d4d53 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -117,7 +117,7 @@ func Parse() error { return fmt.Errorf("absolute path: %w", err) } - if err = checks.DirExist(outDir); err != nil { + if err = mkdirIfNotExists(outDir); err != nil { return fmt.Errorf("output directory: %w", err) } @@ -131,7 +131,7 @@ func Parse() error { return fmt.Errorf("absolute path: %w", err) } - if err = checks.DirExist(outDir); err != nil { + if err = mkdirIfNotExists(outDir); err != nil { return fmt.Errorf("output directory: %w", err) } diff --git a/internal/cli/fileops.go b/internal/cli/fileops.go index d72cf14..b4cb8f4 100644 --- a/internal/cli/fileops.go +++ b/internal/cli/fileops.go @@ -44,39 +44,67 @@ func removeFiles(files []string) error { return nil } +// copyFile +func copyFile(inPath string, outPath string, permission os.FileMode) error { + content, err := os.ReadFile(inPath) + if err != nil { + return fmt.Errorf("read file %s: %w", inPath, err) + } + + if err = os.WriteFile(outPath, content, permission); err != nil { + return fmt.Errorf("write file %s: %w", outPath, err) + } + + return nil +} + +// mkdirIfNotExists takes in a directory path, checks if it exists, and +// create it if not +func mkdirIfNotExists(dir string) error { + if err := os.Mkdir(dir, 0o755); err == nil { + return nil + } else if os.IsExist(err) { + // check that the existing path is a directory + info, err := os.Stat(dir) + if err != nil { + return err + } + + if !info.IsDir() { + return fmt.Errorf("%s exists but is not a directory", dir) + } + + return nil + } else { + return fmt.Errorf("mkdir %s: %w", dir, err) + } +} + // Move extra files like assets (images, fonts, css) over to output, preserving // the file structure. func syncAssets(ctx context.Context, s *state.State, logger *slog.Logger) error { eg, _ := errgroup.WithContext(ctx) - for f := range s.Assets { - f := f - - child := logger.With(slog.String("filepath", f), slog.String("context", "copying asset")) + for assetPath := range s.Assets { + child := logger.With( + slog.String("filepath", assetPath), + slog.String("context", "copying asset"), + ) child.Debug("submitting goroutine") eg.Go(func() error { - // want our assets to go from inDir/a/b/c/image.png -> outDir/a/b/c/image.png - rel, _ := filepath.Rel(s.InDir, f) - path := filepath.Join(s.OutDir, rel) - - // Make dir on filesystem - if err := checks.DirExist(filepath.Dir(path)); err != nil { - return fmt.Errorf("make directory: %w", err) - } - - // Copy from f to out - b, err := os.ReadFile(f) - if err != nil { - return fmt.Errorf("read file: %w", err) - } + // NOTE: want our assets to go from inDir/a/b/c/image.png -> outDir/a/b/c/image.png + relToInputDir, _ := filepath.Rel(s.InDir, assetPath) + parentOutputDir := filepath.Join(s.OutDir, relToInputDir) - if err = os.WriteFile(path, b, 0o644); err != nil { - return fmt.Errorf("write file: %w", err) + // Make equivalent directory in output directory + if err := mkdirIfNotExists(filepath.Dir(parentOutputDir)); err != nil { + return err } - return nil + // Copy file to target directory + return copyFile(assetPath, parentOutputDir, 0o644) }) } From 0dba0f73f7f638c8f6189cc9514a9093cf17fb7a Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 16:05:58 +0100 Subject: [PATCH 02/30] refactor: remove DirExists altogether --- internal/cli/cli.go | 13 +++++-------- internal/cli/fileops.go | 31 +++++-------------------------- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 27d4d53..b0fac01 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -114,11 +114,7 @@ func Parse() error { // Sanitize input directory inDir, err = filepath.Abs(inDir) if err != nil { - return fmt.Errorf("absolute path: %w", err) - } - - if err = mkdirIfNotExists(outDir); err != nil { - return fmt.Errorf("output directory: %w", err) + return fmt.Errorf("filepath.Abs: %w", err) } s.InDir = inDir @@ -128,11 +124,12 @@ func Parse() error { // Sanitize output directory outDir, err = filepath.Abs(outDir) if err != nil { - return fmt.Errorf("absolute path: %w", err) + return fmt.Errorf("filepath.Abs: %w", err) } - if err = mkdirIfNotExists(outDir); err != nil { - return fmt.Errorf("output directory: %w", err) + // Create output directory + if err := os.MkdirAll(outDir, 0o755); err != nil { + return fmt.Errorf("os.MkdirAll %s: %v", outDir, err) } s.OutDir = outDir diff --git a/internal/cli/fileops.go b/internal/cli/fileops.go index b4cb8f4..be83ac4 100644 --- a/internal/cli/fileops.go +++ b/internal/cli/fileops.go @@ -58,28 +58,6 @@ func copyFile(inPath string, outPath string, permission os.FileMode) error { return nil } -// mkdirIfNotExists takes in a directory path, checks if it exists, and -// create it if not -func mkdirIfNotExists(dir string) error { - if err := os.Mkdir(dir, 0o755); err == nil { - return nil - } else if os.IsExist(err) { - // check that the existing path is a directory - info, err := os.Stat(dir) - if err != nil { - return err - } - - if !info.IsDir() { - return fmt.Errorf("%s exists but is not a directory", dir) - } - - return nil - } else { - return fmt.Errorf("mkdir %s: %w", dir, err) - } -} - // Move extra files like assets (images, fonts, css) over to output, preserving // the file structure. func syncAssets(ctx context.Context, s *state.State, logger *slog.Logger) error { @@ -96,15 +74,16 @@ func syncAssets(ctx context.Context, s *state.State, logger *slog.Logger) error eg.Go(func() error { // NOTE: want our assets to go from inDir/a/b/c/image.png -> outDir/a/b/c/image.png relToInputDir, _ := filepath.Rel(s.InDir, assetPath) - parentOutputDir := filepath.Join(s.OutDir, relToInputDir) + outputPath := filepath.Join(s.OutDir, relToInputDir) + parentOutputDir := filepath.Dir(outputPath) // Make equivalent directory in output directory - if err := mkdirIfNotExists(filepath.Dir(parentOutputDir)); err != nil { - return err + if err := os.MkdirAll(parentOutputDir, 0o755); err != nil { + return fmt.Errorf("os.MkdirAll %s: %v", parentOutputDir, err) } // Copy file to target directory - return copyFile(assetPath, parentOutputDir, 0o644) + return copyFile(assetPath, outputPath, 0o644) }) } From ad170e5628f2755309348a5bc6848771e64dd9e9 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 16:33:39 +0100 Subject: [PATCH 03/30] refactor: copyFile to use stream --- internal/cli/fileops.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/cli/fileops.go b/internal/cli/fileops.go index be83ac4..78af7d0 100644 --- a/internal/cli/fileops.go +++ b/internal/cli/fileops.go @@ -44,15 +44,24 @@ func removeFiles(files []string) error { return nil } -// copyFile +// copyFile copies inPath to outPath using ioReader and ioWriter func copyFile(inPath string, outPath string, permission os.FileMode) error { - content, err := os.ReadFile(inPath) + inFile, err := os.Open(inPath) if err != nil { - return fmt.Errorf("read file %s: %w", inPath, err) + return fmt.Errorf("os.Open %s: %w", inPath, err) } + defer inFile.Close() - if err = os.WriteFile(outPath, content, permission); err != nil { - return fmt.Errorf("write file %s: %w", outPath, err) + outFile, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, permission) + if err != nil { + return fmt.Errorf("os.OpenFile %s: %w", outPath, err) + } + defer outFile.Close() + + // Copy the content using a stream + _, err = io.Copy(outFile, inFile) + if err != nil { + return fmt.Errorf("io.Copy %s to %s: %w", inPath, outPath, err) } return nil From 91c103a3a8325b010e979bef1230fc019e54e726 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 16:34:04 +0100 Subject: [PATCH 04/30] feat: func to copy static files to output dir --- internal/cli/fileops.go | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/internal/cli/fileops.go b/internal/cli/fileops.go index 78af7d0..e307885 100644 --- a/internal/cli/fileops.go +++ b/internal/cli/fileops.go @@ -99,6 +99,74 @@ func syncAssets(ctx context.Context, s *state.State, logger *slog.Logger) error return eg.Wait() } +// copyStatic +func copyStatic(s *state.State, logger *slog.Logger) error { + outputDir := filepath.Join(s.OutDir, relStaticOutputDir) + + // Make static directory in output directory + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return fmt.Errorf("os.MkdirAll %s: %w", outputDir, err) + } + + logger.Debug("created static output directory", slog.String("dir", outputDir)) + + staticFS, err := fs.Sub(EmbedFS, relStaticDir) + if err != nil { + return fmt.Errorf("create subfilesystem %s: %w", relStaticDir, err) + } + + logger.Debug("created static subfilesystem", slog.String("dir", relStaticDir)) + + // Walk through all files and directories in the `staticFS`. + // Starting at the root of the sub-filesystem. + if err := fs.WalkDir(staticFS, ".", func(currentPath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip directories and only process files + if d.IsDir() { + return nil + } + + // Construct the destination path for the file + outputPath := filepath.Join(outputDir, currentPath) + parentOutputDir := filepath.Dir(outputPath) + + // Create the destination directory if it doesn't exist + if err := os.MkdirAll(parentOutputDir, 0o755); err != nil { + return fmt.Errorf("os.MkdirAll %s: %w", parentOutputDir, err) + } + + // Open the source file from the embedded filesystem + srcFile, err := staticFS.Open(currentPath) + if err != nil { + return err + } + defer srcFile.Close() + + // Create the destination file + destFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer destFile.Close() + + // Copy the content + if _, err := io.Copy(destFile, srcFile); err != nil { + return err + } + + return nil + }); err != nil { + return fmt.Errorf("fs.WalkDir: %w", err) + } + + logger.Debug("walked static subfilesystem", slog.String("outputDir", outputDir)) + + return nil +} + // Filter hidden files from files func filterHiddenFiles(inDir string, files []string) []string { newFiles := []string{} From 126bef145324f309b76c05849e37ed085db47ac8 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 16:35:02 +0100 Subject: [PATCH 05/30] feat: implement copy static group --- internal/cli/cli.go | 44 ++++++++++++++++++++++++++++++++------------ main.go | 4 ++-- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index b0fac01..eca7204 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -23,12 +23,14 @@ import ( ) const ( - templateDir = "web/template" - version = "v2.3.2" + relTemplateDir = "web/template" + relStaticDir = "web/static" + relStaticOutputDir = "static" + version = "v2.3.2" ) var ( - Templates embed.FS + EmbedFS embed.FS configFile, outDir, inDir string dryRun bool showVersion bool @@ -143,7 +145,7 @@ func Parse() error { } if fileExist, err := checks.FileExist(configFile); err != nil { - return fmt.Errorf("stat: %v", err) + return fmt.Errorf("stat: %w", err) } else if !fileExist { return fmt.Errorf("missing: %s", configFile) } @@ -162,6 +164,7 @@ func Parse() error { if !s.DryRun { logger.Info("cleaning output directory") + // TODO: update this so it ignores staticOutputDir files, err := filepath.Glob(outDir + "/*") if err != nil { return fmt.Errorf("glob files: %w", err) @@ -176,7 +179,7 @@ func Parse() error { // Initiate templates s.Templates = template.Must(template.ParseFS( - Templates, filepath.Join(templateDir, "*"))) + EmbedFS, filepath.Join(relTemplateDir, "*"))) logger.Debug("loaded templates") @@ -223,10 +226,8 @@ func Parse() error { ) // Copy asset dirs/files over to output directory - logger.Info("syncing assets") - - moveGroup, _ := errgroup.WithContext(context.Background()) - moveGroup.Go(func() error { + assetsMoveGroup, _ := errgroup.WithContext(context.Background()) + assetsMoveGroup.Go(func() error { if err := syncAssets(context.Background(), &s, logger); err != nil { return err } @@ -235,20 +236,39 @@ func Parse() error { }, ) - // Create beautiful HTML - logger.Info("templating all files") + logger.Info("synced user assets") + + // Copy static content to the output directory + staticMoveGroup, _ := errgroup.WithContext(context.Background()) + staticMoveGroup.Go(func() error { + if err := copyStatic(&s, logger); err != nil { + return err + } + return nil + }, + ) + + logger.Info("copied static files") + + // Create beautiful HTML renderGroup, _ := errgroup.WithContext(context.Background()) renderGroup.Go(func() error { return render.Render(context.Background(), &s, logger) }) + logger.Info("templated all source files") + // Wait for all goroutines to finish if err := feedGroup.Wait(); err != nil { return err } - if err := moveGroup.Wait(); err != nil { + if err := assetsMoveGroup.Wait(); err != nil { + return err + } + + if err := staticMoveGroup.Wait(); err != nil { return err } diff --git a/main.go b/main.go index f871947..9fa66d3 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "github.com/mstcl/pher/v2/internal/cli" ) -//go:embed web/template/* +//go:embed web/template/* web/static/* var fs embed.FS func main() { @@ -31,7 +31,7 @@ func main() { TimeFormat: time.Kitchen, })) - cli.Templates = fs + cli.EmbedFS = fs if err := cli.Parse(); err != nil { logger.Error(fmt.Sprintf("%v", err)) From 37e87dea808df3b17fd5e568d5044f961bc43e66 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 20:13:29 +0100 Subject: [PATCH 06/30] feat: move css in head to static file --- internal/cli/cli.go | 13 +++- web/template/head.tmpl | 160 +---------------------------------------- 2 files changed, 11 insertions(+), 162 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index eca7204..47e6fb9 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -178,10 +178,17 @@ func Parse() error { } // Initiate templates - s.Templates = template.Must(template.ParseFS( - EmbedFS, filepath.Join(relTemplateDir, "*"))) - logger.Debug("loaded templates") + // TODO: split this out to separate package + funcMap := template.FuncMap{ + "joinPath": path.Join, + } + + tmpl := template.New("main") + tmpl = tmpl.Funcs(funcMap) + s.Templates = template.Must(tmpl.ParseFS(EmbedFS, filepath.Join(relTemplateDir, "*"))) + + logger.Debug("loaded and initialized templates") // Grab files and reorder so indexes are processed last files, err := zglob.Glob(s.InDir + "/**/*.md") diff --git a/web/template/head.tmpl b/web/template/head.tmpl index df1525d..8ab04e4 100644 --- a/web/template/head.tmpl +++ b/web/template/head.tmpl @@ -15,166 +15,8 @@ {{.Title}} + {{.Head}} {{end}} From 00e1ef5f3baa7ecaa22f234446fda4a77012d127 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 20:14:01 +0100 Subject: [PATCH 07/30] feat: vertical style for wide screens --- web/template/index.tmpl | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/web/template/index.tmpl b/web/template/index.tmpl index ee26ce3..6ff17b5 100644 --- a/web/template/index.tmpl +++ b/web/template/index.tmpl @@ -3,18 +3,20 @@ {{- template "head" . -}} - {{- template "header" .}} -
- {{- template "article" .}} - {{- if eq .Layout "log"}} - {{- if .Listing}} - {{- template "log" .}} - {{end}} - {{end}} -
- {{- if or .Backlinks .Listing .Relatedlinks}} - {{- template "aside" . -}} - {{- end}} + {{- template "header" .}} +
+
+ {{- template "article" .}} + {{- if eq .Layout "log"}} + {{- if .Listing}} + {{- template "log" .}} + {{end}} + {{end}} +
+ {{- if or .Backlinks .Listing .Relatedlinks}} + {{- template "aside" . -}} + {{- end}} +
{{- if .Footer}} {{- template "footer" . -}} {{- end}} From c853462e3390cbd3a8a907fc4e80844919797269 Mon Sep 17 00:00:00 2001 From: code Date: Sun, 14 Sep 2025 20:14:24 +0100 Subject: [PATCH 08/30] fix: use path.Join in template instead of string manipulation --- web/template/aside.tmpl | 7 ++++--- web/template/header.tmpl | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/template/aside.tmpl b/web/template/aside.tmpl index 90c09c1..290c9cc 100644 --- a/web/template/aside.tmpl +++ b/web/template/aside.tmpl @@ -43,7 +43,7 @@ {{- range .Backlinks}}