From 73bbaa2c5046dc6c400aea559ccfa96f3850a1e6 Mon Sep 17 00:00:00 2001 From: facundoolano Date: Sun, 11 Feb 2024 13:16:10 -0300 Subject: [PATCH] Add a templates package and struct Squashed commit of the following: commit 7c5b6bf95b14e402b68b141a7d60ccb1468f12b3 Author: facundoolano Date: Sun Feb 11 13:15:08 2024 -0300 restore other tests commit 5cf5c43856fc1a9e8f23dc74b81607ab7387f4c3 Author: facundoolano Date: Sat Feb 10 23:13:31 2024 -0300 restore a test commit acca0936a42b8b915c25f96c6b435887d7235c23 Author: facundoolano Date: Sat Feb 10 22:52:41 2024 -0300 fix a bunch of bugs commit 6f8074402338194ebebaaf929a1d85fdbf0d5e22 Author: facundoolano Date: Sat Feb 10 22:00:43 2024 -0300 implement methods commit 5cfeb1ea8600317d8849c6dca63a009237e033af Author: facundoolano Date: Sat Feb 10 20:16:10 2024 -0300 add template package and struct commit 7a7b79e006ff6629cbf9445927e84e9c1600667b Author: facundoolano Date: Sat Feb 10 20:08:43 2024 -0300 stub template interface --- commands/commands.go | 60 ++++++++++++++---- commands/templates.go | 102 ------------------------------ commands/templates_test.go | 89 -------------------------- main.go | 9 ++- templates/templates.go | 105 +++++++++++++++++++++++++++++++ templates/templates_test.go | 120 ++++++++++++++++++++++++++++++++++++ 6 files changed, 277 insertions(+), 208 deletions(-) delete mode 100644 commands/templates.go delete mode 100644 commands/templates_test.go create mode 100644 templates/templates.go create mode 100644 templates/templates_test.go diff --git a/commands/commands.go b/commands/commands.go index ebe1bd8..3678e2b 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -3,9 +3,13 @@ package commands import ( "errors" "fmt" + "io" "io/fs" "os" "path/filepath" + "strings" + + "github.com/facundoolano/blorg/templates" ) func Init() error { @@ -36,24 +40,34 @@ func Build() error { // render each source file and copy it over to target err = filepath.WalkDir("src", func(path string, entry fs.DirEntry, err error) error { + subpath, _ := filepath.Rel("src", path) + targetPath := filepath.Join("target", subpath) + if entry.IsDir() { - subpath, _ := filepath.Rel("src", path) - targetSubpath := filepath.Join("target", subpath) - os.MkdirAll(targetSubpath, FILE_MODE) + os.MkdirAll(targetPath, FILE_MODE) } else { - - // FIXME what if non text file? - data, targetPath, err := render(path) + template, err := templates.Parse(path) if err != nil { - return fmt.Errorf("failed to render %s", path) + return err } - // write the file contents over to target at the same location - err = os.WriteFile(targetPath, []byte(data), FILE_MODE) - if err != nil { - return fmt.Errorf("failed to load %s", targetPath) + if template != nil { + // if a template was found at source, render it + targetPath = strings.TrimSuffix(targetPath, filepath.Ext(targetPath)) + template.Ext() + + content, err := template.Render() + if err != nil { + return err + } + + // write the file contents over to target at the same location + fmt.Println("writing ", targetPath) + return os.WriteFile(targetPath, content, FILE_MODE) + } else { + // if a non template was found, copy file as is + fmt.Println("writing ", targetPath) + return copyFile(path, targetPath) } - fmt.Printf("wrote %v\n", targetPath) } return nil @@ -62,6 +76,28 @@ func Build() error { return err } +func copyFile(source string, target string) error { + // does this need to be so verbose? + srcFile, err := os.Open(source) + if err != nil { + return err + } + defer srcFile.Close() + + targetFile, _ := os.Create(target) + if err != nil { + return err + } + defer targetFile.Close() + + _, err = io.Copy(targetFile, srcFile) + if err != nil { + return err + } + + return targetFile.Sync() +} + func New() error { // prompt for title // slugify diff --git a/commands/templates.go b/commands/templates.go deleted file mode 100644 index c88a6a7..0000000 --- a/commands/templates.go +++ /dev/null @@ -1,102 +0,0 @@ -// TODO consider making this another package -package commands - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/niklasfasching/go-org/org" - "gopkg.in/yaml.v3" -) - -// TODO move elsewhere? -// TODO doc -func render(sourcePath string) (string, string, error) { - // FIXME remove src target knowledge - subpath, _ := filepath.Rel("src", sourcePath) - targetPath := filepath.Join("target", subpath) - isOrgFile := filepath.Ext(sourcePath) == ".org" - if isOrgFile { - targetPath = strings.TrimSuffix(targetPath, "org") + "html" - } - - file, err := os.Open(sourcePath) - if err != nil { - return "", "", err - } - defer file.Close() - - fileContent, _, err := extractFrontMatter(file) - if err != nil { - return "", "", (fmt.Errorf("error in %s: %s", sourcePath, err)) - } - - var html string - // FIXME this should be renamed to .html - // (in general, the render process should be able to instruct a differnt target path) - if isOrgFile { - doc := org.New().Parse(bytes.NewReader(fileContent), sourcePath) - html, err = doc.Write(org.NewHTMLWriter()) - if err != nil { - return "", "", err - } - - } else { - // TODO render liquid template - html = string(fileContent) - } - - // TODO if yaml contains layout, pass to parent - - // TODO minify - - return html, targetPath, nil -} - -func extractFrontMatter(file io.Reader) ([]byte, map[string]interface{}, error) { - const FM_SEPARATOR = "---" - - var outContent, yamlContent []byte - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - - // if line starts front matter, write lines to yaml until front matter is closed - if strings.TrimSpace(line) == FM_SEPARATOR { - closed := false - for scanner.Scan() { - line := scanner.Text() - if strings.TrimSpace(line) == FM_SEPARATOR { - closed = true - break - } - yamlContent = append(yamlContent, []byte(line+"\n")...) - } - if !closed { - return nil, nil, errors.New("front matter not closed") - } - } else { - // non front matter/yaml content goes to the output slice - outContent = append(outContent, []byte(line+"\n")...) - } - } - // drop the extraneous last line break - outContent = bytes.TrimRight(outContent, "\n") - - var frontMatter map[string]interface{} - if len(yamlContent) != 0 { - err := yaml.Unmarshal([]byte(yamlContent), &frontMatter) - if err != nil { - return nil, nil, fmt.Errorf("invalid yaml: %s", err) - } - } - - return outContent, frontMatter, nil -} diff --git a/commands/templates_test.go b/commands/templates_test.go deleted file mode 100644 index 3df1e59..0000000 --- a/commands/templates_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package commands - -import ( - "strings" - "testing" -) - -func TestExtractFrontMatter(t *testing.T) { - input := `--- -title: my new post -subtitle: a blog post -tags: ["software", "web"] ---- -

Hello World!

` - - outContent, yaml, err := extractFrontMatter(strings.NewReader(input)) - assertEqual(t, err, nil) - assertEqual(t, string(outContent), "

Hello World!

") - assertEqual(t, yaml["title"], "my new post") - assertEqual(t, yaml["subtitle"], "a blog post") - assertEqual(t, yaml["tags"].([]interface{})[0], "software") - assertEqual(t, yaml["tags"].([]interface{})[1], "web") -} - -func TestNonFrontMatterDelimiter(t *testing.T) { - // not identified as front matter, leaving file as is - input := `+++ -title: my new post -subtitle: a blog post -+++ -

Hello World!

` - - out, yaml, err := extractFrontMatter(strings.NewReader(input)) - - assertEqual(t, string(out), input) - assertEqual(t, err, nil) - assertEqual(t, len(yaml), 0) - - // not first thing in file, leaving as is - input = `#+OPTIONS: toc:nil num:nil ---- -title: my new post -subtitle: a blog post -tags: ["software", "web"] ---- -

Hello World!

` - - out, yaml, err = extractFrontMatter(strings.NewReader(input)) - - assertEqual(t, string(out), input) - assertEqual(t, err, nil) - assertEqual(t, len(yaml), 0) -} - -func TestInvalidFrontMatterYaml(t *testing.T) { - input := `--- -title: my new post -subtitle: a blog post -tags: ["software", "web"] -` - - _, _, err := extractFrontMatter(strings.NewReader(input)) - assertEqual(t, err.Error(), "front matter not closed") - - input = `--- -title -tags: ["software", "web"] ---- -

Hello World!

` - - _, _, err = extractFrontMatter(strings.NewReader(input)) - msg := strings.Split(err.Error(), ":")[0] - assertEqual(t, msg, "invalid yaml") -} - -func TestRenderHtml(t *testing.T) { - // TODO -} - -func TestRenderOrg(t *testing.T) { - // TODO -} - -// TODO move to assert package -func assertEqual(t *testing.T, a interface{}, b interface{}) { - if a != b { - t.Fatalf("%v != %v", a, b) - } -} diff --git a/main.go b/main.go index 7bbfdba..fbe53ff 100644 --- a/main.go +++ b/main.go @@ -32,17 +32,16 @@ func run(args []string) error { switch os.Args[1] { case "init": initCmd.Parse(os.Args[2:]) - commands.Init() + return commands.Init() case "build": - commands.Build() + return commands.Build() case "new": newCmd.Parse(os.Args[2:]) - commands.New() + return commands.New() case "serve": - commands.Serve() + return commands.Serve() default: // TODO print usage return errors.New("unknown subcommand") } - return nil } diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000..4255b4d --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,105 @@ +// TODO consider making this another package +package templates + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/niklasfasching/go-org/org" + "gopkg.in/yaml.v3" +) + +const FM_SEPARATOR = "---" + +type Template struct { + srcPath string + Metadata map[string]interface{} +} + +func Parse(path string) (*Template, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + + scanner.Scan() + line := scanner.Text() + + // if the file doesn't start with a front matter delimiter, it's not a template + if strings.TrimSpace(line) != FM_SEPARATOR { + return nil, nil + } + + // read and parse the yaml from the front matter + var yamlContent []byte + closed := false + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == FM_SEPARATOR { + closed = true + break + } + yamlContent = append(yamlContent, []byte(line+"\n")...) + } + if !closed { + return nil, errors.New("front matter not closed") + } + + var metadata map[string]interface{} + if len(yamlContent) != 0 { + err := yaml.Unmarshal([]byte(yamlContent), &metadata) + if err != nil { + return nil, fmt.Errorf("invalid yaml: %s", err) + } + } + + return &Template{srcPath: path, Metadata: metadata}, nil +} + +func (templ Template) Ext() string { + return filepath.Ext(templ.srcPath) +} + +func (templ Template) Render() ([]byte, error) { + file, _ := os.Open(templ.srcPath) + defer file.Close() + scanner := bufio.NewScanner(file) + + // first line is the front matter delimiter, Scan to skip + // and keep skipping until the closing delimiter + scanner.Scan() + scanner.Scan() + for scanner.Text() != FM_SEPARATOR { + scanner.Scan() + } + + // now read the proper template contents to memory + var contents []byte + for scanner.Scan() { + contents = append(contents, scanner.Text()+"\n"...) + } + + if templ.Ext() == ".org" { + // if it's an org file, convert to html + doc := org.New().Parse(bytes.NewReader(contents), templ.srcPath) + html, err := doc.Write(org.NewHTMLWriter()) + contents = []byte(html) + if err != nil { + return nil, err + } + + } else { + // TODO for other file types, assume a liquid template + } + + // TODO: if layout in metadata, pass the result to the rendered parent + + return contents, nil +} diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 0000000..5a02ef2 --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,120 @@ +package templates + +import ( + "os" + "strings" + "testing" +) + +func TestParseTemplate(t *testing.T) { + input := `--- +title: my new post +subtitle: a blog post +tags: ["software", "web"] +--- +

Hello World!

+` + + file := newFile("test*.html", input) + defer os.Remove(file.Name()) + + templ, err := Parse(file.Name()) + assertEqual(t, err, nil) + + assertEqual(t, templ.Ext(), ".html") + assertEqual(t, templ.Metadata["title"], "my new post") + assertEqual(t, templ.Metadata["subtitle"], "a blog post") + assertEqual(t, templ.Metadata["tags"].([]interface{})[0], "software") + assertEqual(t, templ.Metadata["tags"].([]interface{})[1], "web") + + content, err := templ.Render() + assertEqual(t, err, nil) + assertEqual(t, string(content), "

Hello World!

\n") +} + +func TestNonTemplate(t *testing.T) { + // not identified as front matter, leaving file as is + input := `+++ +title: my new post +subtitle: a blog post ++++ +

Hello World!

` + + file := newFile("test*.html", input) + defer os.Remove(file.Name()) + + templ, err := Parse(file.Name()) + assertEqual(t, err, nil) + assertEqual(t, templ, (*Template)(nil)) + + // not first thing in file, leaving as is + input = `#+OPTIONS: toc:nil num:nil +--- +title: my new post +subtitle: a blog post +tags: ["software", "web"] +--- +

Hello World!

` + + file = newFile("test*.html", input) + defer os.Remove(file.Name()) + + templ, err = Parse(file.Name()) + assertEqual(t, err, nil) + assertEqual(t, templ, (*Template)(nil)) +} + +func TestInvalidFrontMatter(t *testing.T) { + input := `--- +title: my new post +subtitle: a blog post +tags: ["software", "web"] +` + file := newFile("test*.html", input) + defer os.Remove(file.Name()) + _, err := Parse(file.Name()) + + assertEqual(t, err.Error(), "front matter not closed") + + input = `--- +title +tags: ["software", "web"] +--- +

Hello World!

` + + file = newFile("test*.html", input) + defer os.Remove(file.Name()) + _, err = Parse(file.Name()) + assert(t, strings.Contains(err.Error(), "invalid yaml")) +} + +func TestRenderLiquid(t *testing.T) { + // TODO +} + +func TestRenderOrg(t *testing.T) { + // TODO +} + +// ------ HELPERS -------- + +func newFile(name string, contents string) *os.File { + file, _ := os.CreateTemp("", name) + file.WriteString(contents) + return file +} + +// TODO move to assert package +func assert(t *testing.T, cond bool) { + t.Helper() + if !cond { + t.Fatalf("%v is false", cond) + } +} + +func assertEqual(t *testing.T, a interface{}, b interface{}) { + t.Helper() + if a != b { + t.Fatalf("%v != %v", a, b) + } +}