mirror of
https://github.com/facundoolano/jorge.git
synced 2024-12-26 21:58:51 +01:00
Add a templates package and struct
Squashed commit of the following: commit 7c5b6bf95b14e402b68b141a7d60ccb1468f12b3 Author: facundoolano <facundo.olano@gmail.com> Date: Sun Feb 11 13:15:08 2024 -0300 restore other tests commit 5cf5c43856fc1a9e8f23dc74b81607ab7387f4c3 Author: facundoolano <facundo.olano@gmail.com> Date: Sat Feb 10 23:13:31 2024 -0300 restore a test commit acca0936a42b8b915c25f96c6b435887d7235c23 Author: facundoolano <facundo.olano@gmail.com> Date: Sat Feb 10 22:52:41 2024 -0300 fix a bunch of bugs commit 6f8074402338194ebebaaf929a1d85fdbf0d5e22 Author: facundoolano <facundo.olano@gmail.com> Date: Sat Feb 10 22:00:43 2024 -0300 implement methods commit 5cfeb1ea8600317d8849c6dca63a009237e033af Author: facundoolano <facundo.olano@gmail.com> Date: Sat Feb 10 20:16:10 2024 -0300 add template package and struct commit 7a7b79e006ff6629cbf9445927e84e9c1600667b Author: facundoolano <facundo.olano@gmail.com> Date: Sat Feb 10 20:08:43 2024 -0300 stub template interface
This commit is contained in:
parent
9650d0b6e9
commit
73bbaa2c50
6 changed files with 277 additions and 208 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"]
|
||||
---
|
||||
<p>Hello World!</p>`
|
||||
|
||||
outContent, yaml, err := extractFrontMatter(strings.NewReader(input))
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(outContent), "<p>Hello World!</p>")
|
||||
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
|
||||
+++
|
||||
<p>Hello World!</p>`
|
||||
|
||||
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"]
|
||||
---
|
||||
<p>Hello World!</p>`
|
||||
|
||||
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"]
|
||||
---
|
||||
<p>Hello World!</p>`
|
||||
|
||||
_, _, 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)
|
||||
}
|
||||
}
|
9
main.go
9
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
|
||||
}
|
||||
|
|
105
templates/templates.go
Normal file
105
templates/templates.go
Normal file
|
@ -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
|
||||
}
|
120
templates/templates_test.go
Normal file
120
templates/templates_test.go
Normal file
|
@ -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"]
|
||||
---
|
||||
<p>Hello World!</p>
|
||||
`
|
||||
|
||||
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), "<p>Hello World!</p>\n")
|
||||
}
|
||||
|
||||
func TestNonTemplate(t *testing.T) {
|
||||
// not identified as front matter, leaving file as is
|
||||
input := `+++
|
||||
title: my new post
|
||||
subtitle: a blog post
|
||||
+++
|
||||
<p>Hello World!</p>`
|
||||
|
||||
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"]
|
||||
---
|
||||
<p>Hello World!</p>`
|
||||
|
||||
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"]
|
||||
---
|
||||
<p>Hello World!</p>`
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue