From c0b98d7a99cf78468c4daeb8909832418c2f2160 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 17 Nov 2024 15:00:55 -0500 Subject: [PATCH 01/64] removed DEV code from main branch --- build.sh | 4 -- cmd/zona/main.go | 53 --------------------- go.mod | 5 -- internal/convert/convert.go | 76 ------------------------------ internal/tree/node.go | 50 -------------------- internal/util/path.go | 94 ------------------------------------- test/d1/file1 | 0 test/d2/tt | 0 test/d5/d4/anod/arst | 0 test/d5/d4/tta | 0 test/d5/t | 0 test/in.md | 3 -- test/test.html | 3 -- zona | 1 - 14 files changed, 289 deletions(-) delete mode 100755 build.sh delete mode 100644 cmd/zona/main.go delete mode 100644 go.mod delete mode 100644 internal/convert/convert.go delete mode 100644 internal/tree/node.go delete mode 100644 internal/util/path.go delete mode 100644 test/d1/file1 delete mode 100644 test/d2/tt delete mode 100644 test/d5/d4/anod/arst delete mode 100644 test/d5/d4/tta delete mode 100644 test/d5/t delete mode 100644 test/in.md delete mode 100644 test/test.html delete mode 120000 zona diff --git a/build.sh b/build.sh deleted file mode 100755 index fc1bdf6..0000000 --- a/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -go build -o bin/zona ./cmd/zona -ln -sf bin/zona ./zona diff --git a/cmd/zona/main.go b/cmd/zona/main.go deleted file mode 100644 index 2b0fe15..0000000 --- a/cmd/zona/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "errors" - "flag" - "fmt" - "os" - - "github.com/ficcdaf/zona/internal/util" -) - -// validateFile checks whether a given path -// is a valid file && matches an expected extension -func validateFile(path, ext string) bool { - return (util.CheckExtension(path, ext) == nil) && (util.PathIsValid(path, true)) -} - -func main() { - mdPath := flag.String("file", "", "Path to the markdown file.") - flag.Parse() - if *mdPath == "" { - // no flag provided, check for positional argument instead - n := flag.NArg() - var e error - switch n { - case 1: - // we read the positional arg - arg := flag.Arg(0) - // mdPath wants a pointer so we get arg's address - mdPath = &arg - case 0: - // in case of no flag and no arg, we fail - e = errors.New("Required argument missing!") - default: - // more args than expected is also fail - e = errors.New("Unexpected arguments!") - } - if e != nil { - fmt.Printf("Error: %s\n", e.Error()) - os.Exit(1) - } - - } - // if !validateFile(*mdPath, ".md") { - // fmt.Println("File validation failed!") - // os.Exit(1) - // } - // convert.ConvertFile(*mdPath, "test/test.html") - err := util.Traverse(*mdPath, "foobar") - if err != nil { - fmt.Printf("Error: %s\n", err.Error()) - } -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 182992a..0000000 --- a/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/ficcdaf/zona - -go 1.23.2 - -require github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 // indirect diff --git a/internal/convert/convert.go b/internal/convert/convert.go deleted file mode 100644 index f6222ff..0000000 --- a/internal/convert/convert.go +++ /dev/null @@ -1,76 +0,0 @@ -package convert - -import ( - "io" - "os" - - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" -) - -// This function takes a Markdown document and returns an HTML document. -func MdToHTML(md []byte) ([]byte, error) { - // create parser with extensions - extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock - p := parser.NewWithExtensions(extensions) - doc := p.Parse(md) - - // build HTML renderer - htmlFlags := html.CommonFlags | html.HrefTargetBlank - opts := html.RendererOptions{Flags: htmlFlags} - renderer := html.NewRenderer(opts) - - return markdown.Render(doc, renderer), nil -} - -// WriteFile writes a given byte array to the given path. -func WriteFile(b []byte, p string) error { - f, err := os.Create(p) - if err != nil { - return err - } - _, err = f.Write(b) - defer f.Close() - if err != nil { - return err - } - return nil -} - -// ReadFile reads a byte array from a given path. -func ReadFile(p string) ([]byte, error) { - f, err := os.Open(p) - if err != nil { - return nil, err - } - var result []byte - buf := make([]byte, 1024) - for { - n, err := f.Read(buf) - // check for a non EOF error - if err != nil && err != io.EOF { - return nil, err - } - // n==0 when there are no chunks left to read - if n == 0 { - defer f.Close() - break - } - result = append(result, buf[:n]...) - } - return result, nil -} - -func ConvertFile(in string, out string) error { - md, err := ReadFile(in) - if err != nil { - return err - } - html, err := MdToHTML(md) - if err != nil { - return err - } - err = WriteFile(html, out) - return err -} diff --git a/internal/tree/node.go b/internal/tree/node.go deleted file mode 100644 index 9025b3d..0000000 --- a/internal/tree/node.go +++ /dev/null @@ -1,50 +0,0 @@ -package tree - -// Node is a struct containing nodes of a file tree. -type Node struct { - // can be nil - Parent *Node - Name string - // Empty value mean directory - Ext string - // cannot be nil; may have len 0 - Children []*Node -} - -// NewNode constructs and returns a Node instance. -// parent and children are optional and can be nil, -// in which case Parent will be stored as nil, -// but Children will be initialized as []*Node of len 0. -// If ext == "", the Node is a directory. -func NewNode(name string, ext string, parent *Node, children []*Node) *Node { - var c []*Node - if children == nil { - c = make([]*Node, 0) - } else { - c = children - } - n := &Node{ - Name: name, - Ext: ext, - Parent: parent, - Children: c, - } - return n -} - -func (n *Node) IsRoot() bool { - return n.Parent == nil -} - -func (n *Node) IsTail() bool { - return len(n.Children) == 0 -} - -func (n *Node) IsDir() bool { - return n.Ext == "" -} - -// TODO: Implement recursive depth-first traversal to process a tree - -func Traverse(root *Node) { -} diff --git a/internal/util/path.go b/internal/util/path.go deleted file mode 100644 index 7013734..0000000 --- a/internal/util/path.go +++ /dev/null @@ -1,94 +0,0 @@ -// Package util provides general utilities. -package util - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" -) - -// CheckExtension checks if the file located at path (string) -// matches the provided extension type -func CheckExtension(path, ext string) error { - if filepath.Ext(path) == ext { - return nil - } else { - return errors.New("Invalid extension.") - } -} - -// PathIsValid checks if a path is valid. -// If requireFile is set, directories are not considered valid. -func PathIsValid(path string, requireFile bool) bool { - s, err := os.Stat(path) - if os.IsNotExist(err) { - return false - } else if requireFile { - // fmt.Printf("Directory status: %s\n", strconv.FormatBool(s.IsDir())) - return !s.IsDir() - } - return err == nil -} - -func getRoot(path string) string { - for { - parent := filepath.Dir(path) - if parent == "." { - break - } - path = parent - } - fmt.Println("getRoot: ", path) - return path -} - -func replaceRoot(inPath, outRoot string) string { - relPath := strings.TrimPrefix(inPath, getRoot(inPath)) - outPath := filepath.Join(outRoot, relPath) - return outPath -} - -func createFileWithParents(path string) error { - dir := filepath.Dir(path) - // Check if the parent directory already exists - // before trying to create it - if _, dirErr := os.Stat(dir); os.IsNotExist(dirErr) { - // create directories - err := os.MkdirAll(dir, os.ModePerm) - if err != nil { - return err - } - // TODO: write the file here - } - return nil -} - -func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) error { - if err != nil { - return err - } - if !entry.IsDir() { - ext := filepath.Ext(inPath) - fmt.Println("Root: ", replaceRoot(inPath, outRoot)) - switch ext { - case ".md": - fmt.Println("Processing markdown...") - default: - // All other file types, we copy! - } - } - fmt.Printf("Visited: %s\n", inPath) - return nil -} - -func Traverse(root string, outRoot string) error { - // err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { - walkFunc := func(path string, entry fs.DirEntry, err error) error { - return processFile(path, entry, err, outRoot) - } - err := filepath.WalkDir(root, walkFunc) - return err -} diff --git a/test/d1/file1 b/test/d1/file1 deleted file mode 100644 index e69de29..0000000 diff --git a/test/d2/tt b/test/d2/tt deleted file mode 100644 index e69de29..0000000 diff --git a/test/d5/d4/anod/arst b/test/d5/d4/anod/arst deleted file mode 100644 index e69de29..0000000 diff --git a/test/d5/d4/tta b/test/d5/d4/tta deleted file mode 100644 index e69de29..0000000 diff --git a/test/d5/t b/test/d5/t deleted file mode 100644 index e69de29..0000000 diff --git a/test/in.md b/test/in.md deleted file mode 100644 index a933e82..0000000 --- a/test/in.md +++ /dev/null @@ -1,3 +0,0 @@ -# My amazing markdown file! - -I can _even_ do **this**! diff --git a/test/test.html b/test/test.html deleted file mode 100644 index ff2795a..0000000 --- a/test/test.html +++ /dev/null @@ -1,3 +0,0 @@ -

My amazing markdown file!

- -

I can even do this!

diff --git a/zona b/zona deleted file mode 120000 index 0ee2e17..0000000 --- a/zona +++ /dev/null @@ -1 +0,0 @@ -bin/zona \ No newline at end of file From e68611afb1f3d320b1fff4e94bb0895a61d5902f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 18 Nov 2024 00:18:08 -0500 Subject: [PATCH 02/64] updated markdown package required --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 182992a..3868457 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/ficcdaf/zona go 1.23.2 -require github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 // indirect +require github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 // indirect From 79ade5e8a39223060d9b70253ba843f8bac43c02 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 18 Nov 2024 00:40:19 -0500 Subject: [PATCH 03/64] Updated Readme to reflect current state of the project; new design goals; new roadmap items fixed TOC Fixed typos --- README.md | 78 ++++++++++++++++++++----------------------------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index f4f5af4..8e7584c 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,45 @@ # Zona -Zona is a small tool for building a static blog website. It allows users to write pages and blog posts in Markdown and automatically build them into a static, lightweight blog. +Zona is a small tool for building a static website. It will allows users to write pages and blog posts in Markdown and build them into a static, lightweight website. -> **Warning:** Implementing v1 functionality is still WIP. There will be binaries available to download from the Releases tab when Zona is ready to be used. Until then, you are welcome to download the source code, but I can't promise anything will work! +**Warning:** Zona has not yet reached **v1**, and it is not yet functional. Therefore, no installation instructions are provided. You may check progress in the `dev` branch of this repository. ## Table of Contents -- [Features](#v1-features) -- [Installation](#installation) +- [Design Goals](#design-goals) +- [v1 Features](#v1-features) - [Roadmap](#roadmap) +- [Contribution](#contribution) +- [Inspirations](#inspirations) + +## Design Goals + +Zona is intended to be lightweight, easy-to-use, and ship with sane defaults while still providing flexibility and powerful customization options. This tool is intended for people that wish to maintain a simple blog or personal website, do not want to write HTML, and don't need any JavaScript or backend functionality. This makes it perfect for static site hosting like Neocities or GitHub Pages. ## v1 Features -- Write your pages in Markdown. -- Build a lightweight website with zero JavaScript. -- Simple CLI build interface. -- HTML layout optimized for screen readers and Neocities. - -## Getting Started - -### Dependencies - -- `go 1.23.2` - -```Bash -# On Arch Linux -sudo pacman -S go - -# On Ubuntu/Debian -sudo apt install go -``` - -### Installation - -First, download the repository and open it: - -```Bash -git clone https://github.com/ficcdaf/zona.git && cd zona -``` - -On Linux: - -```Bash -# run the provided build script -./build.sh -``` - -On other platforms: - -```Bash -go build -o bin/zona cmd/zona -``` - -The resulting binary can be found at `bin/zona`. +- Write pages in Markdown and build a simple HTML website. +- Automatically generated `Archive` page and `Recent Posts` element. +- Custom CSS support with sensible light & dark default themes. +- Single-command build process with optional flags for more control. +- HTML layout optimized for screen readers. +- Lightweight output. ## Roadmap -- [ ] Zona configuration file to define build options. -- [ ] Image optimization & dithering options. -- [ ] AUR package after first release -- [ ] Automatic RSS/Atom feed generation. +- [ ] Optional configuration file to define build options. +- [ ] Optional RSS/Atom feed generation. +- [ ] Optional image optimization & dithering. +- [ ] AUR package. +- [ ] Windows, Mac, Linux releases. +- [ ] Custom tags that expand to user-defined HTML templates. +- [ ] Companion Neovim plugin for previewing website while editing. + +## Contribution + +Zona is a small project maintained by a very busy graduate student. If you want to contribute, you are more than welcome to submit issues and pull requests. ## Inspirations - [Zoner](https://git.sr.ht/~ryantrawick/zoner) -- Zonelets +- [Zonelets](https://zonelets.net/) From a42cf66cee55a427347da5193fedde1cce0494ba Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 15:42:11 -0500 Subject: [PATCH 04/64] Implemented copying site directory and processing certain filetypes --- internal/convert/convert.go | 20 ++++++++++++++++++++ internal/util/path.go | 31 +++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index f6222ff..fe8e8b3 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -3,6 +3,8 @@ package convert import ( "io" "os" + "path/filepath" + "strings" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" @@ -62,6 +64,20 @@ func ReadFile(p string) ([]byte, error) { return result, nil } +// CopyFile reads the file at the input path, and write +// it to the output path. +func CopyFile(inPath string, outPath string) error { + inB, err := ReadFile(inPath) + if err != nil { + return err + } + if err := WriteFile(inB, outPath); err != nil { + return err + } else { + return nil + } +} + func ConvertFile(in string, out string) error { md, err := ReadFile(in) if err != nil { @@ -74,3 +90,7 @@ func ConvertFile(in string, out string) error { err = WriteFile(html, out) return err } + +func ChangeExtension(in string, outExt string) string { + return strings.TrimSuffix(in, filepath.Ext(in)) + outExt +} diff --git a/internal/util/path.go b/internal/util/path.go index 7013734..2cacbf8 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/ficcdaf/zona/internal/convert" ) // CheckExtension checks if the file located at path (string) @@ -51,17 +53,15 @@ func replaceRoot(inPath, outRoot string) string { return outPath } -func createFileWithParents(path string) error { +func createParents(path string) error { dir := filepath.Dir(path) // Check if the parent directory already exists // before trying to create it if _, dirErr := os.Stat(dir); os.IsNotExist(dirErr) { // create directories - err := os.MkdirAll(dir, os.ModePerm) - if err != nil { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { return err } - // TODO: write the file here } return nil } @@ -72,12 +72,31 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er } if !entry.IsDir() { ext := filepath.Ext(inPath) - fmt.Println("Root: ", replaceRoot(inPath, outRoot)) + outPath := replaceRoot(inPath, outRoot) + fmt.Println("NewRoot: ", outPath) switch ext { case ".md": fmt.Println("Processing markdown...") + outPath = convert.ChangeExtension(outPath, ".html") + if err := createParents(outPath); err != nil { + return err + } + if err := convert.ConvertFile(inPath, outPath); err != nil { + return errors.Join(errors.New("Error processing file "+inPath), err) + } else { + return nil + } + // If it's not a file we need to process, + // we simply copy it to the destination path. default: - // All other file types, we copy! + if err := createParents(outPath); err != nil { + return err + } + if err := convert.CopyFile(inPath, outPath); err != nil { + return errors.Join(errors.New("Error processing file "+inPath), err) + } else { + return nil + } } } fmt.Printf("Visited: %s\n", inPath) From 7aebcef803bab92e2a20f4ea92a8e96fff0954d7 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 17:46:09 -0500 Subject: [PATCH 05/64] added testing output dir to ignore list --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7f6e79c..dc6a675 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ bin/ go.sum # test/ +foobar/ From 46e4f483f6f34e259340d89373d28b7598cb5311 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 17:46:36 -0500 Subject: [PATCH 06/64] created testing functions for conversion operations --- internal/convert/convert_test.go | 122 +++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 internal/convert/convert_test.go diff --git a/internal/convert/convert_test.go b/internal/convert/convert_test.go new file mode 100644 index 0000000..68c1134 --- /dev/null +++ b/internal/convert/convert_test.go @@ -0,0 +1,122 @@ +package convert_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/ficcdaf/zona/internal/convert" + "github.com/ficcdaf/zona/internal/util" +) + +func TestMdToHTML(t *testing.T) { + md := []byte("# Hello World\n\nThis is a test.") + expectedHTML := "

Hello World

\n

This is a test.

\n" + nExpectedHTML := util.NormalizeContent(expectedHTML) + html, err := convert.MdToHTML(md) + nHtml := util.NormalizeContent(string(html)) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if nHtml != nExpectedHTML { + t.Errorf("Expected:\n%s\nGot:\n%s", expectedHTML, html) + } +} + +func TestWriteFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "test.txt") + content := []byte("Hello, World!") + + err := convert.WriteFile(content, path) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Verify file content + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("Error reading file: %v", err) + } + if string(data) != string(content) { + t.Errorf("Expected:\n%s\nGot:\n%s", content, data) + } +} + +func TestReadFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "test.txt") + content := []byte("Hello, World!") + + err := os.WriteFile(path, content, 0644) + if err != nil { + t.Fatalf("Error writing file: %v", err) + } + + data, err := convert.ReadFile(path) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if string(data) != string(content) { + t.Errorf("Expected:\n%s\nGot:\n%s", content, data) + } +} + +func TestCopyFile(t *testing.T) { + src := filepath.Join(t.TempDir(), "source.txt") + dst := filepath.Join(t.TempDir(), "dest.txt") + content := []byte("File content for testing.") + + err := os.WriteFile(src, content, 0644) + if err != nil { + t.Fatalf("Error writing source file: %v", err) + } + + err = convert.CopyFile(src, dst) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Verify destination file content + data, err := os.ReadFile(dst) + if err != nil { + t.Fatalf("Error reading destination file: %v", err) + } + if string(data) != string(content) { + t.Errorf("Expected:\n%s\nGot:\n%s", content, data) + } +} + +func TestConvertFile(t *testing.T) { + src := filepath.Join(t.TempDir(), "test.md") + dst := filepath.Join(t.TempDir(), "test.html") + mdContent := []byte("# Test Title\n\nThis is Markdown content.") + nExpectedHTML := util.NormalizeContent("

Test Title

\n

This is Markdown content.

\n") + + err := os.WriteFile(src, mdContent, 0644) + if err != nil { + t.Fatalf("Error writing source Markdown file: %v", err) + } + + err = convert.ConvertFile(src, dst) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Verify destination HTML content + data, err := os.ReadFile(dst) + if err != nil { + t.Fatalf("Error reading HTML file: %v", err) + } + if util.NormalizeContent(string(data)) != nExpectedHTML { + t.Errorf("Expected:\n%s\nGot:\n%s", nExpectedHTML, data) + } +} + +func TestChangeExtension(t *testing.T) { + input := "test.md" + output := convert.ChangeExtension(input, ".html") + expected := "test.html" + + if output != expected { + t.Errorf("Expected %s, got %s", expected, output) + } +} From 7915a4bb09479c8579f15b2c9134fefce564947f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 17:47:08 -0500 Subject: [PATCH 07/64] Implemented proper conversion of links to local markdown files into html links --- cmd/zona/main.go | 15 ++++------ internal/convert/convert.go | 57 +++++++++++++++++++++++++++++++++++-- internal/util/path.go | 19 ++----------- internal/util/util.go | 15 ++++++++++ runtest.sh | 8 ++++++ test/in.md | 3 ++ test/page.md | 3 ++ 7 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 internal/util/util.go create mode 100755 runtest.sh create mode 100644 test/page.md diff --git a/cmd/zona/main.go b/cmd/zona/main.go index 2b0fe15..d7032e3 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -9,11 +9,11 @@ import ( "github.com/ficcdaf/zona/internal/util" ) -// validateFile checks whether a given path -// is a valid file && matches an expected extension -func validateFile(path, ext string) bool { - return (util.CheckExtension(path, ext) == nil) && (util.PathIsValid(path, true)) -} +// // validateFile checks whether a given path +// // is a valid file && matches an expected extension +// func validateFile(path, ext string) bool { +// return (util.CheckExtension(path, ext) == nil) && (util.PathIsValid(path, true)) +// } func main() { mdPath := flag.String("file", "", "Path to the markdown file.") @@ -41,11 +41,6 @@ func main() { } } - // if !validateFile(*mdPath, ".md") { - // fmt.Println("File validation failed!") - // os.Exit(1) - // } - // convert.ConvertFile(*mdPath, "test/test.html") err := util.Traverse(*mdPath, "foobar") if err != nil { fmt.Printf("Error: %s\n", err.Error()) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index fe8e8b3..2cb51eb 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -1,12 +1,14 @@ package convert import ( + "fmt" "io" "os" "path/filepath" "strings" "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) @@ -19,13 +21,64 @@ func MdToHTML(md []byte) ([]byte, error) { doc := p.Parse(md) // build HTML renderer - htmlFlags := html.CommonFlags | html.HrefTargetBlank + htmlFlags := html.CommonFlags | html.HrefTargetBlank | html.CompletePage opts := html.RendererOptions{Flags: htmlFlags} - renderer := html.NewRenderer(opts) + renderer := newZonaRenderer(opts) return markdown.Render(doc, renderer), nil } +// PathIsValid checks if a path is valid. +// If requireFile is set, directories are not considered valid. +func PathIsValid(path string, requireFile bool) bool { + s, err := os.Stat(path) + if os.IsNotExist(err) { + return false + } else if requireFile { + // fmt.Printf("Directory status: %s\n", strconv.FormatBool(s.IsDir())) + return !s.IsDir() + } + return err == nil +} + +func processLink(p string) string { + fmt.Println("Processing link...") + ext := filepath.Ext(p) + // Only process if it points to an existing, local markdown file + if ext == ".md" && filepath.IsLocal(p) { + fmt.Println("Markdown link detected...") + return ChangeExtension(p, ".html") + } else { + return p + } +} + +func renderLink(w io.Writer, l *ast.Link, entering bool) { + if entering { + destPath := processLink(string(l.Destination)) + fmt.Fprintf(w, `") + } else { + io.WriteString(w, "") + } +} + +func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { + if link, ok := node.(*ast.Link); ok { + renderLink(w, link, entering) + return ast.GoToNext, true + } + return ast.GoToNext, false +} + +func newZonaRenderer(opts html.RendererOptions) *html.Renderer { + opts.RenderNodeHook = htmlRenderHook + return html.NewRenderer(opts) +} + // WriteFile writes a given byte array to the given path. func WriteFile(b []byte, p string) error { f, err := os.Create(p) diff --git a/internal/util/path.go b/internal/util/path.go index 2cacbf8..1e79964 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -22,19 +22,6 @@ func CheckExtension(path, ext string) error { } } -// PathIsValid checks if a path is valid. -// If requireFile is set, directories are not considered valid. -func PathIsValid(path string, requireFile bool) bool { - s, err := os.Stat(path) - if os.IsNotExist(err) { - return false - } else if requireFile { - // fmt.Printf("Directory status: %s\n", strconv.FormatBool(s.IsDir())) - return !s.IsDir() - } - return err == nil -} - func getRoot(path string) string { for { parent := filepath.Dir(path) @@ -43,7 +30,7 @@ func getRoot(path string) string { } path = parent } - fmt.Println("getRoot: ", path) + // fmt.Println("getRoot: ", path) return path } @@ -73,7 +60,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er if !entry.IsDir() { ext := filepath.Ext(inPath) outPath := replaceRoot(inPath, outRoot) - fmt.Println("NewRoot: ", outPath) + // fmt.Println("NewRoot: ", outPath) switch ext { case ".md": fmt.Println("Processing markdown...") @@ -99,7 +86,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er } } } - fmt.Printf("Visited: %s\n", inPath) + // fmt.Printf("Visited: %s\n", inPath) return nil } diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..2894192 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,15 @@ +package util + +import "strings" + +func NormalizeContent(content string) string { + var normalized []string + lines := strings.Split(content, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" { + normalized = append(normalized, line) + } + } + return strings.Join(normalized, "\n") +} diff --git a/runtest.sh b/runtest.sh new file mode 100755 index 0000000..d20c26e --- /dev/null +++ b/runtest.sh @@ -0,0 +1,8 @@ +#!/bin/bash +if [ -e foobar ]; then + rm -rf foobar +fi + +go run cmd/zona/main.go test + +bat foobar/in.html diff --git a/test/in.md b/test/in.md index a933e82..578595f 100644 --- a/test/in.md +++ b/test/in.md @@ -1,3 +1,6 @@ # My amazing markdown file! I can _even_ do **this**! + +- Or, I could... +- [Link](page.md) to this file diff --git a/test/page.md b/test/page.md new file mode 100644 index 0000000..e11434d --- /dev/null +++ b/test/page.md @@ -0,0 +1,3 @@ +# My amazing markdown filez 2! + +This file gets linked to... From 68d2ddb692b4d8df978a55db1c1b3d83d7416da7 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 19:38:05 -0500 Subject: [PATCH 08/64] Began implementing YAML metadata --- go.mod | 5 ++- internal/convert/build_page.go | 65 ++++++++++++++++++++++++++++++++++ internal/convert/convert.go | 15 +------- internal/util/path.go | 1 - templates/article.html | 28 +++++++++++++++ test/yamltest.md | 10 ++++++ 6 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 internal/convert/build_page.go create mode 100644 templates/article.html create mode 100644 test/yamltest.md diff --git a/go.mod b/go.mod index 3868457..979a4ca 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/ficcdaf/zona go 1.23.2 -require github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 // indirect +require ( + github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/internal/convert/build_page.go b/internal/convert/build_page.go new file mode 100644 index 0000000..ac63fd2 --- /dev/null +++ b/internal/convert/build_page.go @@ -0,0 +1,65 @@ +package convert + +import ( + "bytes" + "fmt" + "html/template" + "strings" + + "gopkg.in/yaml.v3" +) + +type PageData struct { + Title string + Icon string + Stylesheet string + Header template.HTML + Content template.HTML + NextPost template.HTML + PrevPost template.HTML + Footer template.HTML +} + +func processWithYaml(f []byte) (Metadata, []byte, error) { + // Check if the file has valid metadata + if !bytes.HasPrefix(f, []byte("---\n")) { + // No valid yaml, so return the entire content + return nil, f, nil + } + // Separate YAML from rest of document + split := strings.SplitN(string(f), "---\n", 3) + if len(split) < 3 { + return nil, nil, fmt.Errorf("Invalid frontmatter format.") + } + var metadata Metadata + // Parse YAML + if err := yaml.Unmarshal([]byte(split[1]), &metadata); err != nil { + return nil, nil, err + } + return metadata, []byte(split[2]), nil +} + +func ConvertFile(in string, out string) error { + mdPre, err := ReadFile(in) + if err != nil { + return err + } + metadata, md, err := processWithYaml(mdPre) + if err != nil { + return err + } + + title, ok := metadata["title"].(string) + if !ok { + fmt.Println("No title in page.") + } else { + fmt.Println("Title found: ", title) + } + + html, err := MdToHTML(md) + if err != nil { + return err + } + err = WriteFile(html, out) + return err +} diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 2cb51eb..46d7fc0 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -21,7 +21,7 @@ func MdToHTML(md []byte) ([]byte, error) { doc := p.Parse(md) // build HTML renderer - htmlFlags := html.CommonFlags | html.HrefTargetBlank | html.CompletePage + htmlFlags := html.CommonFlags | html.HrefTargetBlank opts := html.RendererOptions{Flags: htmlFlags} renderer := newZonaRenderer(opts) @@ -131,19 +131,6 @@ func CopyFile(inPath string, outPath string) error { } } -func ConvertFile(in string, out string) error { - md, err := ReadFile(in) - if err != nil { - return err - } - html, err := MdToHTML(md) - if err != nil { - return err - } - err = WriteFile(html, out) - return err -} - func ChangeExtension(in string, outExt string) string { return strings.TrimSuffix(in, filepath.Ext(in)) + outExt } diff --git a/internal/util/path.go b/internal/util/path.go index 1e79964..2f125f9 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -60,7 +60,6 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er if !entry.IsDir() { ext := filepath.Ext(inPath) outPath := replaceRoot(inPath, outRoot) - // fmt.Println("NewRoot: ", outPath) switch ext { case ".md": fmt.Println("Processing markdown...") diff --git a/templates/article.html b/templates/article.html new file mode 100644 index 0000000..eb677d8 --- /dev/null +++ b/templates/article.html @@ -0,0 +1,28 @@ + + + + {{ .Title }} + + + + + + +
+ +
+ {{ .Content }} + +
+
{{ .Footer }}
+
+ + diff --git a/test/yamltest.md b/test/yamltest.md new file mode 100644 index 0000000..974dedb --- /dev/null +++ b/test/yamltest.md @@ -0,0 +1,10 @@ +--- +title: Yaml testing file +--- + +# My amazing markdown file! + +I can _even_ do **this**! + +- Or, I could... +- [Link](page.md) to this file From 11f724732dfbd82ad1e884a55219165e8eb3c037 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 21:14:38 -0500 Subject: [PATCH 09/64] refactored some modules --- cmd/zona/main.go | 10 +++--- internal/convert/build_page.go | 60 ++++++++++++++++++++++++++-------- internal/convert/convert.go | 48 +++++++++++++++++++++++++++ internal/util/path.go | 53 ++---------------------------- 4 files changed, 102 insertions(+), 69 deletions(-) diff --git a/cmd/zona/main.go b/cmd/zona/main.go index d7032e3..e2d2a64 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -6,7 +6,7 @@ import ( "fmt" "os" - "github.com/ficcdaf/zona/internal/util" + "github.com/ficcdaf/zona/internal/convert" ) // // validateFile checks whether a given path @@ -16,9 +16,9 @@ import ( // } func main() { - mdPath := flag.String("file", "", "Path to the markdown file.") + rootPath := flag.String("file", "", "Path to the markdown file.") flag.Parse() - if *mdPath == "" { + if *rootPath == "" { // no flag provided, check for positional argument instead n := flag.NArg() var e error @@ -27,7 +27,7 @@ func main() { // we read the positional arg arg := flag.Arg(0) // mdPath wants a pointer so we get arg's address - mdPath = &arg + rootPath = &arg case 0: // in case of no flag and no arg, we fail e = errors.New("Required argument missing!") @@ -41,7 +41,7 @@ func main() { } } - err := util.Traverse(*mdPath, "foobar") + err := convert.Traverse(*rootPath, "foobar") if err != nil { fmt.Printf("Error: %s\n", err.Error()) } diff --git a/internal/convert/build_page.go b/internal/convert/build_page.go index ac63fd2..7505db9 100644 --- a/internal/convert/build_page.go +++ b/internal/convert/build_page.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "html/template" + "path/filepath" "strings" "gopkg.in/yaml.v3" @@ -13,13 +14,15 @@ type PageData struct { Title string Icon string Stylesheet string - Header template.HTML + Header string Content template.HTML - NextPost template.HTML - PrevPost template.HTML - Footer template.HTML + NextPost string + PrevPost string + Footer string } +type Metadata map[string]interface{} + func processWithYaml(f []byte) (Metadata, []byte, error) { // Check if the file has valid metadata if !bytes.HasPrefix(f, []byte("---\n")) { @@ -31,12 +34,43 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { if len(split) < 3 { return nil, nil, fmt.Errorf("Invalid frontmatter format.") } - var metadata Metadata + var meta Metadata // Parse YAML - if err := yaml.Unmarshal([]byte(split[1]), &metadata); err != nil { + if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil { return nil, nil, err } - return metadata, []byte(split[2]), nil + return meta, []byte(split[2]), nil +} + +// this function converts a file path to its title form +func pathToTitle(path string) string { + stripped := ChangeExtension(filepath.Base(path), "") + replaced := strings.NewReplacer("-", " ", "_", " ", `\ `, " ").Replace(stripped) + return strings.ToTitle(replaced) +} + +func buildPageData(m Metadata) *PageData { + p := &PageData{} + if title, ok := m["title"].(string); ok { + p.Title = title + } else { + p.Title = pathToTitle(m["path"].(string)) + } + if icon, ok := m["icon"].(string); ok { + p.Icon = icon + } else { + p.Icon = "" + } + if style, ok := m["style"].(string); ok { + p.Stylesheet = style + } + if header, ok := m["header"].(string); ok { + p.Header = header + } + if footer, ok := m["footer"].(string); ok { + p.Footer = footer + } + return p } func ConvertFile(in string, out string) error { @@ -48,14 +82,14 @@ func ConvertFile(in string, out string) error { if err != nil { return err } + metadata["path"] = in + pd := buildPageData(metadata) + fmt.Println("Page title: ", pd.Title) + pd.Content = template.HTML(md) - title, ok := metadata["title"].(string) - if !ok { - fmt.Println("No title in page.") - } else { - fmt.Println("Title found: ", title) - } + tmlp, err := template.New("webpage").Parse("placeholder") + // build according to template here html, err := MdToHTML(md) if err != nil { return err diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 46d7fc0..2cfdaf5 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -1,12 +1,15 @@ package convert import ( + "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" + "github.com/ficcdaf/zona/internal/util" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/html" @@ -134,3 +137,48 @@ func CopyFile(inPath string, outPath string) error { func ChangeExtension(in string, outExt string) string { return strings.TrimSuffix(in, filepath.Ext(in)) + outExt } + +func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) error { + if err != nil { + return err + } + if !entry.IsDir() { + ext := filepath.Ext(inPath) + outPath := util.ReplaceRoot(inPath, outRoot) + switch ext { + case ".md": + fmt.Println("Processing markdown...") + outPath = ChangeExtension(outPath, ".html") + if err := util.CreateParents(outPath); err != nil { + return err + } + if err := ConvertFile(inPath, outPath); err != nil { + return errors.Join(errors.New("Error processing file "+inPath), err) + } else { + return nil + } + // If it's not a file we need to process, + // we simply copy it to the destination path. + default: + if err := util.CreateParents(outPath); err != nil { + return err + } + if err := CopyFile(inPath, outPath); err != nil { + return errors.Join(errors.New("Error processing file "+inPath), err) + } else { + return nil + } + } + } + // fmt.Printf("Visited: %s\n", inPath) + return nil +} + +func Traverse(root string, outRoot string) error { + // err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + walkFunc := func(path string, entry fs.DirEntry, err error) error { + return processFile(path, entry, err, outRoot) + } + err := filepath.WalkDir(root, walkFunc) + return err +} diff --git a/internal/util/path.go b/internal/util/path.go index 2f125f9..7359afb 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -3,13 +3,9 @@ package util import ( "errors" - "fmt" - "io/fs" "os" "path/filepath" "strings" - - "github.com/ficcdaf/zona/internal/convert" ) // CheckExtension checks if the file located at path (string) @@ -34,13 +30,13 @@ func getRoot(path string) string { return path } -func replaceRoot(inPath, outRoot string) string { +func ReplaceRoot(inPath, outRoot string) string { relPath := strings.TrimPrefix(inPath, getRoot(inPath)) outPath := filepath.Join(outRoot, relPath) return outPath } -func createParents(path string) error { +func CreateParents(path string) error { dir := filepath.Dir(path) // Check if the parent directory already exists // before trying to create it @@ -52,48 +48,3 @@ func createParents(path string) error { } return nil } - -func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) error { - if err != nil { - return err - } - if !entry.IsDir() { - ext := filepath.Ext(inPath) - outPath := replaceRoot(inPath, outRoot) - switch ext { - case ".md": - fmt.Println("Processing markdown...") - outPath = convert.ChangeExtension(outPath, ".html") - if err := createParents(outPath); err != nil { - return err - } - if err := convert.ConvertFile(inPath, outPath); err != nil { - return errors.Join(errors.New("Error processing file "+inPath), err) - } else { - return nil - } - // If it's not a file we need to process, - // we simply copy it to the destination path. - default: - if err := createParents(outPath); err != nil { - return err - } - if err := convert.CopyFile(inPath, outPath); err != nil { - return errors.Join(errors.New("Error processing file "+inPath), err) - } else { - return nil - } - } - } - // fmt.Printf("Visited: %s\n", inPath) - return nil -} - -func Traverse(root string, outRoot string) error { - // err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { - walkFunc := func(path string, entry fs.DirEntry, err error) error { - return processFile(path, entry, err, outRoot) - } - err := filepath.WalkDir(root, walkFunc) - return err -} From 64e243773a670db0b7133f9e5a0840214ed6a544 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 24 Nov 2024 21:49:37 -0500 Subject: [PATCH 10/64] implemented basic templating and default settings --- cmd/zona/config.go | 1 + cmd/zona/main.go | 4 +- internal/{convert => build}/build_page.go | 35 ++++++++----- internal/build/config.go | 52 +++++++++++++++++++ internal/{convert => build}/convert.go | 50 +------------------ internal/{convert => build}/convert_test.go | 16 +++--- internal/build/traverse.go | 55 +++++++++++++++++++++ 7 files changed, 143 insertions(+), 70 deletions(-) create mode 100644 cmd/zona/config.go rename internal/{convert => build}/build_page.go (77%) create mode 100644 internal/build/config.go rename internal/{convert => build}/convert.go (70%) rename internal/{convert => build}/convert_test.go (90%) create mode 100644 internal/build/traverse.go diff --git a/cmd/zona/config.go b/cmd/zona/config.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/zona/config.go @@ -0,0 +1 @@ +package main diff --git a/cmd/zona/main.go b/cmd/zona/main.go index e2d2a64..d925f7d 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -6,7 +6,7 @@ import ( "fmt" "os" - "github.com/ficcdaf/zona/internal/convert" + "github.com/ficcdaf/zona/internal/build" ) // // validateFile checks whether a given path @@ -41,7 +41,7 @@ func main() { } } - err := convert.Traverse(*rootPath, "foobar") + err := build.Traverse(*rootPath, "foobar") if err != nil { fmt.Printf("Error: %s\n", err.Error()) } diff --git a/internal/convert/build_page.go b/internal/build/build_page.go similarity index 77% rename from internal/convert/build_page.go rename to internal/build/build_page.go index 7505db9..bcb5a38 100644 --- a/internal/convert/build_page.go +++ b/internal/build/build_page.go @@ -1,4 +1,4 @@ -package convert +package build import ( "bytes" @@ -49,26 +49,32 @@ func pathToTitle(path string) string { return strings.ToTitle(replaced) } -func buildPageData(m Metadata) *PageData { +func buildPageData(m Metadata, path string) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { p.Title = title } else { - p.Title = pathToTitle(m["path"].(string)) + p.Title = pathToTitle(path) } if icon, ok := m["icon"].(string); ok { p.Icon = icon } else { - p.Icon = "" + p.Icon = DefaultIcon } if style, ok := m["style"].(string); ok { p.Stylesheet = style + } else { + p.Stylesheet = DefaultStylesheet } if header, ok := m["header"].(string); ok { p.Header = header + } else { + p.Header = DefaultHeader } if footer, ok := m["footer"].(string); ok { p.Footer = footer + } else { + p.Footer = DefaultFooter } return p } @@ -82,18 +88,25 @@ func ConvertFile(in string, out string) error { if err != nil { return err } - metadata["path"] = in - pd := buildPageData(metadata) - fmt.Println("Page title: ", pd.Title) - pd.Content = template.HTML(md) - - tmlp, err := template.New("webpage").Parse("placeholder") + pd := buildPageData(metadata, in) // build according to template here html, err := MdToHTML(md) if err != nil { return err } - err = WriteFile(html, out) + pd.Content = template.HTML(html) + + tmpl, err := template.New("webpage").Parse(DefaultTemplate) + if err != nil { + return err + } + + var output bytes.Buffer + if err := tmpl.Execute(&output, pd); err != nil { + return err + } + + err = WriteFile(output.Bytes(), out) return err } diff --git a/internal/build/config.go b/internal/build/config.go new file mode 100644 index 0000000..5618d93 --- /dev/null +++ b/internal/build/config.go @@ -0,0 +1,52 @@ +package build + +const ( + DefaultHeader = "" + DefaultFooter = "" + DefaultStylesheet = "/style/zonaDefault.css" + DefaultIcon = "" + DefaultTemplate = ` + + + {{ .Title }} + + + + + + +
+ +
+ {{ .Content }} + +
+
{{ .Footer }}
+
+ +` +) + +type Settings struct { + Header string + Footer string + Stylesheet string + Icon string +} + +func NewSettings(header string, footer string, style string, icon string) *Settings { + return &Settings{ + header, + footer, + style, + icon, + } +} diff --git a/internal/convert/convert.go b/internal/build/convert.go similarity index 70% rename from internal/convert/convert.go rename to internal/build/convert.go index 2cfdaf5..740e07c 100644 --- a/internal/convert/convert.go +++ b/internal/build/convert.go @@ -1,15 +1,12 @@ -package convert +package build import ( - "errors" "fmt" "io" - "io/fs" "os" "path/filepath" "strings" - "github.com/ficcdaf/zona/internal/util" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/html" @@ -137,48 +134,3 @@ func CopyFile(inPath string, outPath string) error { func ChangeExtension(in string, outExt string) string { return strings.TrimSuffix(in, filepath.Ext(in)) + outExt } - -func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) error { - if err != nil { - return err - } - if !entry.IsDir() { - ext := filepath.Ext(inPath) - outPath := util.ReplaceRoot(inPath, outRoot) - switch ext { - case ".md": - fmt.Println("Processing markdown...") - outPath = ChangeExtension(outPath, ".html") - if err := util.CreateParents(outPath); err != nil { - return err - } - if err := ConvertFile(inPath, outPath); err != nil { - return errors.Join(errors.New("Error processing file "+inPath), err) - } else { - return nil - } - // If it's not a file we need to process, - // we simply copy it to the destination path. - default: - if err := util.CreateParents(outPath); err != nil { - return err - } - if err := CopyFile(inPath, outPath); err != nil { - return errors.Join(errors.New("Error processing file "+inPath), err) - } else { - return nil - } - } - } - // fmt.Printf("Visited: %s\n", inPath) - return nil -} - -func Traverse(root string, outRoot string) error { - // err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { - walkFunc := func(path string, entry fs.DirEntry, err error) error { - return processFile(path, entry, err, outRoot) - } - err := filepath.WalkDir(root, walkFunc) - return err -} diff --git a/internal/convert/convert_test.go b/internal/build/convert_test.go similarity index 90% rename from internal/convert/convert_test.go rename to internal/build/convert_test.go index 68c1134..49a9691 100644 --- a/internal/convert/convert_test.go +++ b/internal/build/convert_test.go @@ -1,11 +1,11 @@ -package convert_test +package build_test import ( "os" "path/filepath" "testing" - "github.com/ficcdaf/zona/internal/convert" + "github.com/ficcdaf/zona/internal/build" "github.com/ficcdaf/zona/internal/util" ) @@ -13,7 +13,7 @@ func TestMdToHTML(t *testing.T) { md := []byte("# Hello World\n\nThis is a test.") expectedHTML := "

Hello World

\n

This is a test.

\n" nExpectedHTML := util.NormalizeContent(expectedHTML) - html, err := convert.MdToHTML(md) + html, err := build.MdToHTML(md) nHtml := util.NormalizeContent(string(html)) if err != nil { t.Fatalf("Expected no error, got %v", err) @@ -27,7 +27,7 @@ func TestWriteFile(t *testing.T) { path := filepath.Join(t.TempDir(), "test.txt") content := []byte("Hello, World!") - err := convert.WriteFile(content, path) + err := build.WriteFile(content, path) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -51,7 +51,7 @@ func TestReadFile(t *testing.T) { t.Fatalf("Error writing file: %v", err) } - data, err := convert.ReadFile(path) + data, err := build.ReadFile(path) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -70,7 +70,7 @@ func TestCopyFile(t *testing.T) { t.Fatalf("Error writing source file: %v", err) } - err = convert.CopyFile(src, dst) + err = build.CopyFile(src, dst) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -96,7 +96,7 @@ func TestConvertFile(t *testing.T) { t.Fatalf("Error writing source Markdown file: %v", err) } - err = convert.ConvertFile(src, dst) + err = build.ConvertFile(src, dst) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -113,7 +113,7 @@ func TestConvertFile(t *testing.T) { func TestChangeExtension(t *testing.T) { input := "test.md" - output := convert.ChangeExtension(input, ".html") + output := build.ChangeExtension(input, ".html") expected := "test.html" if output != expected { diff --git a/internal/build/traverse.go b/internal/build/traverse.go new file mode 100644 index 0000000..f0bc090 --- /dev/null +++ b/internal/build/traverse.go @@ -0,0 +1,55 @@ +package build + +import ( + "errors" + "fmt" + "io/fs" + "path/filepath" + + "github.com/ficcdaf/zona/internal/util" +) + +func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) error { + if err != nil { + return err + } + if !entry.IsDir() { + ext := filepath.Ext(inPath) + outPath := util.ReplaceRoot(inPath, outRoot) + switch ext { + case ".md": + fmt.Println("Processing markdown...") + outPath = ChangeExtension(outPath, ".html") + if err := util.CreateParents(outPath); err != nil { + return err + } + if err := ConvertFile(inPath, outPath); err != nil { + return errors.Join(errors.New("Error processing file "+inPath), err) + } else { + return nil + } + // If it's not a file we need to process, + // we simply copy it to the destination path. + default: + if err := util.CreateParents(outPath); err != nil { + return err + } + if err := CopyFile(inPath, outPath); err != nil { + return errors.Join(errors.New("Error processing file "+inPath), err) + } else { + return nil + } + } + } + // fmt.Printf("Visited: %s\n", inPath) + return nil +} + +func Traverse(root string, outRoot string) error { + // err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + walkFunc := func(path string, entry fs.DirEntry, err error) error { + return processFile(path, entry, err, outRoot) + } + err := filepath.WalkDir(root, walkFunc) + return err +} From ff1357c8daeca8a3b72c27d7767b9d4f7f97f8bf Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 25 Nov 2024 14:15:22 -0500 Subject: [PATCH 11/64] removed tree package --- internal/tree/node.go | 50 ------------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 internal/tree/node.go diff --git a/internal/tree/node.go b/internal/tree/node.go deleted file mode 100644 index 9025b3d..0000000 --- a/internal/tree/node.go +++ /dev/null @@ -1,50 +0,0 @@ -package tree - -// Node is a struct containing nodes of a file tree. -type Node struct { - // can be nil - Parent *Node - Name string - // Empty value mean directory - Ext string - // cannot be nil; may have len 0 - Children []*Node -} - -// NewNode constructs and returns a Node instance. -// parent and children are optional and can be nil, -// in which case Parent will be stored as nil, -// but Children will be initialized as []*Node of len 0. -// If ext == "", the Node is a directory. -func NewNode(name string, ext string, parent *Node, children []*Node) *Node { - var c []*Node - if children == nil { - c = make([]*Node, 0) - } else { - c = children - } - n := &Node{ - Name: name, - Ext: ext, - Parent: parent, - Children: c, - } - return n -} - -func (n *Node) IsRoot() bool { - return n.Parent == nil -} - -func (n *Node) IsTail() bool { - return len(n.Children) == 0 -} - -func (n *Node) IsDir() bool { - return n.Ext == "" -} - -// TODO: Implement recursive depth-first traversal to process a tree - -func Traverse(root *Node) { -} From 12ebba687bcf00e22b45833a34d2fe1246bb2ce3 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 25 Nov 2024 14:15:33 -0500 Subject: [PATCH 12/64] fixed title casing --- go.mod | 2 ++ internal/build/build_page.go | 11 ++--------- internal/build/convert.go | 4 ++-- internal/build/title.go | 30 ++++++++++++++++++++++++++++++ internal/build/traverse.go | 3 +-- runtest.sh | 2 +- test/this-article-has-a-title.md | 6 ++++++ 7 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 internal/build/title.go create mode 100644 test/this-article-has-a-title.md diff --git a/go.mod b/go.mod index 979a4ca..d37e636 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 gopkg.in/yaml.v3 v3.0.1 ) + +require golang.org/x/text v0.20.0 // indirect diff --git a/internal/build/build_page.go b/internal/build/build_page.go index bcb5a38..2337213 100644 --- a/internal/build/build_page.go +++ b/internal/build/build_page.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "html/template" - "path/filepath" "strings" "gopkg.in/yaml.v3" @@ -42,17 +41,10 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { return meta, []byte(split[2]), nil } -// this function converts a file path to its title form -func pathToTitle(path string) string { - stripped := ChangeExtension(filepath.Base(path), "") - replaced := strings.NewReplacer("-", " ", "_", " ", `\ `, " ").Replace(stripped) - return strings.ToTitle(replaced) -} - func buildPageData(m Metadata, path string) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { - p.Title = title + p.Title = wordsToTitle(title) } else { p.Title = pathToTitle(path) } @@ -89,6 +81,7 @@ func ConvertFile(in string, out string) error { return err } pd := buildPageData(metadata, in) + fmt.Println("Title: ", pd.Title) // build according to template here html, err := MdToHTML(md) diff --git a/internal/build/convert.go b/internal/build/convert.go index 740e07c..cd1602b 100644 --- a/internal/build/convert.go +++ b/internal/build/convert.go @@ -42,11 +42,11 @@ func PathIsValid(path string, requireFile bool) bool { } func processLink(p string) string { - fmt.Println("Processing link...") + // fmt.Println("Processing link...") ext := filepath.Ext(p) // Only process if it points to an existing, local markdown file if ext == ".md" && filepath.IsLocal(p) { - fmt.Println("Markdown link detected...") + // fmt.Println("Markdown link detected...") return ChangeExtension(p, ".html") } else { return p diff --git a/internal/build/title.go b/internal/build/title.go new file mode 100644 index 0000000..704e0b2 --- /dev/null +++ b/internal/build/title.go @@ -0,0 +1,30 @@ +package build + +import ( + "path/filepath" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// pathToWords takes a full path +// and strips separators and extension +// from the file name +func pathToWords(path string) string { + stripped := ChangeExtension(filepath.Base(path), "") + replaced := strings.NewReplacer("-", " ", "_", " ", `\ `, " ").Replace(stripped) + return strings.ToTitle(replaced) +} + +func wordsToTitle(words string) string { + caser := cases.Title(language.English) + return caser.String(words) +} + +// pathToTitle converts a full path to a string +// in title case +func pathToTitle(path string) string { + words := pathToWords(path) + return wordsToTitle(words) +} diff --git a/internal/build/traverse.go b/internal/build/traverse.go index f0bc090..379fe8b 100644 --- a/internal/build/traverse.go +++ b/internal/build/traverse.go @@ -2,7 +2,6 @@ package build import ( "errors" - "fmt" "io/fs" "path/filepath" @@ -18,7 +17,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er outPath := util.ReplaceRoot(inPath, outRoot) switch ext { case ".md": - fmt.Println("Processing markdown...") + // fmt.Println("Processing markdown...") outPath = ChangeExtension(outPath, ".html") if err := util.CreateParents(outPath); err != nil { return err diff --git a/runtest.sh b/runtest.sh index d20c26e..27611e4 100755 --- a/runtest.sh +++ b/runtest.sh @@ -5,4 +5,4 @@ fi go run cmd/zona/main.go test -bat foobar/in.html +# bat foobar/in.html diff --git a/test/this-article-has-a-title.md b/test/this-article-has-a-title.md new file mode 100644 index 0000000..578595f --- /dev/null +++ b/test/this-article-has-a-title.md @@ -0,0 +1,6 @@ +# My amazing markdown file! + +I can _even_ do **this**! + +- Or, I could... +- [Link](page.md) to this file From 4d1b18fd12a8a3d0d13c5d44cb14e3ea80be2549 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 25 Nov 2024 14:55:45 -0500 Subject: [PATCH 13/64] refactoring; began implementing embedding --- cmd/zona/config.go | 1 - cmd/zona/main.go | 5 ++-- internal/{build => builder}/build_page.go | 24 +++++++++-------- internal/{build => builder}/config.go | 29 ++++++++++++++++----- internal/{build => builder}/convert.go | 10 +++---- internal/{build => builder}/convert_test.go | 16 ++++++------ internal/builder/embed/article.html | 28 ++++++++++++++++++++ internal/builder/embed/config.yml | 0 internal/{build => builder}/traverse.go | 13 +++++---- internal/util/path.go | 4 +++ internal/{build => util}/title.go | 16 ++++++------ 11 files changed, 96 insertions(+), 50 deletions(-) delete mode 100644 cmd/zona/config.go rename internal/{build => builder}/build_page.go (76%) rename internal/{build => builder}/config.go (62%) rename internal/{build => builder}/convert.go (94%) rename internal/{build => builder}/convert_test.go (90%) create mode 100644 internal/builder/embed/article.html create mode 100644 internal/builder/embed/config.yml rename internal/{build => builder}/traverse.go (76%) rename internal/{build => util}/title.go (61%) diff --git a/cmd/zona/config.go b/cmd/zona/config.go deleted file mode 100644 index 06ab7d0..0000000 --- a/cmd/zona/config.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/cmd/zona/main.go b/cmd/zona/main.go index d925f7d..b5c4989 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -6,7 +6,7 @@ import ( "fmt" "os" - "github.com/ficcdaf/zona/internal/build" + "github.com/ficcdaf/zona/internal/builder" ) // // validateFile checks whether a given path @@ -41,7 +41,8 @@ func main() { } } - err := build.Traverse(*rootPath, "foobar") + settings := builder.GetSettings() + err := builder.Traverse(*rootPath, "foobar", settings) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } diff --git a/internal/build/build_page.go b/internal/builder/build_page.go similarity index 76% rename from internal/build/build_page.go rename to internal/builder/build_page.go index 2337213..35a601a 100644 --- a/internal/build/build_page.go +++ b/internal/builder/build_page.go @@ -1,4 +1,4 @@ -package build +package builder import ( "bytes" @@ -6,6 +6,7 @@ import ( "html/template" "strings" + "github.com/ficcdaf/zona/internal/util" "gopkg.in/yaml.v3" ) @@ -18,6 +19,7 @@ type PageData struct { NextPost string PrevPost string Footer string + Template string } type Metadata map[string]interface{} @@ -41,37 +43,37 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { return meta, []byte(split[2]), nil } -func buildPageData(m Metadata, path string) *PageData { +func buildPageData(m Metadata, path string, settings *Settings) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { - p.Title = wordsToTitle(title) + p.Title = util.WordsToTitle(title) } else { - p.Title = pathToTitle(path) + p.Title = util.PathToTitle(path) } if icon, ok := m["icon"].(string); ok { p.Icon = icon } else { - p.Icon = DefaultIcon + p.Icon = settings.Icon } if style, ok := m["style"].(string); ok { p.Stylesheet = style } else { - p.Stylesheet = DefaultStylesheet + p.Stylesheet = settings.Stylesheet } if header, ok := m["header"].(string); ok { p.Header = header } else { - p.Header = DefaultHeader + p.Header = settings.Header } if footer, ok := m["footer"].(string); ok { p.Footer = footer } else { - p.Footer = DefaultFooter + p.Footer = settings.Footer } return p } -func ConvertFile(in string, out string) error { +func ConvertFile(in string, out string, settings *Settings) error { mdPre, err := ReadFile(in) if err != nil { return err @@ -80,7 +82,7 @@ func ConvertFile(in string, out string) error { if err != nil { return err } - pd := buildPageData(metadata, in) + pd := buildPageData(metadata, in, settings) fmt.Println("Title: ", pd.Title) // build according to template here @@ -90,7 +92,7 @@ func ConvertFile(in string, out string) error { } pd.Content = template.HTML(html) - tmpl, err := template.New("webpage").Parse(DefaultTemplate) + tmpl, err := template.New("webpage").Parse(settings.DefaultTemplate) if err != nil { return err } diff --git a/internal/build/config.go b/internal/builder/config.go similarity index 62% rename from internal/build/config.go rename to internal/builder/config.go index 5618d93..3059230 100644 --- a/internal/build/config.go +++ b/internal/builder/config.go @@ -1,4 +1,8 @@ -package build +package builder + +import ( + "embed" +) const ( DefaultHeader = "" @@ -35,18 +39,31 @@ const ( ` ) +//go:embed embed +var embedDir embed.FS + type Settings struct { - Header string - Footer string - Stylesheet string - Icon string + Header string + Footer string + Stylesheet string + Icon string + DefaultTemplate string } -func NewSettings(header string, footer string, style string, icon string) *Settings { +func NewSettings(header string, footer string, style string, icon string, temp string) *Settings { return &Settings{ header, footer, style, icon, + temp, } } + +func GetSettings() *Settings { + // TODO: Read a config file to override defaults + // "Defaults" should be a default config file via embed package, + // so the settings func should need to handle one case: + // check if config file exists, if not, use embedded one + return NewSettings(DefaultHeader, DefaultFooter, DefaultStylesheet, DefaultIcon, DefaultTemplate) +} diff --git a/internal/build/convert.go b/internal/builder/convert.go similarity index 94% rename from internal/build/convert.go rename to internal/builder/convert.go index cd1602b..7c032b7 100644 --- a/internal/build/convert.go +++ b/internal/builder/convert.go @@ -1,12 +1,12 @@ -package build +package builder import ( "fmt" "io" "os" "path/filepath" - "strings" + "github.com/ficcdaf/zona/internal/util" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/html" @@ -47,7 +47,7 @@ func processLink(p string) string { // Only process if it points to an existing, local markdown file if ext == ".md" && filepath.IsLocal(p) { // fmt.Println("Markdown link detected...") - return ChangeExtension(p, ".html") + return util.ChangeExtension(p, ".html") } else { return p } @@ -130,7 +130,3 @@ func CopyFile(inPath string, outPath string) error { return nil } } - -func ChangeExtension(in string, outExt string) string { - return strings.TrimSuffix(in, filepath.Ext(in)) + outExt -} diff --git a/internal/build/convert_test.go b/internal/builder/convert_test.go similarity index 90% rename from internal/build/convert_test.go rename to internal/builder/convert_test.go index 49a9691..45c4843 100644 --- a/internal/build/convert_test.go +++ b/internal/builder/convert_test.go @@ -1,11 +1,11 @@ -package build_test +package builder_test import ( "os" "path/filepath" "testing" - "github.com/ficcdaf/zona/internal/build" + "github.com/ficcdaf/zona/internal/builder" "github.com/ficcdaf/zona/internal/util" ) @@ -13,7 +13,7 @@ func TestMdToHTML(t *testing.T) { md := []byte("# Hello World\n\nThis is a test.") expectedHTML := "

Hello World

\n

This is a test.

\n" nExpectedHTML := util.NormalizeContent(expectedHTML) - html, err := build.MdToHTML(md) + html, err := builder.MdToHTML(md) nHtml := util.NormalizeContent(string(html)) if err != nil { t.Fatalf("Expected no error, got %v", err) @@ -27,7 +27,7 @@ func TestWriteFile(t *testing.T) { path := filepath.Join(t.TempDir(), "test.txt") content := []byte("Hello, World!") - err := build.WriteFile(content, path) + err := builder.WriteFile(content, path) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -51,7 +51,7 @@ func TestReadFile(t *testing.T) { t.Fatalf("Error writing file: %v", err) } - data, err := build.ReadFile(path) + data, err := builder.ReadFile(path) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -70,7 +70,7 @@ func TestCopyFile(t *testing.T) { t.Fatalf("Error writing source file: %v", err) } - err = build.CopyFile(src, dst) + err = builder.CopyFile(src, dst) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -96,7 +96,7 @@ func TestConvertFile(t *testing.T) { t.Fatalf("Error writing source Markdown file: %v", err) } - err = build.ConvertFile(src, dst) + err = builder.ConvertFile(src, dst) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -113,7 +113,7 @@ func TestConvertFile(t *testing.T) { func TestChangeExtension(t *testing.T) { input := "test.md" - output := build.ChangeExtension(input, ".html") + output := builder.ChangeExtension(input, ".html") expected := "test.html" if output != expected { diff --git a/internal/builder/embed/article.html b/internal/builder/embed/article.html new file mode 100644 index 0000000..eb677d8 --- /dev/null +++ b/internal/builder/embed/article.html @@ -0,0 +1,28 @@ + + + + {{ .Title }} + + + + + + +
+ +
+ {{ .Content }} + +
+
{{ .Footer }}
+
+ + diff --git a/internal/builder/embed/config.yml b/internal/builder/embed/config.yml new file mode 100644 index 0000000..e69de29 diff --git a/internal/build/traverse.go b/internal/builder/traverse.go similarity index 76% rename from internal/build/traverse.go rename to internal/builder/traverse.go index 379fe8b..44f493f 100644 --- a/internal/build/traverse.go +++ b/internal/builder/traverse.go @@ -1,4 +1,4 @@ -package build +package builder import ( "errors" @@ -8,7 +8,7 @@ import ( "github.com/ficcdaf/zona/internal/util" ) -func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) error { +func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error { if err != nil { return err } @@ -18,11 +18,11 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er switch ext { case ".md": // fmt.Println("Processing markdown...") - outPath = ChangeExtension(outPath, ".html") + outPath = util.ChangeExtension(outPath, ".html") if err := util.CreateParents(outPath); err != nil { return err } - if err := ConvertFile(inPath, outPath); err != nil { + if err := ConvertFile(inPath, outPath, settings); err != nil { return errors.Join(errors.New("Error processing file "+inPath), err) } else { return nil @@ -44,10 +44,9 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string) er return nil } -func Traverse(root string, outRoot string) error { - // err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { +func Traverse(root string, outRoot string, settings *Settings) error { walkFunc := func(path string, entry fs.DirEntry, err error) error { - return processFile(path, entry, err, outRoot) + return processFile(path, entry, err, outRoot, settings) } err := filepath.WalkDir(root, walkFunc) return err diff --git a/internal/util/path.go b/internal/util/path.go index 7359afb..e53c9b7 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -18,6 +18,10 @@ func CheckExtension(path, ext string) error { } } +func ChangeExtension(in string, outExt string) string { + return strings.TrimSuffix(in, filepath.Ext(in)) + outExt +} + func getRoot(path string) string { for { parent := filepath.Dir(path) diff --git a/internal/build/title.go b/internal/util/title.go similarity index 61% rename from internal/build/title.go rename to internal/util/title.go index 704e0b2..4fef45d 100644 --- a/internal/build/title.go +++ b/internal/util/title.go @@ -1,4 +1,4 @@ -package build +package util import ( "path/filepath" @@ -8,23 +8,23 @@ import ( "golang.org/x/text/language" ) -// pathToWords takes a full path +// PathToWords takes a full path // and strips separators and extension // from the file name -func pathToWords(path string) string { +func PathToWords(path string) string { stripped := ChangeExtension(filepath.Base(path), "") replaced := strings.NewReplacer("-", " ", "_", " ", `\ `, " ").Replace(stripped) return strings.ToTitle(replaced) } -func wordsToTitle(words string) string { +func WordsToTitle(words string) string { caser := cases.Title(language.English) return caser.String(words) } -// pathToTitle converts a full path to a string +// PathToTitle converts a full path to a string // in title case -func pathToTitle(path string) string { - words := pathToWords(path) - return wordsToTitle(words) +func PathToTitle(path string) string { + words := PathToWords(path) + return WordsToTitle(words) } From c6c801e24859d113e6dfb201d7b5f77a646083e1 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 25 Nov 2024 16:05:35 -0500 Subject: [PATCH 14/64] continue working on config and default parsing --- cmd/zona/main.go | 2 +- internal/builder/build_page.go | 9 +- internal/builder/config.go | 148 +++++++++++++++++++++++---------- internal/builder/convert.go | 56 +------------ internal/builder/traverse.go | 2 +- internal/util/file.go | 58 +++++++++++++ internal/util/path.go | 7 ++ internal/util/util.go | 10 ++- 8 files changed, 183 insertions(+), 109 deletions(-) create mode 100644 internal/util/file.go diff --git a/cmd/zona/main.go b/cmd/zona/main.go index b5c4989..488cc5f 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -41,7 +41,7 @@ func main() { } } - settings := builder.GetSettings() + settings := builder.GetSettings(*rootPath) err := builder.Traverse(*rootPath, "foobar", settings) if err != nil { fmt.Printf("Error: %s\n", err.Error()) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 35a601a..39dade2 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -74,7 +74,7 @@ func buildPageData(m Metadata, path string, settings *Settings) *PageData { } func ConvertFile(in string, out string, settings *Settings) error { - mdPre, err := ReadFile(in) + mdPre, err := util.ReadFile(in) if err != nil { return err } @@ -86,10 +86,7 @@ func ConvertFile(in string, out string, settings *Settings) error { fmt.Println("Title: ", pd.Title) // build according to template here - html, err := MdToHTML(md) - if err != nil { - return err - } + html := MdToHTML(md) pd.Content = template.HTML(html) tmpl, err := template.New("webpage").Parse(settings.DefaultTemplate) @@ -102,6 +99,6 @@ func ConvertFile(in string, out string, settings *Settings) error { return err } - err = WriteFile(output.Bytes(), out) + err = util.WriteFile(output.Bytes(), out) return err } diff --git a/internal/builder/config.go b/internal/builder/config.go index 3059230..e12bf6f 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -2,68 +2,124 @@ package builder import ( "embed" + "html/template" + "log" + "path/filepath" + + "github.com/ficcdaf/zona/internal/util" + "gopkg.in/yaml.v3" ) const ( - DefaultHeader = "" - DefaultFooter = "" - DefaultStylesheet = "/style/zonaDefault.css" - DefaultIcon = "" - DefaultTemplate = ` - - - {{ .Title }} - - - - - - -
- -
- {{ .Content }} - -
-
{{ .Footer }}
-
- -` + DefConfigName = "config.yml" + DefHeaderName = "header.md" + DefFooterName = "footer.md" + DefStylesheetName = "style.css" + DefIconName = "icon.png" + DefTemplateName = "default.html" ) //go:embed embed var embedDir embed.FS type Settings struct { - Header string - Footer string - Stylesheet string - Icon string - DefaultTemplate string + Header template.HTML + HeaderName string + Footer template.HTML + FooterName string + Stylesheet []byte + StylesheetName string + Icon []byte + IconName string + DefaultTemplate template.HTML + DefaultTemplateName string } -func NewSettings(header string, footer string, style string, icon string, temp string) *Settings { - return &Settings{ - header, - footer, - style, - icon, - temp, +func buildSettings(f []byte) (*Settings, error) { + s := &Settings{} + var c map[string]interface{} + // Parse YAML + if err := yaml.Unmarshal(f, &c); err != nil { + return nil, err } + if headerName, ok := c["header"].(string); ok { + header, err := util.ReadFile(headerName) + s.HeaderName = headerName + if err != nil { + return nil, util.ErrorPrepend("Could not read header specified in config: ", err) + } + s.Header = template.HTML(MdToHTML(header)) + } else { + header := readEmbed(DefHeaderName) + s.Header = template.HTML(MdToHTML(header)) + s.HeaderName = DefHeaderName + } + if footerName, ok := c["footer"].(string); ok { + footer, err := util.ReadFile(footerName) + s.FooterName = footerName + if err != nil { + return nil, util.ErrorPrepend("Could not read footer specified in config: ", err) + } + s.Footer = template.HTML(MdToHTML(footer)) + } else { + footer := readEmbed(DefFooterName) + s.Footer = template.HTML(MdToHTML(footer)) + s.FooterName = DefFooterName + } + if stylesheetName, ok := c["stylesheet"].(string); ok { + stylesheet, err := util.ReadFile(stylesheetName) + if err != nil { + return nil, util.ErrorPrepend("Could not read stylesheet specified in config: ", err) + } + s.StylesheetName = stylesheetName + s.Stylesheet = stylesheet + } else { + stylesheet := readEmbed(DefStylesheetName) + s.Stylesheet = stylesheet + s.StylesheetName = DefStylesheetName + } + if iconName, ok := c["icon"].(string); ok { + icon, err := util.ReadFile(iconName) + if err != nil { + return nil, util.ErrorPrepend("Could not read icon specified in config: ", err) + } + s.Icon = icon + s.IconName = iconName + } else { + icon := readEmbed(DefIconName) + s.Icon = icon + s.IconName = DefIconName + } + if + + return s, nil } -func GetSettings() *Settings { +// readEmbed reads a file inside the embedded dir +func readEmbed(name string) []byte { + f, err := embedDir.ReadFile(name) + if err != nil { + log.Fatalln("Fatal internal error: Could not read embedded default config!") + } + return f +} + +func GetSettings(root string) *Settings { // TODO: Read a config file to override defaults // "Defaults" should be a default config file via embed package, // so the settings func should need to handle one case: // check if config file exists, if not, use embedded one - return NewSettings(DefaultHeader, DefaultFooter, DefaultStylesheet, DefaultIcon, DefaultTemplate) + var config []byte + configPath := filepath.Join(root, DefConfigName) + if !util.FileExists(configPath) { + // Config file does not exist, we used embedded default + config = readEmbed(configPath) + } else { + config, err := util.ReadFile(configPath) + if err != nil { + log.Fatalln("Fatal internal error: Config file exists but could not be read!") + } + } + + // return NewSettings(DefaultHeader, DefaultFooter, DefaultStylesheet, DefaultIcon, DefaultTemplate) } diff --git a/internal/builder/convert.go b/internal/builder/convert.go index 7c032b7..ed72fbd 100644 --- a/internal/builder/convert.go +++ b/internal/builder/convert.go @@ -14,7 +14,7 @@ import ( ) // This function takes a Markdown document and returns an HTML document. -func MdToHTML(md []byte) ([]byte, error) { +func MdToHTML(md []byte) []byte { // create parser with extensions extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock p := parser.NewWithExtensions(extensions) @@ -25,7 +25,7 @@ func MdToHTML(md []byte) ([]byte, error) { opts := html.RendererOptions{Flags: htmlFlags} renderer := newZonaRenderer(opts) - return markdown.Render(doc, renderer), nil + return markdown.Render(doc, renderer) } // PathIsValid checks if a path is valid. @@ -78,55 +78,3 @@ func newZonaRenderer(opts html.RendererOptions) *html.Renderer { opts.RenderNodeHook = htmlRenderHook return html.NewRenderer(opts) } - -// WriteFile writes a given byte array to the given path. -func WriteFile(b []byte, p string) error { - f, err := os.Create(p) - if err != nil { - return err - } - _, err = f.Write(b) - defer f.Close() - if err != nil { - return err - } - return nil -} - -// ReadFile reads a byte array from a given path. -func ReadFile(p string) ([]byte, error) { - f, err := os.Open(p) - if err != nil { - return nil, err - } - var result []byte - buf := make([]byte, 1024) - for { - n, err := f.Read(buf) - // check for a non EOF error - if err != nil && err != io.EOF { - return nil, err - } - // n==0 when there are no chunks left to read - if n == 0 { - defer f.Close() - break - } - result = append(result, buf[:n]...) - } - return result, nil -} - -// CopyFile reads the file at the input path, and write -// it to the output path. -func CopyFile(inPath string, outPath string) error { - inB, err := ReadFile(inPath) - if err != nil { - return err - } - if err := WriteFile(inB, outPath); err != nil { - return err - } else { - return nil - } -} diff --git a/internal/builder/traverse.go b/internal/builder/traverse.go index 44f493f..5272dca 100644 --- a/internal/builder/traverse.go +++ b/internal/builder/traverse.go @@ -33,7 +33,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se if err := util.CreateParents(outPath); err != nil { return err } - if err := CopyFile(inPath, outPath); err != nil { + if err := util.CopyFile(inPath, outPath); err != nil { return errors.Join(errors.New("Error processing file "+inPath), err) } else { return nil diff --git a/internal/util/file.go b/internal/util/file.go new file mode 100644 index 0000000..2028018 --- /dev/null +++ b/internal/util/file.go @@ -0,0 +1,58 @@ +package util + +import ( + "io" + "os" +) + +// WriteFile writes a given byte array to the given path. +func WriteFile(b []byte, p string) error { + f, err := os.Create(p) + if err != nil { + return err + } + _, err = f.Write(b) + defer f.Close() + if err != nil { + return err + } + return nil +} + +// ReadFile reads a byte array from a given path. +func ReadFile(p string) ([]byte, error) { + f, err := os.Open(p) + if err != nil { + return nil, err + } + var result []byte + buf := make([]byte, 1024) + for { + n, err := f.Read(buf) + // check for a non EOF error + if err != nil && err != io.EOF { + return nil, err + } + // n==0 when there are no chunks left to read + if n == 0 { + defer f.Close() + break + } + result = append(result, buf[:n]...) + } + return result, nil +} + +// CopyFile reads the file at the input path, and write +// it to the output path. +func CopyFile(inPath string, outPath string) error { + inB, err := ReadFile(inPath) + if err != nil { + return err + } + if err := WriteFile(inB, outPath); err != nil { + return err + } else { + return nil + } +} diff --git a/internal/util/path.go b/internal/util/path.go index e53c9b7..145bc0e 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -40,6 +40,13 @@ func ReplaceRoot(inPath, outRoot string) string { return outPath } +// FileExists returns a boolean indicating +// whether something exists at the path. +func FileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + func CreateParents(path string) error { dir := filepath.Dir(path) // Check if the parent directory already exists diff --git a/internal/util/util.go b/internal/util/util.go index 2894192..7dc11dd 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,6 +1,9 @@ package util -import "strings" +import ( + "errors" + "strings" +) func NormalizeContent(content string) string { var normalized []string @@ -13,3 +16,8 @@ func NormalizeContent(content string) string { } return strings.Join(normalized, "\n") } + +// ErrorPrepend returns a new error with a message prepended to the given error. +func ErrorPrepend(m string, err error) error { + return errors.Join(errors.New(m), err) +} From 878cb3d3a80ffd4bd84328cd4e77812d6057901f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 25 Nov 2024 16:13:10 -0500 Subject: [PATCH 15/64] finished config parser (untested) refactored some config parser types --- internal/builder/build_page.go | 8 +++--- internal/builder/config.go | 49 ++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 39dade2..4ad28b2 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -53,22 +53,22 @@ func buildPageData(m Metadata, path string, settings *Settings) *PageData { if icon, ok := m["icon"].(string); ok { p.Icon = icon } else { - p.Icon = settings.Icon + p.Icon = settings.IconName } if style, ok := m["style"].(string); ok { p.Stylesheet = style } else { - p.Stylesheet = settings.Stylesheet + p.Stylesheet = settings.StylesheetName } if header, ok := m["header"].(string); ok { p.Header = header } else { - p.Header = settings.Header + p.Header = settings.HeaderName } if footer, ok := m["footer"].(string); ok { p.Footer = footer } else { - p.Footer = settings.Footer + p.Footer = settings.FooterName } return p } diff --git a/internal/builder/config.go b/internal/builder/config.go index e12bf6f..ad2aef5 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -11,12 +11,13 @@ import ( ) const ( - DefConfigName = "config.yml" - DefHeaderName = "header.md" - DefFooterName = "footer.md" - DefStylesheetName = "style.css" - DefIconName = "icon.png" - DefTemplateName = "default.html" + DefConfigName = "config.yml" + DefHeaderName = "header.md" + DefFooterName = "footer.md" + DefStylesheetName = "style.css" + DefIconName = "icon.png" + DefTemplateName = "default.html" + ArticleTemplateName = "article.html" ) //go:embed embed @@ -27,12 +28,13 @@ type Settings struct { HeaderName string Footer template.HTML FooterName string - Stylesheet []byte StylesheetName string - Icon []byte IconName string - DefaultTemplate template.HTML + DefaultTemplate string DefaultTemplateName string + ArticleTemplate string + Stylesheet []byte + Icon []byte } func buildSettings(f []byte) (*Settings, error) { @@ -90,7 +92,20 @@ func buildSettings(f []byte) (*Settings, error) { s.Icon = icon s.IconName = DefIconName } - if + if templateName, ok := c["template"].(string); ok { + temp, err := util.ReadFile(templateName) + if err != nil { + return nil, util.ErrorPrepend("Could not read template specified in config: ", err) + } + s.DefaultTemplate = string(temp) + s.DefaultTemplateName = templateName + } else { + temp := readEmbed(DefTemplateName) + s.DefaultTemplate = string(temp) + s.DefaultTemplateName = DefTemplateName + } + artTemp := readEmbed(ArticleTemplateName) + s.ArticleTemplate = string(artTemp) return s, nil } @@ -105,21 +120,21 @@ func readEmbed(name string) []byte { } func GetSettings(root string) *Settings { - // TODO: Read a config file to override defaults - // "Defaults" should be a default config file via embed package, - // so the settings func should need to handle one case: - // check if config file exists, if not, use embedded one var config []byte configPath := filepath.Join(root, DefConfigName) if !util.FileExists(configPath) { // Config file does not exist, we used embedded default config = readEmbed(configPath) } else { - config, err := util.ReadFile(configPath) + var err error + config, err = util.ReadFile(configPath) if err != nil { log.Fatalln("Fatal internal error: Config file exists but could not be read!") } } - - // return NewSettings(DefaultHeader, DefaultFooter, DefaultStylesheet, DefaultIcon, DefaultTemplate) + s, err := buildSettings(config) + if err != nil { + log.Fatalf("Fatal error: could not parse config: %u\n", err) + } + return s } From bfe7ddffd464c37e3450abf72d9aa5249431e300 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 28 Nov 2024 18:03:00 -0500 Subject: [PATCH 16/64] fixed settings parsing & embed --- internal/builder/build_page.go | 19 ++-- internal/builder/config.go | 123 ++++++++++++----------- internal/builder/embed/config.yml | 1 + internal/builder/embed/default.html | 28 ++++++ internal/builder/embed/favicon.png | Bin 0 -> 2543 bytes internal/builder/embed/footer.md | 1 + internal/builder/embed/header.md | 1 + internal/builder/embed/style.css | 148 ++++++++++++++++++++++++++++ runtest.sh | 2 +- 9 files changed, 257 insertions(+), 66 deletions(-) create mode 100644 internal/builder/embed/default.html create mode 100644 internal/builder/embed/favicon.png create mode 100644 internal/builder/embed/footer.md create mode 100644 internal/builder/embed/header.md create mode 100644 internal/builder/embed/style.css diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 4ad28b2..f0fb5cd 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -14,11 +14,13 @@ type PageData struct { Title string Icon string Stylesheet string - Header string + HeaderName string + Header template.HTML Content template.HTML NextPost string PrevPost string - Footer string + FooterName string + Footer template.HTML Template string } @@ -61,14 +63,19 @@ func buildPageData(m Metadata, path string, settings *Settings) *PageData { p.Stylesheet = settings.StylesheetName } if header, ok := m["header"].(string); ok { - p.Header = header + p.HeaderName = header + // for now we use default anyways + p.Header = settings.Header } else { - p.Header = settings.HeaderName + p.HeaderName = settings.HeaderName + p.Header = settings.Header } if footer, ok := m["footer"].(string); ok { - p.Footer = footer + p.FooterName = footer + p.Footer = settings.Footer } else { - p.Footer = settings.FooterName + p.FooterName = settings.FooterName + p.Footer = settings.Footer } return p } diff --git a/internal/builder/config.go b/internal/builder/config.go index ad2aef5..d291591 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -20,7 +20,23 @@ const ( ArticleTemplateName = "article.html" ) -//go:embed embed +var defaultNames = map[string]string{ + "config": "config.yml", + "header": "header.md", + "footer": "footer.md", + "style": "style.css", + "icon": "favicon.png", + "article": "article.html", + "template": "default.html", +} + +//go:embed embed/article.html +//go:embed embed/config.yml +//go:embed embed/default.html +//go:embed embed/favicon.png +//go:embed embed/footer.md +//go:embed embed/header.md +//go:embed embed/style.css var embedDir embed.FS type Settings struct { @@ -37,6 +53,23 @@ type Settings struct { Icon []byte } +// processSetting checks the user's configuration for +// each option. If set, reads the specified file. If not, +// default option is used. +func processSetting(c map[string]interface{}, s string) (string, []byte, error) { + if name, ok := c[s].(string); ok { + val, err := util.ReadFile(name) + if err != nil { + return "", nil, util.ErrorPrepend("Could not read "+s+" specified in config: ", err) + } + return name, val, nil + } else { + val := readEmbed(defaultNames[s]) + return defaultNames[s], val, nil + } +} + +// buildSettings constructs the Settings struct. func buildSettings(f []byte) (*Settings, error) { s := &Settings{} var c map[string]interface{} @@ -44,66 +77,37 @@ func buildSettings(f []byte) (*Settings, error) { if err := yaml.Unmarshal(f, &c); err != nil { return nil, err } - if headerName, ok := c["header"].(string); ok { - header, err := util.ReadFile(headerName) - s.HeaderName = headerName - if err != nil { - return nil, util.ErrorPrepend("Could not read header specified in config: ", err) - } - s.Header = template.HTML(MdToHTML(header)) - } else { - header := readEmbed(DefHeaderName) - s.Header = template.HTML(MdToHTML(header)) - s.HeaderName = DefHeaderName + n, v, err := processSetting(c, "header") + if err != nil { + return nil, err } - if footerName, ok := c["footer"].(string); ok { - footer, err := util.ReadFile(footerName) - s.FooterName = footerName - if err != nil { - return nil, util.ErrorPrepend("Could not read footer specified in config: ", err) - } - s.Footer = template.HTML(MdToHTML(footer)) - } else { - footer := readEmbed(DefFooterName) - s.Footer = template.HTML(MdToHTML(footer)) - s.FooterName = DefFooterName + s.HeaderName = n + s.Header = template.HTML(MdToHTML(v)) + n, v, err = processSetting(c, "footer") + if err != nil { + return nil, err } - if stylesheetName, ok := c["stylesheet"].(string); ok { - stylesheet, err := util.ReadFile(stylesheetName) - if err != nil { - return nil, util.ErrorPrepend("Could not read stylesheet specified in config: ", err) - } - s.StylesheetName = stylesheetName - s.Stylesheet = stylesheet - } else { - stylesheet := readEmbed(DefStylesheetName) - s.Stylesheet = stylesheet - s.StylesheetName = DefStylesheetName + s.FooterName = n + s.Footer = template.HTML(MdToHTML(v)) + n, v, err = processSetting(c, "style") + if err != nil { + return nil, err } - if iconName, ok := c["icon"].(string); ok { - icon, err := util.ReadFile(iconName) - if err != nil { - return nil, util.ErrorPrepend("Could not read icon specified in config: ", err) - } - s.Icon = icon - s.IconName = iconName - } else { - icon := readEmbed(DefIconName) - s.Icon = icon - s.IconName = DefIconName + s.StylesheetName = n + s.Stylesheet = MdToHTML(v) + + n, v, err = processSetting(c, "icon") + if err != nil { + return nil, err } - if templateName, ok := c["template"].(string); ok { - temp, err := util.ReadFile(templateName) - if err != nil { - return nil, util.ErrorPrepend("Could not read template specified in config: ", err) - } - s.DefaultTemplate = string(temp) - s.DefaultTemplateName = templateName - } else { - temp := readEmbed(DefTemplateName) - s.DefaultTemplate = string(temp) - s.DefaultTemplateName = DefTemplateName + s.IconName = n + s.Icon = v + n, v, err = processSetting(c, "template") + if err != nil { + return nil, err } + s.DefaultTemplateName = n + s.DefaultTemplate = string(v) artTemp := readEmbed(ArticleTemplateName) s.ArticleTemplate = string(artTemp) @@ -112,9 +116,10 @@ func buildSettings(f []byte) (*Settings, error) { // readEmbed reads a file inside the embedded dir func readEmbed(name string) []byte { - f, err := embedDir.ReadFile(name) + f, err := embedDir.ReadFile("embed/" + name) if err != nil { - log.Fatalln("Fatal internal error: Could not read embedded default config!") + // panic(0) + log.Fatalf("Fatal internal error: Could not read embedded default %s! %u", name, err) } return f } @@ -124,7 +129,7 @@ func GetSettings(root string) *Settings { configPath := filepath.Join(root, DefConfigName) if !util.FileExists(configPath) { // Config file does not exist, we used embedded default - config = readEmbed(configPath) + config = readEmbed(defaultNames["config"]) } else { var err error config, err = util.ReadFile(configPath) diff --git a/internal/builder/embed/config.yml b/internal/builder/embed/config.yml index e69de29..cd304c3 100644 --- a/internal/builder/embed/config.yml +++ b/internal/builder/embed/config.yml @@ -0,0 +1 @@ +title: Something diff --git a/internal/builder/embed/default.html b/internal/builder/embed/default.html new file mode 100644 index 0000000..eb677d8 --- /dev/null +++ b/internal/builder/embed/default.html @@ -0,0 +1,28 @@ + + + + {{ .Title }} + + + + + + +
+ +
+ {{ .Content }} + +
+
{{ .Footer }}
+
+ + diff --git a/internal/builder/embed/favicon.png b/internal/builder/embed/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d92131f9e406d2c635eb15fa22a098eaf5dfd73d GIT binary patch literal 2543 zcmeAS@N?(olHy`uVBq!ia0y~yV9Ekv4rT@hhBZ8Md>I%R4+Z#yxVjhkFwh>%o;~Zr z`Ev{m4EFZ+iJP8qb90-Tnp#;|sj8~-^77W!)FdV*EL*lzP*708+9x%BZm*!*w{EZIUYE$-^|RcqP%?3qJ`(rofTCx(9+Tp5D-XCO7imZ zI(T4zS7)cFsHm*0Y)($LzP^4&M*5uDvz9MgW^QJttE;>+Q`*Pd6|y(9_dXQc@BV6MO9M+rYrUpi~m% z7tA15!Fc9CtWqdb?#!Pe*Z;mP|0wJ{>5KU04er5@WF#aH9d6dtZed_x-{tAz7*cWT z?cMOnBB3(v57pLrpOX~5y;zT_dxb`h%5$9+8d?f73>ls=q%Gpyr>`m^u48;V_3g~K z|7SkOeZTr=-mI7FURec;Oj7Zr8k(~CrYzT$OXn7xvwEgi8MAQK_JTl%jK|0P3}X+7 z`S7~uZn~OemRo0#IXOpcE9b^{H;$b#ioBuLw#e*qLZP&L*Da%F)^`&P(w5?M|li(z(xUE;eUQUv4}UvLOI zK`Dqbgz8xkO|!r*Z+k0~^^)sRCH>H}Cpm=&

9Z`H)(>+(c zVac*wu~v2ECUK-FMMSRi+OAvO{#&YFBa6YKo?5J}>@BypUVVEn^2)1)YRlagfkF>j zB0%CRz4Gph$CazRSEMfdX6q&T_Px6<_tx;ZrpzC8 z+JUE?*527Nec8jRa)zxwS58f@uE@IQ9`1dmC{X*p#jEbMp6zvd-d8>aYM=gRzdv^6 z!rFi5R-N~{@+s(YJr7c>Axb6eNgg7PKMqmC2w@53+CpgS%x_({e|TK#GTf{qwyksF3em|rVp}=qZd@TcIah3R z=I3Tl?PI-jleRVJ&WJj)I{BT@>7!Zq4!OEsNGsO}@V4frUJa?@dFq<(zfhM3%Utbc PpjMlwtDnm{r-UW|{6Pd? literal 0 HcmV?d00001 diff --git a/internal/builder/embed/footer.md b/internal/builder/embed/footer.md new file mode 100644 index 0000000..b986fa2 --- /dev/null +++ b/internal/builder/embed/footer.md @@ -0,0 +1 @@ +# Footer File diff --git a/internal/builder/embed/header.md b/internal/builder/embed/header.md new file mode 100644 index 0000000..4b88196 --- /dev/null +++ b/internal/builder/embed/header.md @@ -0,0 +1 @@ +# Header File diff --git a/internal/builder/embed/style.css b/internal/builder/embed/style.css new file mode 100644 index 0000000..7199c84 --- /dev/null +++ b/internal/builder/embed/style.css @@ -0,0 +1,148 @@ +:root { + --main-text-color: white; + --main-bg-color: #1b1b1b; + --main-transparent: rgba(255, 255, 255, 0.15); + --main-small-text-color: rgba(255, 255, 255, 0.45); +} + +body { + line-height: 1.6; + font-size: 18px; + font-family: sans-serif; + background: var(--main-bg-color); + color: var(--main-text-color); + padding-left: calc(100vw - 100%); +} +h1 { + margin-block-start: 0.67rem; + margin-block-end: 0.67rem; + font-size: 2rem; + font-weight: bold; +} +article h1:first-of-type { + margin-block-start: 1.67rem; +} +h2 { + margin-block-start: 0.83rem; + margin-block-end: 0.83rem; + font-size: 1.5rem; + font-weight: bold; +} +h3 { + margin-block-start: 1rem; + margin-block-end: 1rem; + font-size: 1.17em; + font-weight: bold; +} +h4 { + margin-block-start: 1.33rem; + margin-block-end: 1.33rem; + font-size: 1rem; + font-weight: bold; +} +article h1 + h4:first-of-type { + margin-block-start: 0rem; +} +h5 { + margin-block-start: 1.67rem; + margin-block-end: 1.67rem; + font-size: 0.83rem; + font-weight: bold; +} +h6 { + margin-block-start: 2.33rem; + margin-block-end: 2.33rem; + font-size: 0.67rem; + font-weight: bold; +} +a { + color: var(--main-text-color); +} +a:hover { + background: var(--main-transparent); +} +img { + width: auto; + height: auto; +} +blockquote { + color: var(--main-small-text-color); + border-left: 3px solid var(--main-transparent); + padding: 0 1rem; + margin-left: 0; + margin-right: 0; +} +hr { + border: none; + height: 1px; + background: var(--main-small-text-color); +} +code { + background: var(--main-transparent); + border-radius: 0.1875rem; + /* padding: .0625rem .1875rem; */ + /* margin: 0 .1875rem; */ +} +code, +pre { + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; +} +small { + font-size: 0.95rem; + color: var(--main-small-text-color); +} +small a { + color: inherit; /* Inherit the color of the surrounding text */ + text-decoration: underline; /* Optional: Keep the underline to indicate a link */ +} +.image-container { + text-align: center; + margin: 20px 0; /* Optional: add some spacing around the image container */ +} + +.image-container img { + /* max-width: 308px; */ + max-height: 308px; +} + +.image-container small { + display: block; /* Ensure the caption is on a new line */ + margin-top: 5px; /* Optional: adjust spacing between image and caption */ +} + +.image-container small a { + color: inherit; /* Ensure the link color matches the small text */ + text-decoration: underline; /* Optional: underline to indicate a link */ +} +#header ul { + list-style-type: none; + padding-left: 0; +} +#header li { + display: inline; + font-size: 1.2rem; + margin-right: 1.2rem; +} +#container { + margin: 2.5rem auto; + width: 90%; + max-width: 60ch; +} +#postlistdiv ul { + list-style-type: none; + padding-left: 0; +} +.moreposts { + font-size: 0.95rem; + padding-left: 0.5rem; +} +#nextprev { + text-align: center; + margin-top: 1.4rem; + font-size: 0.95rem; +} +#footer { + color: var(--main-small-text-color); +} diff --git a/runtest.sh b/runtest.sh index 27611e4..d20c26e 100755 --- a/runtest.sh +++ b/runtest.sh @@ -5,4 +5,4 @@ fi go run cmd/zona/main.go test -# bat foobar/in.html +bat foobar/in.html From c65ebfc8096d2a193cc976120832495fc75aeb26 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 28 Nov 2024 18:15:12 -0500 Subject: [PATCH 17/64] fixed article template override --- internal/builder/build_page.go | 7 ++++++- internal/builder/config.go | 14 ++------------ internal/builder/embed/default.html | 6 ------ test/in.md | 4 ++++ 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index f0fb5cd..c78f39e 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -77,6 +77,11 @@ func buildPageData(m Metadata, path string, settings *Settings) *PageData { p.FooterName = settings.FooterName p.Footer = settings.Footer } + if t, ok := m["type"].(string); ok && t == "article" || t == "post" { + p.Template = (settings.ArticleTemplate) + } else { + p.Template = (settings.DefaultTemplate) + } return p } @@ -96,7 +101,7 @@ func ConvertFile(in string, out string, settings *Settings) error { html := MdToHTML(md) pd.Content = template.HTML(html) - tmpl, err := template.New("webpage").Parse(settings.DefaultTemplate) + tmpl, err := template.New("webpage").Parse(pd.Template) if err != nil { return err } diff --git a/internal/builder/config.go b/internal/builder/config.go index d291591..6809c36 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -10,16 +10,6 @@ import ( "gopkg.in/yaml.v3" ) -const ( - DefConfigName = "config.yml" - DefHeaderName = "header.md" - DefFooterName = "footer.md" - DefStylesheetName = "style.css" - DefIconName = "icon.png" - DefTemplateName = "default.html" - ArticleTemplateName = "article.html" -) - var defaultNames = map[string]string{ "config": "config.yml", "header": "header.md", @@ -108,7 +98,7 @@ func buildSettings(f []byte) (*Settings, error) { } s.DefaultTemplateName = n s.DefaultTemplate = string(v) - artTemp := readEmbed(ArticleTemplateName) + artTemp := readEmbed(string(defaultNames["article"])) s.ArticleTemplate = string(artTemp) return s, nil @@ -126,7 +116,7 @@ func readEmbed(name string) []byte { func GetSettings(root string) *Settings { var config []byte - configPath := filepath.Join(root, DefConfigName) + configPath := filepath.Join(root, defaultNames["config"]) if !util.FileExists(configPath) { // Config file does not exist, we used embedded default config = readEmbed(defaultNames["config"]) diff --git a/internal/builder/embed/default.html b/internal/builder/embed/default.html index eb677d8..2dfb1a8 100644 --- a/internal/builder/embed/default.html +++ b/internal/builder/embed/default.html @@ -15,13 +15,7 @@

-
{{ .Content }} - -
{{ .Footer }}
diff --git a/test/in.md b/test/in.md index 578595f..2fd2350 100644 --- a/test/in.md +++ b/test/in.md @@ -1,3 +1,7 @@ +--- +type: article +--- + # My amazing markdown file! I can _even_ do **this**! From 065c344c036414dcf908738527b83d9116f8c8cd Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 28 Nov 2024 18:44:47 -0500 Subject: [PATCH 18/64] fixed stylesheet embedding --- cmd/zona/main.go | 2 +- internal/builder/build_page.go | 2 +- internal/builder/config.go | 44 +++++++--- internal/util/path.go | 10 +++ test/style/style.css | 148 +++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 test/style/style.css diff --git a/cmd/zona/main.go b/cmd/zona/main.go index 488cc5f..d61da6c 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -41,7 +41,7 @@ func main() { } } - settings := builder.GetSettings(*rootPath) + settings := builder.GetSettings(*rootPath, "foobar") err := builder.Traverse(*rootPath, "foobar", settings) if err != nil { fmt.Printf("Error: %s\n", err.Error()) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index c78f39e..c5fac75 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -60,7 +60,7 @@ func buildPageData(m Metadata, path string, settings *Settings) *PageData { if style, ok := m["style"].(string); ok { p.Stylesheet = style } else { - p.Stylesheet = settings.StylesheetName + p.Stylesheet = settings.StylePath } if header, ok := m["header"].(string); ok { p.HeaderName = header diff --git a/internal/builder/config.go b/internal/builder/config.go index 6809c36..de88646 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -11,13 +11,14 @@ import ( ) var defaultNames = map[string]string{ - "config": "config.yml", - "header": "header.md", - "footer": "footer.md", - "style": "style.css", - "icon": "favicon.png", - "article": "article.html", - "template": "default.html", + "config": "config.yml", + "header": "header.md", + "footer": "footer.md", + "style": "style.css", + "stylePath": filepath.Join("style", "style.css"), + "icon": "favicon.png", + "article": "article.html", + "template": "default.html", } //go:embed embed/article.html @@ -40,9 +41,12 @@ type Settings struct { DefaultTemplateName string ArticleTemplate string Stylesheet []byte + StylePath string Icon []byte } +var isDefaultStyle bool + // processSetting checks the user's configuration for // each option. If set, reads the specified file. If not, // default option is used. @@ -55,12 +59,13 @@ func processSetting(c map[string]interface{}, s string) (string, []byte, error) return name, val, nil } else { val := readEmbed(defaultNames[s]) + isDefaultStyle = true return defaultNames[s], val, nil } } // buildSettings constructs the Settings struct. -func buildSettings(f []byte) (*Settings, error) { +func buildSettings(f []byte, outRoot string) (*Settings, error) { s := &Settings{} var c map[string]interface{} // Parse YAML @@ -79,12 +84,27 @@ func buildSettings(f []byte) (*Settings, error) { } s.FooterName = n s.Footer = template.HTML(MdToHTML(v)) + isDefaultStyle = false n, v, err = processSetting(c, "style") if err != nil { return nil, err } s.StylesheetName = n - s.Stylesheet = MdToHTML(v) + s.Stylesheet = v + + if isDefaultStyle { + stylePath := filepath.Join(outRoot, defaultNames["stylePath"]) + // We convert the stylesheet path to its website root dir format and store it + s.StylePath = "/" + util.StripTopDir(stylePath) + err := util.CreateParents(stylePath) + if err != nil { + return nil, util.ErrorPrepend("Could not create default stylesheet directory: ", err) + } + err = util.WriteFile(s.Stylesheet, stylePath) + if err != nil { + return nil, util.ErrorPrepend("Could not create default stylesheet: ", err) + } + } n, v, err = processSetting(c, "icon") if err != nil { @@ -114,7 +134,7 @@ func readEmbed(name string) []byte { return f } -func GetSettings(root string) *Settings { +func GetSettings(root string, outRoot string) *Settings { var config []byte configPath := filepath.Join(root, defaultNames["config"]) if !util.FileExists(configPath) { @@ -122,12 +142,12 @@ func GetSettings(root string) *Settings { config = readEmbed(defaultNames["config"]) } else { var err error - config, err = util.ReadFile(configPath) + config, err = util.ReadFile(filepath.Join(root, configPath)) if err != nil { log.Fatalln("Fatal internal error: Config file exists but could not be read!") } } - s, err := buildSettings(config) + s, err := buildSettings(config, outRoot) if err != nil { log.Fatalf("Fatal error: could not parse config: %u\n", err) } diff --git a/internal/util/path.go b/internal/util/path.go index 145bc0e..12fa15e 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -47,6 +47,7 @@ func FileExists(path string) bool { return !os.IsNotExist(err) } +// CreateParents creates the parent directories required for a given path func CreateParents(path string) error { dir := filepath.Dir(path) // Check if the parent directory already exists @@ -59,3 +60,12 @@ func CreateParents(path string) error { } return nil } + +func StripTopDir(path string) string { + cleanedPath := filepath.Clean(path) + components := strings.Split(cleanedPath, string(filepath.Separator)) + if len(components) <= 1 { + return path + } + return filepath.Join(components[1:]...) +} diff --git a/test/style/style.css b/test/style/style.css new file mode 100644 index 0000000..7199c84 --- /dev/null +++ b/test/style/style.css @@ -0,0 +1,148 @@ +:root { + --main-text-color: white; + --main-bg-color: #1b1b1b; + --main-transparent: rgba(255, 255, 255, 0.15); + --main-small-text-color: rgba(255, 255, 255, 0.45); +} + +body { + line-height: 1.6; + font-size: 18px; + font-family: sans-serif; + background: var(--main-bg-color); + color: var(--main-text-color); + padding-left: calc(100vw - 100%); +} +h1 { + margin-block-start: 0.67rem; + margin-block-end: 0.67rem; + font-size: 2rem; + font-weight: bold; +} +article h1:first-of-type { + margin-block-start: 1.67rem; +} +h2 { + margin-block-start: 0.83rem; + margin-block-end: 0.83rem; + font-size: 1.5rem; + font-weight: bold; +} +h3 { + margin-block-start: 1rem; + margin-block-end: 1rem; + font-size: 1.17em; + font-weight: bold; +} +h4 { + margin-block-start: 1.33rem; + margin-block-end: 1.33rem; + font-size: 1rem; + font-weight: bold; +} +article h1 + h4:first-of-type { + margin-block-start: 0rem; +} +h5 { + margin-block-start: 1.67rem; + margin-block-end: 1.67rem; + font-size: 0.83rem; + font-weight: bold; +} +h6 { + margin-block-start: 2.33rem; + margin-block-end: 2.33rem; + font-size: 0.67rem; + font-weight: bold; +} +a { + color: var(--main-text-color); +} +a:hover { + background: var(--main-transparent); +} +img { + width: auto; + height: auto; +} +blockquote { + color: var(--main-small-text-color); + border-left: 3px solid var(--main-transparent); + padding: 0 1rem; + margin-left: 0; + margin-right: 0; +} +hr { + border: none; + height: 1px; + background: var(--main-small-text-color); +} +code { + background: var(--main-transparent); + border-radius: 0.1875rem; + /* padding: .0625rem .1875rem; */ + /* margin: 0 .1875rem; */ +} +code, +pre { + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; +} +small { + font-size: 0.95rem; + color: var(--main-small-text-color); +} +small a { + color: inherit; /* Inherit the color of the surrounding text */ + text-decoration: underline; /* Optional: Keep the underline to indicate a link */ +} +.image-container { + text-align: center; + margin: 20px 0; /* Optional: add some spacing around the image container */ +} + +.image-container img { + /* max-width: 308px; */ + max-height: 308px; +} + +.image-container small { + display: block; /* Ensure the caption is on a new line */ + margin-top: 5px; /* Optional: adjust spacing between image and caption */ +} + +.image-container small a { + color: inherit; /* Ensure the link color matches the small text */ + text-decoration: underline; /* Optional: underline to indicate a link */ +} +#header ul { + list-style-type: none; + padding-left: 0; +} +#header li { + display: inline; + font-size: 1.2rem; + margin-right: 1.2rem; +} +#container { + margin: 2.5rem auto; + width: 90%; + max-width: 60ch; +} +#postlistdiv ul { + list-style-type: none; + padding-left: 0; +} +.moreposts { + font-size: 0.95rem; + padding-left: 0.5rem; +} +#nextprev { + text-align: center; + margin-top: 1.4rem; + font-size: 0.95rem; +} +#footer { + color: var(--main-small-text-color); +} From 93c0359df20cde33348a9c02302f16651d6f9938 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 28 Nov 2024 19:05:24 -0500 Subject: [PATCH 19/64] fixed relative paths for stylesheet --- internal/builder/build_page.go | 20 +++++++++++++++----- internal/builder/config.go | 3 +-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index c5fac75..e222f3e 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "html/template" + "log" + "path/filepath" "strings" "github.com/ficcdaf/zona/internal/util" @@ -45,23 +47,31 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { return meta, []byte(split[2]), nil } -func buildPageData(m Metadata, path string, settings *Settings) *PageData { +func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { p.Title = util.WordsToTitle(title) } else { - p.Title = util.PathToTitle(path) + p.Title = util.PathToTitle(in) } if icon, ok := m["icon"].(string); ok { p.Icon = icon } else { p.Icon = settings.IconName } + var stylePath string if style, ok := m["style"].(string); ok { - p.Stylesheet = style + stylePath = style } else { - p.Stylesheet = settings.StylePath + stylePath = settings.StylePath } + curDir := filepath.Dir(out) + relPath, err := filepath.Rel(curDir, stylePath) + // fmt.Printf("fp: %s, sp: %s, rp: %s\n", curDir, stylePath, relPath) + if err != nil { + log.Fatalln("Error calculating stylesheet path: ", err) + } + p.Stylesheet = relPath if header, ok := m["header"].(string); ok { p.HeaderName = header // for now we use default anyways @@ -94,7 +104,7 @@ func ConvertFile(in string, out string, settings *Settings) error { if err != nil { return err } - pd := buildPageData(metadata, in, settings) + pd := buildPageData(metadata, in, out, settings) fmt.Println("Title: ", pd.Title) // build according to template here diff --git a/internal/builder/config.go b/internal/builder/config.go index de88646..5f96c41 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -94,8 +94,7 @@ func buildSettings(f []byte, outRoot string) (*Settings, error) { if isDefaultStyle { stylePath := filepath.Join(outRoot, defaultNames["stylePath"]) - // We convert the stylesheet path to its website root dir format and store it - s.StylePath = "/" + util.StripTopDir(stylePath) + s.StylePath = stylePath err := util.CreateParents(stylePath) if err != nil { return nil, util.ErrorPrepend("Could not create default stylesheet directory: ", err) From ab1b7afaf8bbc1be8fa3ae95efb0bead35397ee8 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 30 Nov 2024 18:24:36 -0500 Subject: [PATCH 20/64] fixed issue with header/footer rules not rendering properly --- internal/builder/embed/default.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/builder/embed/default.html b/internal/builder/embed/default.html index 2dfb1a8..393514e 100644 --- a/internal/builder/embed/default.html +++ b/internal/builder/embed/default.html @@ -14,9 +14,9 @@
- + {{ .Content }} -
{{ .Footer }}
+

{{ .Footer }}
From 46f7891e1be2118d2ddc6425de46e393dc88b538 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 30 Nov 2024 18:24:58 -0500 Subject: [PATCH 21/64] fixed config path resolution --- internal/builder/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/builder/config.go b/internal/builder/config.go index 5f96c41..831ec01 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -141,9 +141,10 @@ func GetSettings(root string, outRoot string) *Settings { config = readEmbed(defaultNames["config"]) } else { var err error - config, err = util.ReadFile(filepath.Join(root, configPath)) + // config, err = util.ReadFile(filepath.Join(root, configPath)) + config, err = util.ReadFile(configPath) if err != nil { - log.Fatalln("Fatal internal error: Config file exists but could not be read!") + log.Fatalln("Fatal internal error: Config file exists but could not be read!", err) } } s, err := buildSettings(config, outRoot) From b30e0d3ed9cb046637489335b41fb9897f91f23e Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 30 Nov 2024 18:25:11 -0500 Subject: [PATCH 22/64] fixed yaml frontmatter not being parsed properly --- internal/builder/build_page.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index e222f3e..783268f 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -30,12 +30,14 @@ type Metadata map[string]interface{} func processWithYaml(f []byte) (Metadata, []byte, error) { // Check if the file has valid metadata - if !bytes.HasPrefix(f, []byte("---\n")) { + trimmed := bytes.TrimSpace(f) + normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n") + if !strings.HasPrefix(normalized, ("---\n")) { // No valid yaml, so return the entire content return nil, f, nil } // Separate YAML from rest of document - split := strings.SplitN(string(f), "---\n", 3) + split := strings.SplitN(normalized, "---\n", 3) if len(split) < 3 { return nil, nil, fmt.Errorf("Invalid frontmatter format.") } From 58ee11622d032d2818326e1636cbdad26cba0a23 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 30 Nov 2024 23:40:31 -0500 Subject: [PATCH 23/64] fix install script --- build.sh | 3 ++- go.mod | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index fc1bdf6..c63bb5d 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,5 @@ #!/bin/bash +go mod tidy go build -o bin/zona ./cmd/zona -ln -sf bin/zona ./zona +sudo cp -f bin/zona /usr/bin/zona diff --git a/go.mod b/go.mod index d37e636..20206b8 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/text v0.20.0 // indirect +require golang.org/x/text v0.20.0 From 1467001f6779cecbcf370733f74b71ebf86a62ca Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 1 Dec 2024 00:03:41 -0500 Subject: [PATCH 24/64] renamed build to install --- build.sh => install.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename build.sh => install.sh (100%) diff --git a/build.sh b/install.sh similarity index 100% rename from build.sh rename to install.sh From e3681267616d81199d7093998d9aa12aaa0bb297 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 2 Dec 2024 00:34:01 -0500 Subject: [PATCH 25/64] Update readme --- README.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8e7584c..066093e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Zona -Zona is a small tool for building a static website. It will allows users to write pages and blog posts in Markdown and build them into a static, lightweight website. +Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles. -**Warning:** Zona has not yet reached **v1**, and it is not yet functional. Therefore, no installation instructions are provided. You may check progress in the `dev` branch of this repository. +**Warning:** Zona has not yet reached **v1**. The `dev-*` branches of this repository contain the code -- however, there is no assurance of stability or functionality until the first release. Configuration and usage documentation will also be provided at this time. ## Table of Contents @@ -14,26 +14,28 @@ Zona is a small tool for building a static website. It will allows users to writ ## Design Goals -Zona is intended to be lightweight, easy-to-use, and ship with sane defaults while still providing flexibility and powerful customization options. This tool is intended for people that wish to maintain a simple blog or personal website, do not want to write HTML, and don't need any JavaScript or backend functionality. This makes it perfect for static site hosting like Neocities or GitHub Pages. +Zona is intended to be easy-to-use. A user should be able to build a reasonably complex website or blog with only a directory of Markdown content and a single command, without needing to write any HTML or configuration. However, users should optionally have access to sensible and flexible configuration options, including writing HTML. The output of Zona should also be lightweight, retaining the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages. ## v1 Features -- Write pages in Markdown and build a simple HTML website. -- Automatically generated `Archive` page and `Recent Posts` element. -- Custom CSS support with sensible light & dark default themes. -- Single-command build process with optional flags for more control. -- HTML layout optimized for screen readers. +- Write pages purely in Markdown. +- Single-command build process. - Lightweight output. +- Sensible default template and stylesheet. +- Configuration entirely optional, but very powerful. +- Site header and footer defined in Markdown. +- Declarative metadata per Markdown file. +- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements. +- Support for custom stylesheets, favicons, and page templates. ## Roadmap -- [ ] Optional configuration file to define build options. -- [ ] Optional RSS/Atom feed generation. -- [ ] Optional image optimization & dithering. -- [ ] AUR package. +- [ ] RSS/Atom feed generation. +- [ ] Image optimization & dithering. - [ ] Windows, Mac, Linux releases. -- [ ] Custom tags that expand to user-defined HTML templates. -- [ ] Companion Neovim plugin for previewing website while editing. +- [ ] AUR package. +- [ ] Custom Markdown tags that expand to user-defined templates. +- [ ] Live preview local server. ## Contribution @@ -43,3 +45,5 @@ Zona is a small project maintained by a very busy graduate student. If you want - [Zoner](https://git.sr.ht/~ryantrawick/zoner) - [Zonelets](https://zonelets.net/) + +> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I have never used it, nor read its documentation, thus it is not listed as an inspiration. From 89f43ea03cb73c3b73c5e26cdca14d6df43e861d Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 2 Dec 2024 00:34:01 -0500 Subject: [PATCH 26/64] Update readme --- README.md | 82 +++++++++++++++----------------------- README.md.orig | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 README.md.orig diff --git a/README.md b/README.md index f4f5af4..066093e 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,49 @@ # Zona -Zona is a small tool for building a static blog website. It allows users to write pages and blog posts in Markdown and automatically build them into a static, lightweight blog. +Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles. -> **Warning:** Implementing v1 functionality is still WIP. There will be binaries available to download from the Releases tab when Zona is ready to be used. Until then, you are welcome to download the source code, but I can't promise anything will work! +**Warning:** Zona has not yet reached **v1**. The `dev-*` branches of this repository contain the code -- however, there is no assurance of stability or functionality until the first release. Configuration and usage documentation will also be provided at this time. ## Table of Contents -- [Features](#v1-features) -- [Installation](#installation) +- [Design Goals](#design-goals) +- [v1 Features](#v1-features) - [Roadmap](#roadmap) +- [Contribution](#contribution) +- [Inspirations](#inspirations) + +## Design Goals + +Zona is intended to be easy-to-use. A user should be able to build a reasonably complex website or blog with only a directory of Markdown content and a single command, without needing to write any HTML or configuration. However, users should optionally have access to sensible and flexible configuration options, including writing HTML. The output of Zona should also be lightweight, retaining the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages. ## v1 Features -- Write your pages in Markdown. -- Build a lightweight website with zero JavaScript. -- Simple CLI build interface. -- HTML layout optimized for screen readers and Neocities. - -## Getting Started - -### Dependencies - -- `go 1.23.2` - -```Bash -# On Arch Linux -sudo pacman -S go - -# On Ubuntu/Debian -sudo apt install go -``` - -### Installation - -First, download the repository and open it: - -```Bash -git clone https://github.com/ficcdaf/zona.git && cd zona -``` - -On Linux: - -```Bash -# run the provided build script -./build.sh -``` - -On other platforms: - -```Bash -go build -o bin/zona cmd/zona -``` - -The resulting binary can be found at `bin/zona`. +- Write pages purely in Markdown. +- Single-command build process. +- Lightweight output. +- Sensible default template and stylesheet. +- Configuration entirely optional, but very powerful. +- Site header and footer defined in Markdown. +- Declarative metadata per Markdown file. +- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements. +- Support for custom stylesheets, favicons, and page templates. ## Roadmap -- [ ] Zona configuration file to define build options. -- [ ] Image optimization & dithering options. -- [ ] AUR package after first release -- [ ] Automatic RSS/Atom feed generation. +- [ ] RSS/Atom feed generation. +- [ ] Image optimization & dithering. +- [ ] Windows, Mac, Linux releases. +- [ ] AUR package. +- [ ] Custom Markdown tags that expand to user-defined templates. +- [ ] Live preview local server. + +## Contribution + +Zona is a small project maintained by a very busy graduate student. If you want to contribute, you are more than welcome to submit issues and pull requests. ## Inspirations - [Zoner](https://git.sr.ht/~ryantrawick/zoner) -- Zonelets +- [Zonelets](https://zonelets.net/) + +> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I have never used it, nor read its documentation, thus it is not listed as an inspiration. diff --git a/README.md.orig b/README.md.orig new file mode 100644 index 0000000..9665995 --- /dev/null +++ b/README.md.orig @@ -0,0 +1,106 @@ +# Zona + +Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles. + +**Warning:** Zona has not yet reached **v1**. The `dev-*` branches of this repository contain the code -- however, there is no assurance of stability or functionality until the first release. Configuration and usage documentation will also be provided at this time. + +## Table of Contents + +- [Features](#v1-features) +- [Installation](#installation) +- [Roadmap](#roadmap) + +## v1 Features + +- Write your pages in Markdown. +- Build a lightweight website with zero JavaScript. +- Simple CLI build interface. +- HTML layout optimized for screen readers and Neocities. + +## Getting Started + +### Dependencies + +- `go 1.23.2` + +```Bash +# On Arch Linux +sudo pacman -S go + +# On Ubuntu/Debian +sudo apt install go +``` + +### Installation + +First, download the repository and open it: + +```Bash +git clone https://github.com/ficcdaf/zona.git && cd zona +``` + +On Linux: + +```Bash +# run the provided build script +./build.sh +``` + +On other platforms: + +```Bash +go build -o bin/zona cmd/zona +``` + +The resulting binary can be found at `bin/zona`. + +## Roadmap + +- [ ] Zona configuration file to define build options. +- [ ] Image optimization & dithering options. +- [ ] AUR package after first release +- [ ] Automatic RSS/Atom feed generation. +======= +- [Contribution](#contribution) +- [Inspirations](#inspirations) + +## Design Goals + +Zona is intended to be easy-to-use. A user should be able to build a reasonably complex website or blog with only a directory of Markdown content and a single command, without needing to write any HTML or configuration. However, users should optionally have access to sensible and flexible configuration options, including writing HTML. The output of Zona should also be lightweight, retaining the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages. + +## v1 Features + +- Write pages purely in Markdown. +- Single-command build process. +- Lightweight output. +- Sensible default template and stylesheet. +- Configuration entirely optional, but very powerful. +- Site header and footer defined in Markdown. +- Declarative metadata per Markdown file. +- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements. +- Support for custom stylesheets, favicons, and page templates. + +## Roadmap + +- [ ] RSS/Atom feed generation. +- [ ] Image optimization & dithering. +- [ ] Windows, Mac, Linux releases. +- [ ] AUR package. +- [ ] Custom Markdown tags that expand to user-defined templates. +- [ ] Live preview local server. + +## Contribution + +Zona is a small project maintained by a very busy graduate student. If you want to contribute, you are more than welcome to submit issues and pull requests. +>>>>>>> e368126 (Update readme) + +## Inspirations + +- [Zoner](https://git.sr.ht/~ryantrawick/zoner) +<<<<<<< HEAD +- Zonelets +======= +- [Zonelets](https://zonelets.net/) + +> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I have never used it, nor read its documentation, thus it is not listed as an inspiration. +>>>>>>> e368126 (Update readme) From d934e0c250c468697c342977e103a599a40eaf08 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 4 Dec 2024 14:36:53 -0500 Subject: [PATCH 27/64] updated go to 1.23.4 --- go.mod | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index d37e636..d9c42d7 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module github.com/ficcdaf/zona -go 1.23.2 +// go 1.23.2 +go 1.23.4 require ( github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/text v0.20.0 // indirect +require golang.org/x/text v0.20.0 From fb67ef046a955d81717bde8b7f561dca9acce3a3 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 29 Dec 2024 20:06:57 -0500 Subject: [PATCH 28/64] added check for posts directory --- go.mod | 2 +- internal/builder/build_page.go | 3 ++- internal/util/path.go | 21 +++++++++++++++++++++ runtest.sh | 2 +- test/in.md | 2 +- test/posts/in.md | 10 ++++++++++ test/yamltest.md | 1 + 7 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 test/posts/in.md diff --git a/go.mod b/go.mod index d9c42d7..9b7a11c 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/text v0.20.0 +require golang.org/x/text v0.21.0 diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 783268f..b3bb817 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -89,7 +89,8 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD p.FooterName = settings.FooterName p.Footer = settings.Footer } - if t, ok := m["type"].(string); ok && t == "article" || t == "post" { + // TODO: Don't hard code posts dir name + if t, ok := m["type"].(string); util.InDir(in, "posts") && !ok || (ok && t == "article" || t == "post") { p.Template = (settings.ArticleTemplate) } else { p.Template = (settings.DefaultTemplate) diff --git a/internal/util/path.go b/internal/util/path.go index 12fa15e..02b4b7b 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -40,6 +40,27 @@ func ReplaceRoot(inPath, outRoot string) string { return outPath } +// InDir checks whether checkPath is +// inside targDir. +func InDir(checkPath string, targDir string) bool { + // fmt.Println("checking dir..") + i := 0 + for i < 10 { + parent := filepath.Dir(checkPath) + fmted := filepath.Base(parent) + switch fmted { + case targDir: + // fmt.Printf("%s in %s\n", checkPath, targDir) + return true + case ".": + return false + } + checkPath = parent + i += 1 + } + return false +} + // FileExists returns a boolean indicating // whether something exists at the path. func FileExists(path string) bool { diff --git a/runtest.sh b/runtest.sh index d20c26e..1a75f4d 100755 --- a/runtest.sh +++ b/runtest.sh @@ -5,4 +5,4 @@ fi go run cmd/zona/main.go test -bat foobar/in.html +bat foobar/posts/in.html diff --git a/test/in.md b/test/in.md index 2fd2350..57171ba 100644 --- a/test/in.md +++ b/test/in.md @@ -1,5 +1,5 @@ --- -type: article +title: tiltetest --- # My amazing markdown file! diff --git a/test/posts/in.md b/test/posts/in.md new file mode 100644 index 0000000..57171ba --- /dev/null +++ b/test/posts/in.md @@ -0,0 +1,10 @@ +--- +title: tiltetest +--- + +# My amazing markdown file! + +I can _even_ do **this**! + +- Or, I could... +- [Link](page.md) to this file diff --git a/test/yamltest.md b/test/yamltest.md index 974dedb..ffa1fb3 100644 --- a/test/yamltest.md +++ b/test/yamltest.md @@ -1,5 +1,6 @@ --- title: Yaml testing file +type: article --- # My amazing markdown file! From 0c1c842bcd7f34129e3231a2296f5a546c3ee2f3 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 30 Dec 2024 23:31:43 -0500 Subject: [PATCH 29/64] added custom image renderer with image-container div --- internal/builder/convert.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/builder/convert.go b/internal/builder/convert.go index ed72fbd..ea7e46f 100644 --- a/internal/builder/convert.go +++ b/internal/builder/convert.go @@ -53,6 +53,20 @@ func processLink(p string) string { } } +func renderImage(w io.Writer, node *ast.Image, entering bool) { + // we add image-container div tag + // here before the opening img tag + if entering { + fmt.Fprintf(w, "
\n") + fmt.Fprintf(w, ``, node.Destination, node.Title) + } else { + // if it's the closing img tag + // we close the div tag *after* + fmt.Fprintf(w, `
`) + fmt.Println("Image node not entering??") + } +} + func renderLink(w io.Writer, l *ast.Link, entering bool) { if entering { destPath := processLink(string(l.Destination)) @@ -66,10 +80,15 @@ func renderLink(w io.Writer, l *ast.Link, entering bool) { } } +// htmlRenderHook hooks the HTML renderer and overrides the rendering of certain nodes. func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { if link, ok := node.(*ast.Link); ok { renderLink(w, link, entering) return ast.GoToNext, true + } else if image, ok := node.(*ast.Image); ok { + // TODO: should do something more interesting with the alt text -- like put it in a tag? + renderImage(w, image, entering) + return ast.GoToNext, true } return ast.GoToNext, false } From ce706e4ff9d49d4e6f57e339c04527f039b0613f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 30 Dec 2024 23:31:50 -0500 Subject: [PATCH 30/64] added tests for image renderer --- runtest.sh | 2 +- test/assets/pic.png | Bin 0 -> 1073 bytes test/img.md | 3 +++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 test/assets/pic.png create mode 100644 test/img.md diff --git a/runtest.sh b/runtest.sh index 1a75f4d..5df5005 100755 --- a/runtest.sh +++ b/runtest.sh @@ -5,4 +5,4 @@ fi go run cmd/zona/main.go test -bat foobar/posts/in.html +bat foobar/img.html diff --git a/test/assets/pic.png b/test/assets/pic.png new file mode 100644 index 0000000000000000000000000000000000000000..9470f0878f45a9a2cff72ff6fcbfd550daac338f GIT binary patch literal 1073 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z|g?K%)r3l`$c>s0|P@>fKP}k!=M4eOdR?Q z3=Fy@L4LsuY(JkIleY9@ZkL$$>d*6;IUCcyC109jz|V8Qp7W%)&6&e1N&{OSPA<}| z2nm1h-2aAwfmzej#WAFU@$KEvyfu~*Ef2HO($XY;eA~b0;mcE&8&kfo|K=LG?)3%X zio<3*e6!2n%)C=|JT|*I|F+$3wLR~OyXCL#nEm4&Z$ow@Q})H;Z7OBQ>UT#f?S42v zR(EDBPcAkn%>)wA9C!x6=h-*@bva_&Woo0 z`Yxh>1gsp&r0WD0=KRm+yr~=CVf4n7qrc0{1uzJhG*xgB13ah6*h&MW>Vu@kavY>s?S_?Vj)uotyAI$mJD zc|e{;cY^O<#|un54#^!QWeHc2Mq9!pe zy!O5Nm+6THr+sHMNwZ>&xrzuAMiOztgzI2C24BUlO%D+ zk%KwWRQuUX-H#wS9w!ZstzLruOWXXLCD_6R4mn2n-23h$*1?b@A>+uwzVKr8^~>1` zOdW{@Y>F~14|gw6RB8bE=FfsdAKxkPosexSl6|Of?DyY<{KJxLG`3O9F zr_jbo?Xf-B z$A73@uKWf6HZcy_`VCthdbV5NC{j3fTQ;IVDAqx_Ky;~SS0j?F$8#%6err%dX9h=q)k+>=PQPTgiB4FsBG zajD>|)0E5BB}O}w9j!BE@7((NGUKka{;buB*Zl5;)NfBowp&IhYl^^W~p{TGz=Jzf1= J);T3K0RX-kk;(u7 literal 0 HcmV?d00001 diff --git a/test/img.md b/test/img.md new file mode 100644 index 0000000..05c1463 --- /dev/null +++ b/test/img.md @@ -0,0 +1,3 @@ +# An image is in this page + +![my alternate text](assets/pic.png "my title") From 525cbcd9807e4ee7d648eca30ed096f15f055449 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 31 Dec 2024 00:29:32 -0500 Subject: [PATCH 31/64] feat: rendering for image descriptions Text inside an image's alt-text section `![like this text](img.png)` will be treated as markdown and rendered as HTML inside `` tags (all still inside the image-container div). --- internal/builder/convert.go | 51 ++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/internal/builder/convert.go b/internal/builder/convert.go index ea7e46f..d8125b9 100644 --- a/internal/builder/convert.go +++ b/internal/builder/convert.go @@ -53,12 +53,21 @@ func processLink(p string) string { } } -func renderImage(w io.Writer, node *ast.Image, entering bool) { +// renderImage outputs an ast Image node as HTML string. +func renderImage(w io.Writer, node *ast.Image, entering bool, next *ast.Text) { // we add image-container div tag // here before the opening img tag if entering { fmt.Fprintf(w, "
\n") fmt.Fprintf(w, ``, node.Destination, node.Title) + if next != nil && len(next.Literal) > 0 { + // handle rendering Literal as markdown here + md := []byte(next.Literal) + html := convertEmbedded(md) + // TODO: render inside a special div? + // is this necessary since this is all inside image-container anyways? + fmt.Fprintf(w, `%s`, html) + } } else { // if it's the closing img tag // we close the div tag *after* @@ -80,19 +89,55 @@ func renderLink(w io.Writer, l *ast.Link, entering bool) { } } +func htmlRenderHookNoImage(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { + if link, ok := node.(*ast.Link); ok { + renderLink(w, link, entering) + return ast.GoToNext, true + } else if _, ok := node.(*ast.Image); ok { + // we do not render images + return ast.GoToNext, true + } + return ast.GoToNext, false +} + // htmlRenderHook hooks the HTML renderer and overrides the rendering of certain nodes. func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { if link, ok := node.(*ast.Link); ok { renderLink(w, link, entering) return ast.GoToNext, true } else if image, ok := node.(*ast.Image); ok { - // TODO: should do something more interesting with the alt text -- like put it in a tag? - renderImage(w, image, entering) + var nextNode *ast.Text + if entering { + nextNodes := node.GetChildren() + fmt.Println("img next node len:", len(nextNodes)) + if len(nextNodes) == 1 { + if textNode, ok := nextNodes[0].(*ast.Text); ok { + nextNode = textNode + } + } + } + renderImage(w, image, entering, nextNode) + // Skip rendering of `nextNode` explicitly + if nextNode != nil { + return ast.SkipChildren, true + } return ast.GoToNext, true } return ast.GoToNext, false } +// convertEmbedded renders markdown as HTML +// but does NOT render images +func convertEmbedded(md []byte) []byte { + p := parser.NewWithExtensions(parser.CommonExtensions) + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + opts.RenderNodeHook = htmlRenderHookNoImage + r := html.NewRenderer(opts) + htmlText := markdown.ToHTML(md, p, r) + return htmlText +} + func newZonaRenderer(opts html.RendererOptions) *html.Renderer { opts.RenderNodeHook = htmlRenderHook return html.NewRenderer(opts) From ffe5ea4efcb26d9995128123aebff4bbcbab7539 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 31 Dec 2024 00:29:48 -0500 Subject: [PATCH 32/64] updated test file for image tag embedded rendering --- test/img.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/img.md b/test/img.md index 05c1463..6c2fce1 100644 --- a/test/img.md +++ b/test/img.md @@ -1,3 +1,3 @@ # An image is in this page -![my alternate text](assets/pic.png "my title") +![my *alternate* text](assets/pic.png "my title") From 709a2738f9220c60497ffacfca729066fcee150f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 31 Dec 2024 00:48:22 -0500 Subject: [PATCH 33/64] feat: image rendering now supports alt text for accessibility --- internal/builder/convert.go | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/internal/builder/convert.go b/internal/builder/convert.go index d8125b9..12831b3 100644 --- a/internal/builder/convert.go +++ b/internal/builder/convert.go @@ -1,6 +1,7 @@ package builder import ( + "bytes" "fmt" "io" "os" @@ -59,14 +60,18 @@ func renderImage(w io.Writer, node *ast.Image, entering bool, next *ast.Text) { // here before the opening img tag if entering { fmt.Fprintf(w, "
\n") - fmt.Fprintf(w, ``, node.Destination, node.Title) + fmt.Fprintf(w, ` 0 { - // handle rendering Literal as markdown here md := []byte(next.Literal) - html := convertEmbedded(md) + html, doc := convertEmbedded(md) + altText := extractPlainText(md, doc) + fmt.Fprintf(w, ` alt="%s">`, altText) // TODO: render inside a special div? // is this necessary since this is all inside image-container anyways? fmt.Fprintf(w, `%s`, html) + } else { + // + io.WriteString(w, ">") } } else { // if it's the closing img tag @@ -128,17 +133,34 @@ func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, // convertEmbedded renders markdown as HTML // but does NOT render images -func convertEmbedded(md []byte) []byte { +// also returns document AST +func convertEmbedded(md []byte) ([]byte, *ast.Node) { p := parser.NewWithExtensions(parser.CommonExtensions) + doc := p.Parse(md) htmlFlags := html.CommonFlags | html.HrefTargetBlank opts := html.RendererOptions{Flags: htmlFlags} opts.RenderNodeHook = htmlRenderHookNoImage r := html.NewRenderer(opts) - htmlText := markdown.ToHTML(md, p, r) - return htmlText + html := markdown.Render(doc, r) + return html, &doc } func newZonaRenderer(opts html.RendererOptions) *html.Renderer { opts.RenderNodeHook = htmlRenderHook return html.NewRenderer(opts) } + +// ExtractPlainText walks the AST and extracts plain text from the Markdown input. +func extractPlainText(md []byte, doc *ast.Node) string { + var buffer bytes.Buffer + + // Walk the AST and extract text nodes + ast.WalkFunc(*doc, func(node ast.Node, entering bool) ast.WalkStatus { + if textNode, ok := node.(*ast.Text); ok && entering { + buffer.Write(textNode.Literal) // Append the text content + } + return ast.GoToNext + }) + + return buffer.String() +} From 35c14f09c08eda636b62c736df85c21ea6a44ee8 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 31 Dec 2024 22:47:01 -0500 Subject: [PATCH 34/64] begin implementing separate metadata parsing --- internal/builder/build_page.go | 29 ++++++++++++ internal/builder/build_page_test.go | 37 +++++++++++++++ internal/builder/process.go | 73 +++++++++++++++++++++++++++++ internal/builder/traverse.go | 5 +- internal/util/file.go | 24 ++++++++++ internal/util/queue.go | 26 ++++++++++ 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 internal/builder/build_page_test.go create mode 100644 internal/builder/process.go create mode 100644 internal/util/queue.go diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index b3bb817..0183871 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -24,6 +24,7 @@ type PageData struct { FooterName string Footer template.HTML Template string + Type string } type Metadata map[string]interface{} @@ -49,6 +50,32 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { return meta, []byte(split[2]), nil } +func processFrontmatter(p string) (Metadata, error) { + // read only the first three lines + f, err := util.ReadNLines(p, 3) + if err != nil { + return nil, err + } + // Check if the file has valid metadata + trimmed := bytes.TrimSpace(f) + normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n") + if !strings.HasPrefix(normalized, ("---\n")) { + // No valid yaml, so return the entire content + return nil, nil + } + // Separate YAML from rest of document + split := strings.SplitN(normalized, "---\n", 3) + if len(split) < 3 { + return nil, fmt.Errorf("Invalid frontmatter format.") + } + var meta Metadata + // Parse YAML + if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil { + return nil, err + } + return meta, nil +} + func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { @@ -92,8 +119,10 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD // TODO: Don't hard code posts dir name if t, ok := m["type"].(string); util.InDir(in, "posts") && !ok || (ok && t == "article" || t == "post") { p.Template = (settings.ArticleTemplate) + p.Type = "post" } else { p.Template = (settings.DefaultTemplate) + p.Type = "" } return p } diff --git a/internal/builder/build_page_test.go b/internal/builder/build_page_test.go new file mode 100644 index 0000000..3785faa --- /dev/null +++ b/internal/builder/build_page_test.go @@ -0,0 +1,37 @@ +package builder + +import "testing" + +func Test_processWithYaml(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + f []byte + want Metadata + want2 []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got2, gotErr := processWithYaml(tt.f) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("processWithYaml() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("processWithYaml() succeeded unexpectedly") + } + // TODO: update the condition below to compare got with tt.want. + if true { + t.Errorf("processWithYaml() = %v, want %v", got, tt.want) + } + if true { + t.Errorf("processWithYaml() = %v, want %v", got2, tt.want2) + } + }) + } +} diff --git a/internal/builder/process.go b/internal/builder/process.go new file mode 100644 index 0000000..5f57055 --- /dev/null +++ b/internal/builder/process.go @@ -0,0 +1,73 @@ +package builder + +import ( + "io/fs" + "path/filepath" + + "github.com/ficcdaf/zona/internal/util" +) + +type ProcessMemory struct { + // Pages holds all page data that may be + // needed while building *other* pages. + Pages []*Page + // Queue is a FIFO queue of Pages indexes to be built. + // queue should be constructed after all the Pages have been parsed + Queue []int + // Posts is an array of pointers to post pages + Posts []*Page +} + +type Page struct { + Data *PageData + Ext string + InPath string + OutPath string + Copy bool +} + +// processFile processes the metadata only +// of each file +func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings, pm *ProcessMemory) error { + if err != nil { + return err + } + var toProcess bool + var outPath string + var ext string + if !entry.IsDir() { + ext = filepath.Ext(inPath) + outPath = util.ReplaceRoot(inPath, outRoot) + switch ext { + case ".md": + // fmt.Println("Processing markdown...") + toProcess = true + outPath = util.ChangeExtension(outPath, ".html") + // If it's not a file we need to process, + // we simply copy it to the destination path. + default: + toProcess = false + } + } + page := &Page{ + nil, + ext, + inPath, + outPath, + !toProcess, + } + if toProcess { + // process its frontmatter here + m, err := processFrontmatter(inPath) + if err != nil { + return err + } + pd := buildPageData(m, inPath, outPath, settings) + if pd.Type == "post" { + pm.Posts = append(pm.Posts, page) + } + page.Data = pd + } + pm.Pages = append(pm.Pages, page) + return nil +} diff --git a/internal/builder/traverse.go b/internal/builder/traverse.go index 5272dca..824df5a 100644 --- a/internal/builder/traverse.go +++ b/internal/builder/traverse.go @@ -8,7 +8,8 @@ import ( "github.com/ficcdaf/zona/internal/util" ) -func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error { +// TODO: Process the metadata and build a queue of files to convert here instead of converting them immediately +func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error { if err != nil { return err } @@ -46,7 +47,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se func Traverse(root string, outRoot string, settings *Settings) error { walkFunc := func(path string, entry fs.DirEntry, err error) error { - return processFile(path, entry, err, outRoot, settings) + return buildFile(path, entry, err, outRoot, settings) } err := filepath.WalkDir(root, walkFunc) return err diff --git a/internal/util/file.go b/internal/util/file.go index 2028018..e164319 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -1,6 +1,8 @@ package util import ( + "bufio" + "bytes" "io" "os" ) @@ -56,3 +58,25 @@ func CopyFile(inPath string, outPath string) error { return nil } } + +// ReadNLines reads the first N lines from a file as a single byte array +func ReadNLines(filename string, n int) ([]byte, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var buffer bytes.Buffer + scanner := bufio.NewScanner(file) + for i := 0; i < 3 && scanner.Scan(); i++ { + buffer.Write(scanner.Bytes()) + buffer.WriteByte('\n') + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} diff --git a/internal/util/queue.go b/internal/util/queue.go new file mode 100644 index 0000000..3fce4fa --- /dev/null +++ b/internal/util/queue.go @@ -0,0 +1,26 @@ +package util + +// Enqueue appends an int to the queue +func Enqueue(queue []int, element int) []int { + queue = append(queue, element) + return queue +} + +// Dequeue pops the first element of the queue +func Dequeue(queue []int) (int, []int) { + element := queue[0] // The first element is the one to be dequeued. + if len(queue) == 1 { + tmp := []int{} + return element, tmp + } + return element, queue[1:] // Slice off the element once it is dequeued. +} + +func Tail(queue []int) int { + l := len(queue) + if l == 0 { + return -1 + } else { + return l - 1 + } +} From af81617db51f6b94eb619997b53bd6ae7855e4d5 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 1 Jan 2025 23:55:16 -0500 Subject: [PATCH 35/64] fixed processFrontmatter and added test --- cmd/zona/main.go | 3 +- internal/builder/build_page.go | 10 ++- internal/builder/build_page_test.go | 89 +++++++++++++------- internal/builder/convert_test.go | 122 ---------------------------- internal/builder/process.go | 73 +++++++++++------ internal/builder/traverse.go | 11 ++- internal/util/file_test.go | 37 +++++++++ 7 files changed, 162 insertions(+), 183 deletions(-) delete mode 100644 internal/builder/convert_test.go create mode 100644 internal/util/file_test.go diff --git a/cmd/zona/main.go b/cmd/zona/main.go index d61da6c..5409934 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -42,7 +42,8 @@ func main() { } settings := builder.GetSettings(*rootPath, "foobar") - err := builder.Traverse(*rootPath, "foobar", settings) + // err := builder.Traverse(*rootPath, "foobar", settings) + err := builder.ProcessTraverse(*rootPath, "foobar", settings) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 0183871..03b01e0 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -51,8 +51,8 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { } func processFrontmatter(p string) (Metadata, error) { - // read only the first three lines - f, err := util.ReadNLines(p, 3) + // TODO: Also save index of the line where the actual document starts? + f, err := util.ReadFile(p) if err != nil { return nil, err } @@ -60,11 +60,13 @@ func processFrontmatter(p string) (Metadata, error) { trimmed := bytes.TrimSpace(f) normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n") if !strings.HasPrefix(normalized, ("---\n")) { - // No valid yaml, so return the entire content + // No frontmatter, return nil -- handled by caller return nil, nil } // Separate YAML from rest of document split := strings.SplitN(normalized, "---\n", 3) + // __AUTO_GENERATED_PRINT_VAR_START__ + fmt.Println(fmt.Sprintf("processFrontmatter split: %v", split)) // __AUTO_GENERATED_PRINT_VAR_END__ if len(split) < 3 { return nil, fmt.Errorf("Invalid frontmatter format.") } @@ -127,7 +129,7 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD return p } -func ConvertFile(in string, out string, settings *Settings) error { +func BuildHtmlFile(in string, out string, settings *Settings) error { mdPre, err := util.ReadFile(in) if err != nil { return err diff --git a/internal/builder/build_page_test.go b/internal/builder/build_page_test.go index 3785faa..cf0a311 100644 --- a/internal/builder/build_page_test.go +++ b/internal/builder/build_page_test.go @@ -1,37 +1,64 @@ +// FILE: internal/builder/build_page_test.go package builder -import "testing" +import ( + "os" + "testing" +) -func Test_processWithYaml(t *testing.T) { - tests := []struct { - name string // description of this test case - // Named input parameters for target function. - f []byte - want Metadata - want2 []byte - wantErr bool - }{ - // TODO: Add test cases. +func TestProcessFrontmatter(t *testing.T) { + // Create a temporary file with valid frontmatter + validContent := `--- +title: "Test Title" +description: "Test Description" +--- +This is the body of the document.` + + tmpfile, err := os.CreateTemp("", "testfile") + if err != nil { + t.Fatal(err) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, got2, gotErr := processWithYaml(tt.f) - if gotErr != nil { - if !tt.wantErr { - t.Errorf("processWithYaml() failed: %v", gotErr) - } - return - } - if tt.wantErr { - t.Fatal("processWithYaml() succeeded unexpectedly") - } - // TODO: update the condition below to compare got with tt.want. - if true { - t.Errorf("processWithYaml() = %v, want %v", got, tt.want) - } - if true { - t.Errorf("processWithYaml() = %v, want %v", got2, tt.want2) - } - }) + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write([]byte(validContent)); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + // Test the processFrontmatter function with valid frontmatter + meta, err := processFrontmatter(tmpfile.Name()) + if err != nil { + t.Fatalf("processFrontmatter failed: %v", err) + } + + if meta["title"] != "Test Title" || meta["description"] != "Test Description" { + t.Errorf("Expected title 'Test Title' and description 'Test Description', got title '%s' and description '%s'", meta["title"], meta["description"]) + } + + // Create a temporary file with invalid frontmatter + invalidContent := `--- +title: "Test Title" +description: "Test Description" +This is the body of the document.` + + tmpfile, err = os.CreateTemp("", "testfile") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write([]byte(invalidContent)); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + // Test the processFrontmatter function with invalid frontmatter + _, err = processFrontmatter(tmpfile.Name()) + if err == nil { + t.Fatalf("Expected error for invalid frontmatter, got nil") } } diff --git a/internal/builder/convert_test.go b/internal/builder/convert_test.go deleted file mode 100644 index 45c4843..0000000 --- a/internal/builder/convert_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package builder_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/ficcdaf/zona/internal/builder" - "github.com/ficcdaf/zona/internal/util" -) - -func TestMdToHTML(t *testing.T) { - md := []byte("# Hello World\n\nThis is a test.") - expectedHTML := "

Hello World

\n

This is a test.

\n" - nExpectedHTML := util.NormalizeContent(expectedHTML) - html, err := builder.MdToHTML(md) - nHtml := util.NormalizeContent(string(html)) - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - if nHtml != nExpectedHTML { - t.Errorf("Expected:\n%s\nGot:\n%s", expectedHTML, html) - } -} - -func TestWriteFile(t *testing.T) { - path := filepath.Join(t.TempDir(), "test.txt") - content := []byte("Hello, World!") - - err := builder.WriteFile(content, path) - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - // Verify file content - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("Error reading file: %v", err) - } - if string(data) != string(content) { - t.Errorf("Expected:\n%s\nGot:\n%s", content, data) - } -} - -func TestReadFile(t *testing.T) { - path := filepath.Join(t.TempDir(), "test.txt") - content := []byte("Hello, World!") - - err := os.WriteFile(path, content, 0644) - if err != nil { - t.Fatalf("Error writing file: %v", err) - } - - data, err := builder.ReadFile(path) - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - if string(data) != string(content) { - t.Errorf("Expected:\n%s\nGot:\n%s", content, data) - } -} - -func TestCopyFile(t *testing.T) { - src := filepath.Join(t.TempDir(), "source.txt") - dst := filepath.Join(t.TempDir(), "dest.txt") - content := []byte("File content for testing.") - - err := os.WriteFile(src, content, 0644) - if err != nil { - t.Fatalf("Error writing source file: %v", err) - } - - err = builder.CopyFile(src, dst) - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - // Verify destination file content - data, err := os.ReadFile(dst) - if err != nil { - t.Fatalf("Error reading destination file: %v", err) - } - if string(data) != string(content) { - t.Errorf("Expected:\n%s\nGot:\n%s", content, data) - } -} - -func TestConvertFile(t *testing.T) { - src := filepath.Join(t.TempDir(), "test.md") - dst := filepath.Join(t.TempDir(), "test.html") - mdContent := []byte("# Test Title\n\nThis is Markdown content.") - nExpectedHTML := util.NormalizeContent("

Test Title

\n

This is Markdown content.

\n") - - err := os.WriteFile(src, mdContent, 0644) - if err != nil { - t.Fatalf("Error writing source Markdown file: %v", err) - } - - err = builder.ConvertFile(src, dst) - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - // Verify destination HTML content - data, err := os.ReadFile(dst) - if err != nil { - t.Fatalf("Error reading HTML file: %v", err) - } - if util.NormalizeContent(string(data)) != nExpectedHTML { - t.Errorf("Expected:\n%s\nGot:\n%s", nExpectedHTML, data) - } -} - -func TestChangeExtension(t *testing.T) { - input := "test.md" - output := builder.ChangeExtension(input, ".html") - expected := "test.html" - - if output != expected { - t.Errorf("Expected %s, got %s", expected, output) - } -} diff --git a/internal/builder/process.go b/internal/builder/process.go index 5f57055..260be0a 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -8,22 +8,39 @@ import ( ) type ProcessMemory struct { - // Pages holds all page data that may be + // Files holds all page data that may be // needed while building *other* pages. - Pages []*Page + Files []*File // Queue is a FIFO queue of Pages indexes to be built. // queue should be constructed after all the Pages have been parsed Queue []int // Posts is an array of pointers to post pages - Posts []*Page + // This list is ONLY referenced for generating + // the archive, NOT by the build process! + Posts []*File } -type Page struct { - Data *PageData - Ext string - InPath string - OutPath string - Copy bool +type File struct { + Data *PageData + Ext string + InPath string + OutPath string + ShouldCopy bool + HasFrontmatter bool +} + +// NewProcessMemory initializes an empty +// process memory structure +func NewProcessMemory() *ProcessMemory { + f := make([]*File, 0) + q := make([]int, 0) + p := make([]*File, 0) + pm := &ProcessMemory{ + f, + q, + p, + } + return pm } // processFile processes the metadata only @@ -38,36 +55,44 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se if !entry.IsDir() { ext = filepath.Ext(inPath) outPath = util.ReplaceRoot(inPath, outRoot) + // NOTE: This could be an if statement, but keeping + // the switch makes it easy to extend the logic here later switch ext { case ".md": - // fmt.Println("Processing markdown...") toProcess = true outPath = util.ChangeExtension(outPath, ".html") - // If it's not a file we need to process, - // we simply copy it to the destination path. default: toProcess = false } } - page := &Page{ - nil, - ext, - inPath, - outPath, - !toProcess, - } + + var pd *PageData + hasFrontmatter := false if toProcess { // process its frontmatter here m, err := processFrontmatter(inPath) if err != nil { return err } - pd := buildPageData(m, inPath, outPath, settings) - if pd.Type == "post" { - pm.Posts = append(pm.Posts, page) + if m != nil { + hasFrontmatter = true } - page.Data = pd + pd = buildPageData(m, inPath, outPath, settings) + + } else { + pd = nil } - pm.Pages = append(pm.Pages, page) + file := &File{ + pd, + ext, + inPath, + outPath, + !toProcess, + hasFrontmatter, + } + if pd != nil && pd.Type == "post" { + pm.Posts = append(pm.Posts, file) + } + pm.Files = append(pm.Files, file) return nil } diff --git a/internal/builder/traverse.go b/internal/builder/traverse.go index 824df5a..d397f39 100644 --- a/internal/builder/traverse.go +++ b/internal/builder/traverse.go @@ -23,7 +23,7 @@ func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, sett if err := util.CreateParents(outPath); err != nil { return err } - if err := ConvertFile(inPath, outPath, settings); err != nil { + if err := BuildHtmlFile(inPath, outPath, settings); err != nil { return errors.Join(errors.New("Error processing file "+inPath), err) } else { return nil @@ -52,3 +52,12 @@ func Traverse(root string, outRoot string, settings *Settings) error { err := filepath.WalkDir(root, walkFunc) return err } + +func ProcessTraverse(root string, outRoot string, settings *Settings) error { + pm := NewProcessMemory() + walkFunc := func(path string, entry fs.DirEntry, err error) error { + return processFile(path, entry, err, outRoot, settings, pm) + } + err := filepath.WalkDir(root, walkFunc) + return err +} diff --git a/internal/util/file_test.go b/internal/util/file_test.go new file mode 100644 index 0000000..be3131d --- /dev/null +++ b/internal/util/file_test.go @@ -0,0 +1,37 @@ +// FILE: internal/util/file_test.go +package util + +import ( + "bytes" + "os" + "testing" +) + +func TestReadNLines(t *testing.T) { + // Create a temporary file + tmpfile, err := os.CreateTemp("", "testfile") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + // Write some lines to the temporary file + content := []byte("line1\nline2\nline3\nline4\nline5\n") + if _, err := tmpfile.Write(content); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + // Test the ReadNLines function + lines, err := ReadNLines(tmpfile.Name(), 3) + if err != nil { + t.Fatalf("ReadNLines failed: %v", err) + } + + expected := []byte("line1\nline2\nline3\n") + if !bytes.Equal(lines, expected) { + t.Errorf("Expected %q, got %q", expected, lines) + } +} From 587085df86e342059fbdb1906ad05016da2b52a4 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 1 Jan 2025 23:56:49 -0500 Subject: [PATCH 36/64] fixed some error formatting --- internal/builder/build_page.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 03b01e0..96441da 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -40,7 +40,7 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { // Separate YAML from rest of document split := strings.SplitN(normalized, "---\n", 3) if len(split) < 3 { - return nil, nil, fmt.Errorf("Invalid frontmatter format.") + return nil, nil, fmt.Errorf("invalid frontmatter format") } var meta Metadata // Parse YAML @@ -65,10 +65,8 @@ func processFrontmatter(p string) (Metadata, error) { } // Separate YAML from rest of document split := strings.SplitN(normalized, "---\n", 3) - // __AUTO_GENERATED_PRINT_VAR_START__ - fmt.Println(fmt.Sprintf("processFrontmatter split: %v", split)) // __AUTO_GENERATED_PRINT_VAR_END__ if len(split) < 3 { - return nil, fmt.Errorf("Invalid frontmatter format.") + return nil, fmt.Errorf("invalid frontmatter format") } var meta Metadata // Parse YAML From 4d27581f0aef48ed5c2210aa80c97fd15b804e36 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 2 Jan 2025 01:47:49 -0500 Subject: [PATCH 37/64] improved frontmatter processing, added tests --- internal/builder/build_page.go | 26 ------ internal/builder/frontmatter.go | 81 +++++++++++++++++++ ...build_page_test.go => frontmatter_test.go} | 31 ++++++- internal/builder/process.go | 2 +- 4 files changed, 111 insertions(+), 29 deletions(-) create mode 100644 internal/builder/frontmatter.go rename internal/builder/{build_page_test.go => frontmatter_test.go} (66%) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 96441da..dfa0241 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -50,32 +50,6 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { return meta, []byte(split[2]), nil } -func processFrontmatter(p string) (Metadata, error) { - // TODO: Also save index of the line where the actual document starts? - f, err := util.ReadFile(p) - if err != nil { - return nil, err - } - // Check if the file has valid metadata - trimmed := bytes.TrimSpace(f) - normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n") - if !strings.HasPrefix(normalized, ("---\n")) { - // No frontmatter, return nil -- handled by caller - return nil, nil - } - // Separate YAML from rest of document - split := strings.SplitN(normalized, "---\n", 3) - if len(split) < 3 { - return nil, fmt.Errorf("invalid frontmatter format") - } - var meta Metadata - // Parse YAML - if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil { - return nil, err - } - return meta, nil -} - func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { diff --git a/internal/builder/frontmatter.go b/internal/builder/frontmatter.go new file mode 100644 index 0000000..73dfa89 --- /dev/null +++ b/internal/builder/frontmatter.go @@ -0,0 +1,81 @@ +package builder + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +func processFrontmatter(p string) (Metadata, int, error) { + f, l, err := readFrontmatter(p) + if err != nil { + return nil, l, err + } + var meta Metadata + // Parse YAML + if err := yaml.Unmarshal(f, &meta); err != nil { + return nil, l, fmt.Errorf("yaml frontmatter could not be parsed: %w", err) + } + return meta, l, nil +} + +// readFrontmatter reads the file at `path` and scans +// it for --- delimited frontmatter. It does not attempt +// to parse the data, it only scans for the delimiters. +// It returns the frontmatter contents as a byte array +// and its length in lines. +func readFrontmatter(path string) ([]byte, int, error) { + file, err := os.Open(path) + if err != nil { + return nil, 0, err + } + defer file.Close() + + lines := make([]string, 0, 10) + s := bufio.NewScanner(file) + i := 0 + delims := 0 + for s.Scan() { + l := s.Text() + if l == `---` { + if i > 0 && delims == 0 { + // if --- is not the first line, we + // assume the file does not contain frontmatter + return nil, 0, nil + } + delims += 1 + i += 1 + if delims == 2 { + break + } + } else { + lines = append(lines, l) + i += 1 + } + } + // check whether any errors occurred while scanning + if err := s.Err(); err != nil { + return nil, 0, err + } + if delims == 2 { + l := len(lines) + if l == 0 { + // no valid frontmatter + return nil, 0, errors.New("frontmatter cannot be empty") + } + // convert to byte array + var b bytes.Buffer + for _, line := range lines { + b.WriteString(line + "\n") + } + return b.Bytes(), l, nil + } else { + // not enough delimiters, don't + // treat as frontmatter + return nil, 0, errors.New("frontmatter is missing closing delimiter") + } +} diff --git a/internal/builder/build_page_test.go b/internal/builder/frontmatter_test.go similarity index 66% rename from internal/builder/build_page_test.go rename to internal/builder/frontmatter_test.go index cf0a311..8661984 100644 --- a/internal/builder/build_page_test.go +++ b/internal/builder/frontmatter_test.go @@ -28,10 +28,13 @@ This is the body of the document.` } // Test the processFrontmatter function with valid frontmatter - meta, err := processFrontmatter(tmpfile.Name()) + meta, l, err := processFrontmatter(tmpfile.Name()) if err != nil { t.Fatalf("processFrontmatter failed: %v", err) } + if l != 2 { + t.Errorf("Expected length 2, got %d", l) + } if meta["title"] != "Test Title" || meta["description"] != "Test Description" { t.Errorf("Expected title 'Test Title' and description 'Test Description', got title '%s' and description '%s'", meta["title"], meta["description"]) @@ -41,6 +44,7 @@ This is the body of the document.` invalidContent := `--- title: "Test Title" description: "Test Description" +There is no closing delimiter??? This is the body of the document.` tmpfile, err = os.CreateTemp("", "testfile") @@ -57,7 +61,30 @@ This is the body of the document.` } // Test the processFrontmatter function with invalid frontmatter - _, err = processFrontmatter(tmpfile.Name()) + _, _, err = processFrontmatter(tmpfile.Name()) + if err == nil { + t.Fatalf("Expected error for invalid frontmatter, got nil") + } + // Create a temporary file with invalid frontmatter + invalidContent = `--- +--- +This is the body of the document.` + + tmpfile, err = os.CreateTemp("", "testfile") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write([]byte(invalidContent)); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + // Test the processFrontmatter function with invalid frontmatter + _, _, err = processFrontmatter(tmpfile.Name()) if err == nil { t.Fatalf("Expected error for invalid frontmatter, got nil") } diff --git a/internal/builder/process.go b/internal/builder/process.go index 260be0a..45de14a 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -70,7 +70,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se hasFrontmatter := false if toProcess { // process its frontmatter here - m, err := processFrontmatter(inPath) + m, _, err := processFrontmatter(inPath) if err != nil { return err } From 4315348cf5c144a9b4e63728834720970b7c4302 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 5 Jan 2025 03:58:44 -0500 Subject: [PATCH 38/64] added comment --- internal/builder/build_page.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index dfa0241..02d4765 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -101,6 +101,9 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD return p } +// WARNING: This is a reference implementation +// with passing tests but not likely to work in +// the broader scope of the program! func BuildHtmlFile(in string, out string, settings *Settings) error { mdPre, err := util.ReadFile(in) if err != nil { From 2d3480e94d0d35fece3b291d6278145ad83dd466 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 11 Jan 2025 01:53:19 -0500 Subject: [PATCH 39/64] updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7f6e79c..dc6a675 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ bin/ go.sum # test/ +foobar/ From 1c05dd4a46989c0682629b579edee3f2896c2e26 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 11 Jan 2025 02:03:51 -0500 Subject: [PATCH 40/64] updated README update readme upd --- README.md | 77 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 066093e..bca9e60 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,56 @@ # Zona -Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles. +Zona is a tool for building a static website, optimized for lightweight blogs +following minimalist design principles. -**Warning:** Zona has not yet reached **v1**. The `dev-*` branches of this repository contain the code -- however, there is no assurance of stability or functionality until the first release. Configuration and usage documentation will also be provided at this time. - -## Table of Contents - -- [Design Goals](#design-goals) -- [v1 Features](#v1-features) -- [Roadmap](#roadmap) -- [Contribution](#contribution) -- [Inspirations](#inspirations) + +> [!NOTE] +> Zona is currently in development. The `main` branch of this repository +> does not yet contain the software. The `dev-stable` branch contains the code +> used to generate [ficd.ca](https://ficd.ca) -- although the program is +> undocumented and missing features, so please proceed at your own risk. The +> `dev` branch contains the latest development updates and is not guaranteed to +> be functional (or even compile) at any given commit. Kindly note that the +> commit history will be cleaned up before the program is merged into the `main` +> branch. + ## Design Goals -Zona is intended to be easy-to-use. A user should be able to build a reasonably complex website or blog with only a directory of Markdown content and a single command, without needing to write any HTML or configuration. However, users should optionally have access to sensible and flexible configuration options, including writing HTML. The output of Zona should also be lightweight, retaining the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages. +Zona is intended to be easy-to-use. A user should be able to build a reasonably +complex website or blog with only a directory of Markdown content and a single +command, without needing to write any HTML or configuration. However, users +should optionally have access to sensible and flexible configuration options, +including writing HTML. The output of Zona should also be lightweight, retaining +the smallest file sizes possible. These characteristics make Zona well-suited +for both beginners and power users that wish to host a website on a service like +Neocities or GitHub Pages. -## v1 Features +## Feature Progress -- Write pages purely in Markdown. -- Single-command build process. -- Lightweight output. -- Sensible default template and stylesheet. -- Configuration entirely optional, but very powerful. -- Site header and footer defined in Markdown. -- Declarative metadata per Markdown file. -- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements. -- Support for custom stylesheets, favicons, and page templates. - -## Roadmap - -- [ ] RSS/Atom feed generation. -- [ ] Image optimization & dithering. -- [ ] Windows, Mac, Linux releases. -- [ ] AUR package. -- [ ] Custom Markdown tags that expand to user-defined templates. -- [ ] Live preview local server. - -## Contribution - -Zona is a small project maintained by a very busy graduate student. If you want to contribute, you are more than welcome to submit issues and pull requests. +- [x] Write pages purely in Markdown. +- [x] Single-command build process. +- [x] Lightweight output. +- [x] Sensible default template and stylesheet. +- [x] Configuration file. +- [x] Internal links preserved. +- [x] Custom image element parsing & formatting. +- [x] Site header and footer defined in Markdown. +- [x] YAML frontmatter support. +- [ ] Automatically treat contents of `posts/` directory as blog posts. +- [ ] Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` [ ] + elements. +- [ ] Support for custom stylesheets, favicons, and page templates. +- [ ] Image optimization and dithering. +- [ ] Custom markdown tags that expand to user-defined templates. +- [ ] Live preview server. +- [ ] Robust tests. ## Inspirations - [Zoner](https://git.sr.ht/~ryantrawick/zoner) - [Zonelets](https://zonelets.net/) -> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I have never used it, nor read its documentation, thus it is not listed as an inspiration. +> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I +> have never used it, nor read its documentation, thus it is not listed as an +> inspiration. From 7644a310165c98b7c4e6a593c28b4927eff2d52f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 3 Feb 2025 21:13:20 -0500 Subject: [PATCH 41/64] added build processed files untested! --- cmd/zona/main.go | 2 +- internal/builder/build_page.go | 53 +++++++++++++++++++++++++++++++--- internal/builder/process.go | 16 +++++++++- internal/builder/traverse.go | 6 ++-- internal/util/file.go | 29 +++++++++++++++++++ 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/cmd/zona/main.go b/cmd/zona/main.go index 5409934..85a2e6b 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -43,7 +43,7 @@ func main() { } settings := builder.GetSettings(*rootPath, "foobar") // err := builder.Traverse(*rootPath, "foobar", settings) - err := builder.ProcessTraverse(*rootPath, "foobar", settings) + pm, err := builder.ProcessTraverse(*rootPath, "foobar", settings) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 02d4765..fc0be6d 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -2,6 +2,7 @@ package builder import ( "bytes" + "errors" "fmt" "html/template" "log" @@ -101,10 +102,7 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD return p } -// WARNING: This is a reference implementation -// with passing tests but not likely to work in -// the broader scope of the program! -func BuildHtmlFile(in string, out string, settings *Settings) error { +func _BuildHtmlFile(in string, out string, settings *Settings) error { mdPre, err := util.ReadFile(in) if err != nil { return err @@ -133,3 +131,50 @@ func BuildHtmlFile(in string, out string, settings *Settings) error { err = util.WriteFile(output.Bytes(), out) return err } + +func BuildFile(f *File, settings *Settings) error { + if f.ShouldCopy { + if err := util.CreateParents(f.OutPath); err != nil { + return err + } + if err := util.CopyFile(f.InPath, f.OutPath); err != nil { + return errors.Join(errors.New("Error processing file "+f.InPath), err) + } else { + return nil + } + } + + if err := util.CreateParents(f.OutPath); err != nil { + return err + } + if err := BuildHtmlFile(f.FrontMatterLen, f.InPath, f.OutPath, f.Data, settings); err != nil { + return errors.Join(errors.New("Error processing file "+f.InPath), err) + } else { + return nil + } +} + +func BuildHtmlFile(l int, in string, out string, pd *PageData, settings *Settings) error { + md, err := util.ReadLineRange(in, l, -1) + if err != nil { + return err + } + fmt.Println("Title: ", pd.Title) + + // build according to template here + html := MdToHTML(md) + pd.Content = template.HTML(html) + + tmpl, err := template.New("webpage").Parse(pd.Template) + if err != nil { + return err + } + + var output bytes.Buffer + if err := tmpl.Execute(&output, pd); err != nil { + return err + } + + err = util.WriteFile(output.Bytes(), out) + return err +} diff --git a/internal/builder/process.go b/internal/builder/process.go index 45de14a..b5dbfe4 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -27,6 +27,7 @@ type File struct { OutPath string ShouldCopy bool HasFrontmatter bool + FrontMatterLen int } // NewProcessMemory initializes an empty @@ -68,9 +69,11 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se var pd *PageData hasFrontmatter := false + l := 0 if toProcess { // process its frontmatter here - m, _, err := processFrontmatter(inPath) + m, le, err := processFrontmatter(inPath) + l = le if err != nil { return err } @@ -89,6 +92,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se outPath, !toProcess, hasFrontmatter, + l, } if pd != nil && pd.Type == "post" { pm.Posts = append(pm.Posts, file) @@ -96,3 +100,13 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se pm.Files = append(pm.Files, file) return nil } + +func BuildProcessedFiles(pm *ProcessMemory, settings *Settings) error { + for _, f := range pm.Files { + err := BuildFile(f, settings) + if err != nil { + return err + } + } + return nil +} diff --git a/internal/builder/traverse.go b/internal/builder/traverse.go index d397f39..9022fef 100644 --- a/internal/builder/traverse.go +++ b/internal/builder/traverse.go @@ -23,7 +23,7 @@ func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, sett if err := util.CreateParents(outPath); err != nil { return err } - if err := BuildHtmlFile(inPath, outPath, settings); err != nil { + if err := _BuildHtmlFile(inPath, outPath, settings); err != nil { return errors.Join(errors.New("Error processing file "+inPath), err) } else { return nil @@ -53,11 +53,11 @@ func Traverse(root string, outRoot string, settings *Settings) error { return err } -func ProcessTraverse(root string, outRoot string, settings *Settings) error { +func ProcessTraverse(root string, outRoot string, settings *Settings) (*ProcessMemory, error) { pm := NewProcessMemory() walkFunc := func(path string, entry fs.DirEntry, err error) error { return processFile(path, entry, err, outRoot, settings, pm) } err := filepath.WalkDir(root, walkFunc) - return err + return pm, err } diff --git a/internal/util/file.go b/internal/util/file.go index e164319..3e18f1d 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -80,3 +80,32 @@ func ReadNLines(filename string, n int) ([]byte, error) { return buffer.Bytes(), nil } + +// ReadLineRange reads a file in a given range of lines +func ReadLineRange(filename string, start int, end int) ([]byte, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var buffer bytes.Buffer + scanner := bufio.NewScanner(file) + i := 0 + for scanner.Scan() { + i++ + if i >= start && (i <= end || end == -1) { + buffer.Write(scanner.Bytes()) + buffer.WriteByte('\n') + } + if i > end && end != -1 { + break + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} From 46292005103aa5b0b249657f8765c450dd52adad Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 3 Feb 2025 21:13:46 -0500 Subject: [PATCH 42/64] added incomplete test --- internal/util/file_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/util/file_test.go b/internal/util/file_test.go index be3131d..df42d4f 100644 --- a/internal/util/file_test.go +++ b/internal/util/file_test.go @@ -35,3 +35,35 @@ func TestReadNLines(t *testing.T) { t.Errorf("Expected %q, got %q", expected, lines) } } + +func TestReadLineRange(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + filename string + start int + end int + want []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := ReadLineRange(tt.filename, tt.start, tt.end) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("ReadLineRange() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("ReadLineRange() succeeded unexpectedly") + } + // TODO: update the condition below to compare got with tt.want. + if true { + t.Errorf("ReadLineRange() = %v, want %v", got, tt.want) + } + }) + } +} From 0ecad9e96aed208a6d05eaeb4b773bf00be316fb Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 8 Feb 2025 00:11:28 -0500 Subject: [PATCH 43/64] fixed frontmatter processing, added test --- cmd/zona/main.go | 1 + internal/builder/build_page.go | 2 +- internal/builder/frontmatter.go | 9 ++- internal/builder/frontmatter_test.go | 92 ++++++++++++++++++++++++++++ internal/builder/process.go | 2 +- 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/cmd/zona/main.go b/cmd/zona/main.go index 85a2e6b..f892939 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -47,4 +47,5 @@ func main() { if err != nil { fmt.Printf("Error: %s\n", err.Error()) } + fmt.Printf("%#v", pm) } diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index fc0be6d..62e96da 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -147,7 +147,7 @@ func BuildFile(f *File, settings *Settings) error { if err := util.CreateParents(f.OutPath); err != nil { return err } - if err := BuildHtmlFile(f.FrontMatterLen, f.InPath, f.OutPath, f.Data, settings); err != nil { + if err := BuildHtmlFile(f.FrontMatterLen, f.InPath, f.OutPath, f.PageData, settings); err != nil { return errors.Join(errors.New("Error processing file "+f.InPath), err) } else { return nil diff --git a/internal/builder/frontmatter.go b/internal/builder/frontmatter.go index 73dfa89..e542234 100644 --- a/internal/builder/frontmatter.go +++ b/internal/builder/frontmatter.go @@ -42,9 +42,10 @@ func readFrontmatter(path string) ([]byte, int, error) { for s.Scan() { l := s.Text() if l == `---` { - if i > 0 && delims == 0 { + if i == 1 && delims == 0 { // if --- is not the first line, we // assume the file does not contain frontmatter + // fmt.Println("Delimiter first line") return nil, 0, nil } delims += 1 @@ -53,6 +54,9 @@ func readFrontmatter(path string) ([]byte, int, error) { break } } else { + if i == 0 { + return nil, 0, nil + } lines = append(lines, l) i += 1 } @@ -76,6 +80,7 @@ func readFrontmatter(path string) ([]byte, int, error) { } else { // not enough delimiters, don't // treat as frontmatter - return nil, 0, errors.New("frontmatter is missing closing delimiter") + s := fmt.Sprintf("%s: frontmatter is missing closing delimiter", path) + return nil, 0, errors.New(s) } } diff --git a/internal/builder/frontmatter_test.go b/internal/builder/frontmatter_test.go index 8661984..d6abf07 100644 --- a/internal/builder/frontmatter_test.go +++ b/internal/builder/frontmatter_test.go @@ -2,6 +2,7 @@ package builder import ( + "bytes" "os" "testing" ) @@ -89,3 +90,94 @@ This is the body of the document.` t.Fatalf("Expected error for invalid frontmatter, got nil") } } + +func TestReadFrontmatter(t *testing.T) { + tests := []struct { + name string + content string + wantErr bool + wantData []byte + wantLen int + }{ + { + name: "Valid frontmatter", + content: `--- +title: "Test" +author: "User" +--- +Content here`, + wantErr: false, + wantData: []byte("title: \"Test\"\nauthor: \"User\"\n"), + wantLen: 2, + }, + { + name: "Missing closing delimiter", + content: `--- +title: "Incomplete Frontmatter"`, + wantErr: true, + }, + { + name: "Frontmatter later in file", + content: `This is some content +--- +title: "Not Frontmatter" +---`, + wantErr: false, + wantData: nil, // Should return nil because `---` is not the first line + wantLen: 0, + }, + { + name: "Empty frontmatter", + content: `--- +---`, + wantErr: true, + }, + { + name: "No frontmatter", + content: `This is just a normal file.`, + wantErr: false, + wantData: nil, // Should return nil as there's no frontmatter + wantLen: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Create a temporary file + tmpFile, err := os.CreateTemp("", "testfile-*.md") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Write test content + _, err = tmpFile.WriteString(tc.content) + if err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } + tmpFile.Close() + + // Call function under test + data, length, err := readFrontmatter(tmpFile.Name()) + + // Check for expected error + if tc.wantErr { + if err == nil { + t.Errorf("expected error but got none") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + // Check content + if !bytes.Equal(data, tc.wantData) { + t.Errorf("expected %q, got %q", tc.wantData, data) + } + // Check length + if length != tc.wantLen { + t.Errorf("expected length %d, got %d", tc.wantLen, length) + } + } + }) + } +} diff --git a/internal/builder/process.go b/internal/builder/process.go index b5dbfe4..1f2e2c0 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -21,7 +21,7 @@ type ProcessMemory struct { } type File struct { - Data *PageData + PageData *PageData Ext string InPath string OutPath string From 65b62ef9a653249e1c4791f89f7e141436f4521e Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 8 Feb 2025 00:15:51 -0500 Subject: [PATCH 44/64] added todo tracker --- TODO.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..788e1d4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +# TODO + +**Last working on:** I fixed the frontmatter processing; now I should verify that the entire directory is being processed okay. + +## Thoroughly test directory processing + +- Is the pagedata being constructed as expected? +- Is the processmemory struct working as expected? + +NOTE: I should really write these as actual tests, not just running tests on my testing directory myself. + From 116fb6a8835d92dfd5b75436757b1d87709c5c84 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 18 Mar 2025 00:15:59 -0400 Subject: [PATCH 45/64] restore previous functionality with new processing system --- TODO.md | 4 +++- cmd/zona/main.go | 8 ++++++++ internal/builder/process.go | 4 +++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 788e1d4..4d1232e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,8 @@ # TODO -**Last working on:** I fixed the frontmatter processing; now I should verify that the entire directory is being processed okay. +- Fix the relative URL situation in the headers + - The link that's defined in header file should be relative to root, not the page being processed. + - How to handle this? ## Thoroughly test directory processing diff --git a/cmd/zona/main.go b/cmd/zona/main.go index f892939..e1d8a59 100644 --- a/cmd/zona/main.go +++ b/cmd/zona/main.go @@ -43,9 +43,17 @@ func main() { } settings := builder.GetSettings(*rootPath, "foobar") // err := builder.Traverse(*rootPath, "foobar", settings) + // traverse the source and process file metadata pm, err := builder.ProcessTraverse(*rootPath, "foobar", settings) if err != nil { fmt.Printf("Error: %s\n", err.Error()) + os.Exit(1) } + err = builder.BuildProcessedFiles(pm, settings) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + os.Exit(1) + } + fmt.Printf("%#v", pm) } diff --git a/internal/builder/process.go b/internal/builder/process.go index 1f2e2c0..b46cedb 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -53,7 +53,9 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se var toProcess bool var outPath string var ext string - if !entry.IsDir() { + if entry.IsDir() { + return nil + } else { ext = filepath.Ext(inPath) outPath = util.ReplaceRoot(inPath, outRoot) // NOTE: This could be an if statement, but keeping From ea286868b70645ff96cc9fa936c499197f80e84c Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 23 Mar 2025 22:49:28 -0400 Subject: [PATCH 46/64] updated readme --- README.md | 70 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index bca9e60..fae69c8 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ # Zona -Zona is a tool for building a static website, optimized for lightweight blogs -following minimalist design principles. +[Zona](https://sr.ht/~ficd/zona/) is a tool for building a static website, +optimized for lightweight blogs following minimalist design principles. The +project is hosted on [sourcehut](https://sr.ht/~ficd/zona/) and mirrored on +[GitHub](https://github.com/ficcdaf/zona). You are welcome to open an issue on +GitHub or send a message to the +[mailing list](https://lists.sr.ht/~ficd/zona-devel). -> [!NOTE] -> Zona is currently in development. The `main` branch of this repository -> does not yet contain the software. The `dev-stable` branch contains the code -> used to generate [ficd.ca](https://ficd.ca) -- although the program is -> undocumented and missing features, so please proceed at your own risk. The -> `dev` branch contains the latest development updates and is not guaranteed to -> be functional (or even compile) at any given commit. Kindly note that the -> commit history will be cleaned up before the program is merged into the `main` -> branch. + +> [!NOTE] +> Zona is currently in development. The `main` branch of this repository does +> not yet contain the software. The `dev-stable` branch contains the code used +> to generate [ficd.ca](https://ficd.ca) -- although the program is undocumented +> and missing features, so please proceed at your own risk. The `dev` branch +> contains the latest development updates and is not guaranteed to be functional +> (or even compile) at any given commit. Kindly note that the commit history +> will be cleaned up before the program is merged into the `main` branch. + ## Design Goals @@ -26,31 +31,30 @@ the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages. -## Feature Progress +## Features Implemented -- [x] Write pages purely in Markdown. -- [x] Single-command build process. -- [x] Lightweight output. -- [x] Sensible default template and stylesheet. -- [x] Configuration file. -- [x] Internal links preserved. -- [x] Custom image element parsing & formatting. -- [x] Site header and footer defined in Markdown. -- [x] YAML frontmatter support. -- [ ] Automatically treat contents of `posts/` directory as blog posts. -- [ ] Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` [ ] - elements. -- [ ] Support for custom stylesheets, favicons, and page templates. -- [ ] Image optimization and dithering. -- [ ] Custom markdown tags that expand to user-defined templates. -- [ ] Live preview server. -- [ ] Robust tests. +- Write pages purely in Markdown. +- Single-command build process. +- Lightweight output. +- Sensible default template and stylesheet. +- Configuration file. +- Internal links preserved. +- Custom image element parsing & formatting. +- Site header and footer defined in Markdown. +- YAML frontmatter support. + +## Planned Features + +- Automatically treat contents of `posts/` directory as blog posts. +- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` + elements. +- Support for custom stylesheets, favicons, and page templates. +- Image optimization and dithering. +- Custom markdown tags that expand to user-defined templates. +- Live preview server. +- Robust tests. ## Inspirations - [Zoner](https://git.sr.ht/~ryantrawick/zoner) - [Zonelets](https://zonelets.net/) - -> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I -> have never used it, nor read its documentation, thus it is not listed as an -> inspiration. From fdb8753538a44ab8cec51a73fc495681c7b464a2 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 24 Mar 2025 00:44:36 -0400 Subject: [PATCH 47/64] fix: metadata no longer rendered as part of page content --- internal/builder/build_page.go | 12 ++++++++++-- internal/util/file.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 62e96da..e0efc28 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -28,7 +28,7 @@ type PageData struct { Type string } -type Metadata map[string]interface{} +type Metadata map[string]any func processWithYaml(f []byte) (Metadata, []byte, error) { // Check if the file has valid metadata @@ -155,7 +155,15 @@ func BuildFile(f *File, settings *Settings) error { } func BuildHtmlFile(l int, in string, out string, pd *PageData, settings *Settings) error { - md, err := util.ReadLineRange(in, l, -1) + // WARN: ReadLineRange is fine, but l is the len of the frontmatter + // NOT including the delimiters! + start := l + // if the frontmatter exists (len > 0), then we need to + // account for two lines of delimiter! + if l != 0 { + start += 2 + } + md, err := util.ReadLineRange(in, start, -1) if err != nil { return err } diff --git a/internal/util/file.go b/internal/util/file.go index 3e18f1d..4570eb2 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -93,7 +93,6 @@ func ReadLineRange(filename string, start int, end int) ([]byte, error) { scanner := bufio.NewScanner(file) i := 0 for scanner.Scan() { - i++ if i >= start && (i <= end || end == -1) { buffer.Write(scanner.Bytes()) buffer.WriteByte('\n') @@ -101,6 +100,7 @@ func ReadLineRange(filename string, start int, end int) ([]byte, error) { if i > end && end != -1 { break } + i++ } if err := scanner.Err(); err != nil { From 988d4ba42e3f3f3f9160c033104f7cd85794e304 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Fri, 4 Apr 2025 23:53:47 -0400 Subject: [PATCH 48/64] added proper output directory structure --- internal/builder/process.go | 4 +++- internal/util/path.go | 14 ++++++++++++++ internal/util/path_test.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 internal/util/path_test.go diff --git a/internal/builder/process.go b/internal/builder/process.go index b46cedb..7108668 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -57,15 +57,17 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se return nil } else { ext = filepath.Ext(inPath) - outPath = util.ReplaceRoot(inPath, outRoot) // NOTE: This could be an if statement, but keeping // the switch makes it easy to extend the logic here later switch ext { case ".md": toProcess = true + outPath = util.ReplaceRoot(inPath, outRoot) outPath = util.ChangeExtension(outPath, ".html") + outPath = util.Indexify(outPath) default: toProcess = false + outPath = util.ReplaceRoot(inPath, outRoot) } } diff --git a/internal/util/path.go b/internal/util/path.go index 02b4b7b..6c95cfc 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -22,6 +22,7 @@ func ChangeExtension(in string, outExt string) string { return strings.TrimSuffix(in, filepath.Ext(in)) + outExt } +// TODO: look for .zona.yml instead? func getRoot(path string) string { for { parent := filepath.Dir(path) @@ -40,6 +41,19 @@ func ReplaceRoot(inPath, outRoot string) string { return outPath } +// Indexify converts format path/file.ext +// into path/file/index.ext +func Indexify(in string) string { + ext := filepath.Ext(in) + trimmed := strings.TrimSuffix(in, ext) + filename := filepath.Base(trimmed) + if filename == "index" { + return in + } + prefix := strings.TrimSuffix(trimmed, filename) + return filepath.Join(prefix, filename, "index"+ext) +} + // InDir checks whether checkPath is // inside targDir. func InDir(checkPath string, targDir string) bool { diff --git a/internal/util/path_test.go b/internal/util/path_test.go new file mode 100644 index 0000000..ccac2fc --- /dev/null +++ b/internal/util/path_test.go @@ -0,0 +1,35 @@ +package util_test + +import ( + "testing" + + "github.com/ficcdaf/zona/internal/util" +) + +func TestIndexify(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + in string + want string + }{ + { + "Simple Path", + "foo/bar/name.html", + "foo/bar/name/index.html", + }, + { + "Index Name", + "foo/bar/index.md", + "foo/bar/index.md", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := util.Indexify(tt.in) + if got != tt.want { + t.Errorf("Indexify() = %v, want %v", got, tt.want) + } + }) + } +} From 18800e1cd8544039f6792fdb19c560bd19197e15 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 23 Mar 2025 22:51:21 -0400 Subject: [PATCH 49/64] Updated readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fae69c8..d1f6286 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ [Zona](https://sr.ht/~ficd/zona/) is a tool for building a static website, optimized for lightweight blogs following minimalist design principles. The project is hosted on [sourcehut](https://sr.ht/~ficd/zona/) and mirrored on -[GitHub](https://github.com/ficcdaf/zona). You are welcome to open an issue on -GitHub or send a message to the -[mailing list](https://lists.sr.ht/~ficd/zona-devel). +[GitHub](https://github.com/ficcdaf/zona). I am not accepting GitHub issues, +please make your submission to the [issue tracker] or send an email to the +public mailing list at +[~ficd/zona-devel@lists.sr.ht](mailto:~ficd/zona-devel@lists.sr.ht) -> [!NOTE] > Zona is currently in development. The `main` branch of this repository does > not yet contain the software. The `dev-stable` branch contains the code used > to generate [ficd.ca](https://ficd.ca) -- although the program is undocumented From c797c0e85d87ca312c322f58294efdba7ef68ab5 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 14:01:20 -0400 Subject: [PATCH 50/64] fixed missing link in readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d1f6286..589d721 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ optimized for lightweight blogs following minimalist design principles. The project is hosted on [sourcehut](https://sr.ht/~ficd/zona/) and mirrored on [GitHub](https://github.com/ficcdaf/zona). I am not accepting GitHub issues, -please make your submission to the [issue tracker] or send an email to the -public mailing list at +please make your submission to the +[issue tracker](https://todo.sr.ht/~ficd/zona) or send an email to the public +mailing list at [~ficd/zona-devel@lists.sr.ht](mailto:~ficd/zona-devel@lists.sr.ht) From fdec4b6f25e0b7e711743832e0778e7f3f3873d1 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 16:24:51 -0400 Subject: [PATCH 51/64] bumped dependencies and go version --- go.mod | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 9b7a11c..8f10aa7 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ module github.com/ficcdaf/zona -// go 1.23.2 -go 1.23.4 +go 1.24.2 require ( - github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 + github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/text v0.21.0 +require golang.org/x/text v0.23.0 From 4b62ed116edb327b4ba3bdfc02feb1bc2b6d825d Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 16:29:49 -0400 Subject: [PATCH 52/64] renamed config file --- internal/builder/config.go | 4 ++-- internal/builder/embed/{config.yml => .zona.yml} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename internal/builder/embed/{config.yml => .zona.yml} (100%) diff --git a/internal/builder/config.go b/internal/builder/config.go index 831ec01..5d960e5 100644 --- a/internal/builder/config.go +++ b/internal/builder/config.go @@ -11,7 +11,7 @@ import ( ) var defaultNames = map[string]string{ - "config": "config.yml", + "config": ".zona.yml", "header": "header.md", "footer": "footer.md", "style": "style.css", @@ -22,7 +22,7 @@ var defaultNames = map[string]string{ } //go:embed embed/article.html -//go:embed embed/config.yml +//go:embed embed/.zona.yml //go:embed embed/default.html //go:embed embed/favicon.png //go:embed embed/footer.md diff --git a/internal/builder/embed/config.yml b/internal/builder/embed/.zona.yml similarity index 100% rename from internal/builder/embed/config.yml rename to internal/builder/embed/.zona.yml From c26387990469d25ce53d3e0e33395895e8ca9934 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 16:51:38 -0400 Subject: [PATCH 53/64] added justfile --- justfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 justfile diff --git a/justfile b/justfile new file mode 100644 index 0000000..9543699 --- /dev/null +++ b/justfile @@ -0,0 +1,12 @@ +test: + go test ./... + +gentest: + #!/bin/bash + if [ -e foobar ]; then + rm -rf foobar + fi + + go run cmd/zona/main.go test + + # bat foobar/img.html From 59ead0f26a3dd380dfbddfe10d5814c0ae5f9ba1 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 17:17:30 -0400 Subject: [PATCH 54/64] treat config file as site root --- internal/util/path.go | 14 +++++++++----- test/.zona.yml | 0 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 test/.zona.yml diff --git a/internal/util/path.go b/internal/util/path.go index 6c95cfc..1d9a740 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -22,17 +22,21 @@ func ChangeExtension(in string, outExt string) string { return strings.TrimSuffix(in, filepath.Ext(in)) + outExt } -// TODO: look for .zona.yml instead? +// find the root. check for a .zona.yml first, +// then check if it's cwd. func getRoot(path string) string { + marker := ".zona.yml" for { + // fmt.Printf("check for: %s\n", candidate) parent := filepath.Dir(path) - if parent == "." { - break + candidate := filepath.Join(parent, marker) + if FileExists(candidate) { + return parent + } else if parent == "." { + return path } path = parent } - // fmt.Println("getRoot: ", path) - return path } func ReplaceRoot(inPath, outRoot string) string { diff --git a/test/.zona.yml b/test/.zona.yml new file mode 100644 index 0000000..e69de29 From bdd9e63fdfef14f981e07a5329041d98258bb29b Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 17:57:15 -0400 Subject: [PATCH 55/64] working on path normalizer --- internal/builder/build_page.go | 7 ++++++- internal/util/path.go | 25 +++++++++++++++++++++++++ test/posts/in.md | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index e0efc28..3a45dfe 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -59,7 +59,12 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD p.Title = util.PathToTitle(in) } if icon, ok := m["icon"].(string); ok { - p.Icon = icon + i, err := util.NormalizePath(icon) + if err != nil { + p.Icon = settings.IconName + } else { + p.Icon = i + } } else { p.Icon = settings.IconName } diff --git a/internal/util/path.go b/internal/util/path.go index 1d9a740..da85642 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -3,6 +3,7 @@ package util import ( "errors" + "fmt" "os" "path/filepath" "strings" @@ -108,3 +109,27 @@ func StripTopDir(path string) string { } return filepath.Join(components[1:]...) } + +// we want to preserve a valid web-style path +// and convert relative path to web-style +// so we need to see +// TODO; use Rel function to get abs path between +// the file being analyzed's path, and what lil bro +// is pointing to +func NormalizePath(path string) (string, error) { + // empty path is root + if path == "" { + return "/", nil + } + if path[0] == '.' { + fmt.Println("Local path detected...") + abs, err := filepath.Abs(path) + fmt.Printf("abs: %s\n", abs) + if err != nil { + return "", fmt.Errorf("Couldn't normalize path: %w", err) + } + return ReplaceRoot(abs, "/"), nil + } else { + return path, nil + } +} diff --git a/test/posts/in.md b/test/posts/in.md index 57171ba..fc679ea 100644 --- a/test/posts/in.md +++ b/test/posts/in.md @@ -1,5 +1,6 @@ --- title: tiltetest +icon: ../assets/pic.png --- # My amazing markdown file! From d31fc4b1c42c767703a9be07295a0172c7065a8c Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 8 Apr 2025 16:40:01 -0400 Subject: [PATCH 56/64] added build manifest for github mirroring --- .build.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .build.yml diff --git a/.build.yml b/.build.yml new file mode 100644 index 0000000..15d2cd9 --- /dev/null +++ b/.build.yml @@ -0,0 +1,16 @@ +image: alpine/latest +packages: + - git + - openssh +secrets: + - 0639564d-6995-4e2e-844b-2f8feb0b7fb1 +environment: + repo: zona + github: git@github.com:ficcdaf/zona.git +tasks: + - mirror: | + ssh-keyscan github.com >> ~/.ssh/known_hosts + cd "$repo" + git remote add github "$github" + git push --mirror github + From af2f071a5af759d2b190960ee6374db9464446bb Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 8 Apr 2025 16:44:37 -0400 Subject: [PATCH 57/64] added ci for github mirroring --- .build.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .build.yml diff --git a/.build.yml b/.build.yml new file mode 100644 index 0000000..7546403 --- /dev/null +++ b/.build.yml @@ -0,0 +1,17 @@ +image: alpine/latest +packages: + - git + - openssh +secrets: + - 0639564d-6995-4e2e-844b-2f8feb0b7fb1 +environment: + repo: zona + github: git@github.com:ficcdaf/zona.git +tasks: + - mirror: | + ssh-keyscan github.com >> ~/.ssh/known_hosts + cd "$repo" + git remote add github "$github" + git push --mirror github + + From 577eeeab2d1164555448dfd8b894527d8825de41 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 10 Apr 2025 16:44:48 -0400 Subject: [PATCH 58/64] updated todo --- TODO.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 4d1232e..653b13a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,22 @@ # TODO - Fix the relative URL situation in the headers - - The link that's defined in header file should be relative to root, not the page being processed. + - The link that's defined in header file should be relative to root, not the + page being processed. - How to handle this? +- Syntax highlighting for code blocks +- Implement zola-style directory structure + - `templates`, `content`, `static`? + - Paths in page metadata should start at these folders + - What about markdown links to internal pages? + - Steal Zola's syntax? + - Link starting with `@` → `content/` +- Implement zola-style sections with `_index.md` ## Thoroughly test directory processing - Is the pagedata being constructed as expected? - Is the processmemory struct working as expected? -NOTE: I should really write these as actual tests, not just running tests on my testing directory myself. - +NOTE: I should really write these as actual tests, not just running tests on my +testing directory myself. From c0429fa92b7294a06a761760597eeefe5f205be6 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Fri, 11 Apr 2025 15:46:43 -0400 Subject: [PATCH 59/64] updated todo --- TODO.md | 67 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/TODO.md b/TODO.md index 653b13a..6fd4ca4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,22 +1,49 @@ -# TODO +# TO-DO -- Fix the relative URL situation in the headers - - The link that's defined in header file should be relative to root, not the - page being processed. - - How to handle this? -- Syntax highlighting for code blocks -- Implement zola-style directory structure - - `templates`, `content`, `static`? - - Paths in page metadata should start at these folders +- Refactor the directory structure processing + - Implement zola-style structure instead + - `zona init` command to populate the required files, _with_ defaults + (unlike zola) + - Interactive for setting values, also an option to create `.gitignore` + with `public` in it. + - `zona.yml` is **required** and should mark the root: + - `templates`, `content`, `static`, `zona.yml` + - multiple `zona.yml` files should be an error + - if the folder containing `zona.yml` doesn't contain _exactly_ the + expected directories and files, it's an error + - Paths in page metadata should start at these folders + - i.e. `(template|footer|header): name.html` → `root/templates/name.html` + - `(style|icon): name.ext` → `root/static/name.ext` + - Traverse `content` and `static` separately, applying different rules + - everything in `static/**` should be directly copied + - `content/**` should be processed + - `*.md` converted, everything else copied directly + - `./name.md` → ./name/index.html + - Either `./name.md` or `./name/index.md` are valid, _together_ they + cause an error! - What about markdown links to internal pages? - - Steal Zola's syntax? - - Link starting with `@` → `content/` -- Implement zola-style sections with `_index.md` - -## Thoroughly test directory processing - -- Is the pagedata being constructed as expected? -- Is the processmemory struct working as expected? - -NOTE: I should really write these as actual tests, not just running tests on my -testing directory myself. + - Relative links should be supported to play nice with LSP + - in case of relative link, zona should attempt to resolve it, figuring + out which file it's pointing to, and convert it to a `/` prefixed link + pointing to appropriate place + - so `../blog/a-post.md` → `/blog/a-post` where `/blog/a-post/index.html` + exists + - links from project root should also be supported + - check link validity at build time and supply warning + - _tl;dr_ all links should be resolved to the absolute path to that resource + starting from the website root. that's the link that should actually be + written to the HTML. +- Re-consider what `zona.yml` should have in it. + - Set syntax highlighting theme here + - a string that's not a file path: name of any built-in theme in + [chroma](https://github.com/alecthomas/chroma) + - path to `xml` _or_ `yml` file: custom theme for passing to chroma + - if `xml`, pass directly + - if `yml`, parse and convert into expected `xml` format before passing + - Set website root URL here + - toggle option for zona's custom image label expansion, image container div, + etc, basically all the custom rendering stuff +- Syntax highlighting for code blocks +- Add `zona serve` command with local dev server to preview the site +- Both `zona build` and `zona serve` should output warning and error +- Write actual unit tests! From 8895ba969c492c85e2035809c5011c4831630a59 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 3 May 2025 00:00:44 -0400 Subject: [PATCH 60/64] added comments to recipes --- justfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/justfile b/justfile index 9543699..39f5308 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,8 @@ +# run go tests test: go test ./... +# test outputs gentest: #!/bin/bash if [ -e foobar ]; then From 99bc12857829adbf75676759f79e5535b0683f60 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 3 May 2025 00:00:16 -0400 Subject: [PATCH 61/64] fixed relative path normalizing --- internal/builder/build_page.go | 2 +- internal/util/path.go | 35 +++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 3a45dfe..2a0fe04 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -59,7 +59,7 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD p.Title = util.PathToTitle(in) } if icon, ok := m["icon"].(string); ok { - i, err := util.NormalizePath(icon) + i, err := util.NormalizePath(icon, in) if err != nil { p.Icon = settings.IconName } else { diff --git a/internal/util/path.go b/internal/util/path.go index da85642..2442f87 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -28,9 +28,12 @@ func ChangeExtension(in string, outExt string) string { func getRoot(path string) string { marker := ".zona.yml" for { - // fmt.Printf("check for: %s\n", candidate) parent := filepath.Dir(path) + if parent == "/" { + panic(1) + } candidate := filepath.Join(parent, marker) + // fmt.Printf("check for: %s\n", candidate) if FileExists(candidate) { return parent } else if parent == "." { @@ -110,26 +113,28 @@ func StripTopDir(path string) string { return filepath.Join(components[1:]...) } +func resolveRelativeTo(relPath string, basePath string) string { + baseDir := filepath.Dir(basePath) + combined := filepath.Join(baseDir, relPath) + resolved := filepath.Clean(combined) + return resolved +} + // we want to preserve a valid web-style path // and convert relative path to web-style // so we need to see -// TODO; use Rel function to get abs path between -// the file being analyzed's path, and what lil bro -// is pointing to -func NormalizePath(path string) (string, error) { +func NormalizePath(target string, source string) (string, error) { + fmt.Printf("normalizing: %s\n", target) // empty path is root - if path == "" { + if target == "" { return "/", nil } - if path[0] == '.' { - fmt.Println("Local path detected...") - abs, err := filepath.Abs(path) - fmt.Printf("abs: %s\n", abs) - if err != nil { - return "", fmt.Errorf("Couldn't normalize path: %w", err) - } - return ReplaceRoot(abs, "/"), nil + if target[0] == '.' { + resolved := resolveRelativeTo(target, source) + normalized := ReplaceRoot(resolved, "/") + fmt.Printf("Normalized: %s\n", normalized) + return normalized, nil } else { - return path, nil + return target, nil } } From 3355bc5544ab513fe2735b85aa343a02c1aae5b7 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 3 May 2025 00:17:34 -0400 Subject: [PATCH 62/64] added proper frontmatter struct --- internal/builder/build_page.go | 40 ++++++++++++++++++++------------- internal/builder/frontmatter.go | 6 ++--- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 2a0fe04..4fa9b3b 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -30,7 +30,16 @@ type PageData struct { type Metadata map[string]any -func processWithYaml(f []byte) (Metadata, []byte, error) { +type FrontMatter struct { + Title string `yaml:"title"` + Icon string `yaml:"icon"` + Style string `yaml:"style"` + Header string `yaml:"header"` + Footer string `yaml:"footer"` + Type string `yaml:"type"` +} + +func processWithYaml(f []byte) (*FrontMatter, []byte, error) { // Check if the file has valid metadata trimmed := bytes.TrimSpace(f) normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n") @@ -43,23 +52,23 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { if len(split) < 3 { return nil, nil, fmt.Errorf("invalid frontmatter format") } - var meta Metadata + var meta FrontMatter // Parse YAML if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil { return nil, nil, err } - return meta, []byte(split[2]), nil + return &meta, []byte(split[2]), nil } -func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData { +func buildPageData(m *FrontMatter, in string, out string, settings *Settings) *PageData { p := &PageData{} - if title, ok := m["title"].(string); ok { - p.Title = util.WordsToTitle(title) + if m != nil && m.Title != "" { + p.Title = util.WordsToTitle(m.Title) } else { p.Title = util.PathToTitle(in) } - if icon, ok := m["icon"].(string); ok { - i, err := util.NormalizePath(icon, in) + if m != nil && m.Icon != "" { + i, err := util.NormalizePath(m.Icon, in) if err != nil { p.Icon = settings.IconName } else { @@ -69,8 +78,8 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD p.Icon = settings.IconName } var stylePath string - if style, ok := m["style"].(string); ok { - stylePath = style + if m != nil && m.Style != "" { + stylePath = m.Style } else { stylePath = settings.StylePath } @@ -81,23 +90,24 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD log.Fatalln("Error calculating stylesheet path: ", err) } p.Stylesheet = relPath - if header, ok := m["header"].(string); ok { - p.HeaderName = header + + if m != nil && m.Header != "" { + p.HeaderName = m.Header // for now we use default anyways p.Header = settings.Header } else { p.HeaderName = settings.HeaderName p.Header = settings.Header } - if footer, ok := m["footer"].(string); ok { - p.FooterName = footer + if m != nil && m.Footer != "" { + p.FooterName = m.Footer p.Footer = settings.Footer } else { p.FooterName = settings.FooterName p.Footer = settings.Footer } // TODO: Don't hard code posts dir name - if t, ok := m["type"].(string); util.InDir(in, "posts") && !ok || (ok && t == "article" || t == "post") { + if (m != nil && (m.Type == "article" || m.Type == "post")) || util.InDir(in, "posts") { p.Template = (settings.ArticleTemplate) p.Type = "post" } else { diff --git a/internal/builder/frontmatter.go b/internal/builder/frontmatter.go index e542234..b25a1b0 100644 --- a/internal/builder/frontmatter.go +++ b/internal/builder/frontmatter.go @@ -10,17 +10,17 @@ import ( "gopkg.in/yaml.v3" ) -func processFrontmatter(p string) (Metadata, int, error) { +func processFrontmatter(p string) (*FrontMatter, int, error) { f, l, err := readFrontmatter(p) if err != nil { return nil, l, err } - var meta Metadata + var meta FrontMatter // Parse YAML if err := yaml.Unmarshal(f, &meta); err != nil { return nil, l, fmt.Errorf("yaml frontmatter could not be parsed: %w", err) } - return meta, l, nil + return &meta, l, nil } // readFrontmatter reads the file at `path` and scans From 9a94052b42cb0fe8377d9754444ce2de074fac4d Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 15 Jun 2025 19:16:16 -0400 Subject: [PATCH 63/64] updated todo --- TODO.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.md b/TODO.md index 6fd4ca4..005efcf 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,8 @@ # TO-DO +- **First**, re-write the settings & configuration system from scratch! It's + broken and messy and not worth trying to fix like this. Instead, actually + architect how it should work, _then_ implement it. - Refactor the directory structure processing - Implement zola-style structure instead - `zona init` command to populate the required files, _with_ defaults From da9589ec5711d8fe6159e96756a1cb6faedc1509 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 15 Jun 2025 19:19:29 -0400 Subject: [PATCH 64/64] added rewrite notice to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 589d721..5de03c3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Zona +**IMPORTANT:** Zona is currently migrating to a Python implementation. The new +repository is called [zona-py](https://git.sr.ht/~ficd/zona-py). **It will +replace this repository once the migration is complete.** + [Zona](https://sr.ht/~ficd/zona/) is a tool for building a static website, optimized for lightweight blogs following minimalist design principles. The project is hosted on [sourcehut](https://sr.ht/~ficd/zona/) and mirrored on