From fb67ef046a955d81717bde8b7f561dca9acce3a3 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sun, 29 Dec 2024 20:06:57 -0500 Subject: [PATCH 01/31] 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 02/31] 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 03/31] 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 04/31] 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 05/31] 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 06/31] 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 07/31] 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 08/31] 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 09/31] 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 10/31] 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 11/31] 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 7644a310165c98b7c4e6a593c28b4927eff2d52f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 3 Feb 2025 21:13:20 -0500 Subject: [PATCH 12/31] 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 13/31] 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 14/31] 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 15/31] 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 16/31] 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 fdb8753538a44ab8cec51a73fc495681c7b464a2 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Mon, 24 Mar 2025 00:44:36 -0400 Subject: [PATCH 17/31] 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 18/31] 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 fdec4b6f25e0b7e711743832e0778e7f3f3873d1 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 5 Apr 2025 16:24:51 -0400 Subject: [PATCH 19/31] 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 20/31] 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 21/31] 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 22/31] 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 23/31] 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 9766fb01e328c0fa474c7bb66baf37bd5386e661 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 8 Apr 2025 16:44:12 -0400 Subject: [PATCH 24/31] added CI --- .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 af2f071a5af759d2b190960ee6374db9464446bb Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 8 Apr 2025 16:44:37 -0400 Subject: [PATCH 25/31] 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 26/31] 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 27/31] 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 28/31] 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 29/31] 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 30/31] 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 31/31] 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