mirror of
https://github.com/facundoolano/jorge.git
synced 2024-12-25 21:58:28 +01:00
Introduce config mod and struct (#2)
* outline config mod and struct * replace site interfaces * load config from config.yml * adapt commands to load and pass config * fix test * fix tests * use symlinks for static assets * implement absolute url filter * doc comment * fix tests * remove outdated TODO comments * Add go build actions workflow (#3) * Add go build actions workflow * set go to 1.22
This commit is contained in:
parent
859327d4bd
commit
a4279aae0d
11 changed files with 265 additions and 135 deletions
|
@ -2,17 +2,11 @@ package commands
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/facundoolano/blorg/config"
|
||||
"github.com/facundoolano/blorg/site"
|
||||
)
|
||||
|
||||
const SRC_DIR = "src"
|
||||
const TARGET_DIR = "target"
|
||||
const LAYOUTS_DIR = "layouts"
|
||||
const INCLUDES_DIR = "includes"
|
||||
const DATA_DIR = "data"
|
||||
|
||||
func Init() error {
|
||||
// get working directory
|
||||
// default to .
|
||||
|
@ -34,15 +28,15 @@ func New() error {
|
|||
|
||||
// Read the files in src/ render them and copy the result to target/
|
||||
func Build(root string) error {
|
||||
src := filepath.Join(root, SRC_DIR)
|
||||
target := filepath.Join(root, TARGET_DIR)
|
||||
layouts := filepath.Join(root, LAYOUTS_DIR)
|
||||
data := filepath.Join(root, DATA_DIR)
|
||||
|
||||
site, err := site.Load(src, layouts, data)
|
||||
config, err := config.Load(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return site.Build(src, target, true, false)
|
||||
site, err := site.Load(*config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return site.Build()
|
||||
}
|
||||
|
|
|
@ -8,19 +8,24 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/facundoolano/blorg/config"
|
||||
"github.com/facundoolano/blorg/site"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// Generate and serve the site, rebuilding when the source files change.
|
||||
func Serve() error {
|
||||
func Serve(rootDir string) error {
|
||||
config, err := config.LoadDevServer(rootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := rebuild(); err != nil {
|
||||
if err := rebuild(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// watch for changes in src and layouts, and trigger a rebuild
|
||||
watcher, err := setupWatcher()
|
||||
watcher, err := setupWatcher(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -35,13 +40,14 @@ func Serve() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rebuild() error {
|
||||
site, err := site.Load(SRC_DIR, LAYOUTS_DIR, DATA_DIR)
|
||||
func rebuild(config *config.Config) error {
|
||||
|
||||
site, err := site.Load(*config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := site.Build(SRC_DIR, TARGET_DIR, false, true); err != nil {
|
||||
if err := site.Build(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -66,7 +72,7 @@ func (d HTMLDir) Open(name string) (http.File, error) {
|
|||
return f, err
|
||||
}
|
||||
|
||||
func setupWatcher() (*fsnotify.Watcher, error) {
|
||||
func setupWatcher(config *config.Config) (*fsnotify.Watcher, error) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -96,12 +102,12 @@ func setupWatcher() (*fsnotify.Watcher, error) {
|
|||
|
||||
// since new nested directories could be triggering this change, and we need to watch those too
|
||||
// and since re-watching files is a noop, I just re-add the entire src everytime there's a change
|
||||
if err := addAll(watcher); err != nil {
|
||||
if err := addAll(watcher, config); err != nil {
|
||||
fmt.Println("error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rebuild(); err != nil {
|
||||
if err := rebuild(config); err != nil {
|
||||
fmt.Println("error:", err)
|
||||
return
|
||||
}
|
||||
|
@ -115,19 +121,19 @@ func setupWatcher() (*fsnotify.Watcher, error) {
|
|||
}
|
||||
}()
|
||||
|
||||
err = addAll(watcher)
|
||||
err = addAll(watcher, config)
|
||||
|
||||
return watcher, err
|
||||
}
|
||||
|
||||
// Add the layouts and all source directories to the given watcher
|
||||
func addAll(watcher *fsnotify.Watcher) error {
|
||||
err := watcher.Add(LAYOUTS_DIR)
|
||||
err = watcher.Add(DATA_DIR)
|
||||
err = watcher.Add(INCLUDES_DIR)
|
||||
func addAll(watcher *fsnotify.Watcher, config *config.Config) error {
|
||||
err := watcher.Add(config.LayoutsDir)
|
||||
err = watcher.Add(config.DataDir)
|
||||
err = watcher.Add(config.IncludesDir)
|
||||
// fsnotify watches all files within a dir, but non recursively
|
||||
// this walks through the src dir and adds watches for each found directory
|
||||
filepath.WalkDir(SRC_DIR, func(path string, entry fs.DirEntry, err error) error {
|
||||
filepath.WalkDir(config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if entry.IsDir() {
|
||||
watcher.Add(path)
|
||||
}
|
||||
|
|
116
config/config.go
Normal file
116
config/config.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// The properties that are depended upon in the source code are declared explicitly in the config struct.
|
||||
// The constructors will set default values for most.
|
||||
// Depending on the command, different defaults will be used (serve is assumed to be a "dev" environment
|
||||
// while build is assumed to be prod)
|
||||
// Some defaults could be overridden by cli flags (eg disable live reload on serve).
|
||||
// The user can override some of those via config yaml.
|
||||
// The non declared values found in config yaml will just be passed as site.config values
|
||||
|
||||
type Config struct {
|
||||
RootDir string
|
||||
SrcDir string
|
||||
TargetDir string
|
||||
LayoutsDir string
|
||||
IncludesDir string
|
||||
DataDir string
|
||||
|
||||
SiteUrl string
|
||||
SlugFormat string
|
||||
|
||||
Minify bool
|
||||
LiveReload bool
|
||||
LinkStatic bool
|
||||
|
||||
ServerHost string
|
||||
ServerPort int
|
||||
|
||||
pageDefaults map[string]interface{}
|
||||
|
||||
// the user provided overrides, as found in config.yml
|
||||
// these will passed as found as template context
|
||||
overrides map[string]interface{}
|
||||
}
|
||||
|
||||
func Load(rootDir string) (*Config, error) {
|
||||
// TODO allow to disable minify
|
||||
|
||||
config := &Config{
|
||||
RootDir: rootDir,
|
||||
SrcDir: filepath.Join(rootDir, "src"),
|
||||
TargetDir: filepath.Join(rootDir, "target"),
|
||||
LayoutsDir: filepath.Join(rootDir, "layouts"),
|
||||
IncludesDir: filepath.Join(rootDir, "includes"),
|
||||
DataDir: filepath.Join(rootDir, "data"),
|
||||
SlugFormat: ":title",
|
||||
Minify: true,
|
||||
LiveReload: false,
|
||||
LinkStatic: false,
|
||||
pageDefaults: map[string]interface{}{},
|
||||
}
|
||||
|
||||
// load overrides from config.yml
|
||||
configPath := filepath.Join(rootDir, "config.yml")
|
||||
yamlContent, err := os.ReadFile(configPath)
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// config file is not mandatory
|
||||
return config, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(yamlContent, &config.overrides)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set user-provided overrides of declared config keys
|
||||
if url, found := config.overrides["url"]; found {
|
||||
config.SiteUrl = url.(string)
|
||||
}
|
||||
if slug, found := config.overrides["url"]; found {
|
||||
config.SlugFormat = slug.(string)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func LoadDevServer(rootDir string) (*Config, error) {
|
||||
// TODO revisit is this Load vs LoadDevServer is the best way to handle both modes
|
||||
// TODO some of the options need to be overridable: host, port, live reload at least
|
||||
|
||||
config, err := Load(rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// setup serve command specific overrides (these could be eventually tweaked with flags)
|
||||
config.ServerHost = "localhost"
|
||||
config.ServerPort = 4001
|
||||
config.Minify = false
|
||||
config.LiveReload = true
|
||||
config.LinkStatic = true
|
||||
config.SiteUrl = fmt.Sprintf("http://%s:%d", config.ServerHost, config.ServerPort)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (config Config) AsContext() map[string]interface{} {
|
||||
context := map[string]interface{}{
|
||||
"url": config.SiteUrl,
|
||||
}
|
||||
maps.Copy(context, config.overrides)
|
||||
return context
|
||||
}
|
9
go.mod
9
go.mod
|
@ -3,18 +3,17 @@ module github.com/facundoolano/blorg
|
|||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/elliotchance/orderedmap/v2 v2.2.0
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/niklasfasching/go-org v1.7.0
|
||||
github.com/osteele/liquid v1.3.2
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
|
||||
github.com/osteele/tuesday v1.0.3 // indirect
|
||||
github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4 // indirect
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
|
9
go.sum
9
go.sum
|
@ -12,11 +12,8 @@ github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU
|
|||
github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4 h1:qk1XyC6UGfPa51PGmsTQJavyhfMLScqw97pEV3sFClI=
|
||||
github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4/go.mod h1:X6iKjXCleSyo/LZzKZ9zDF/ZB2L9gC36I5gLMf32w3M=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
|
@ -31,5 +28,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
6
main.go
6
main.go
|
@ -43,7 +43,11 @@ func run(args []string) error {
|
|||
newCmd.Parse(os.Args[2:])
|
||||
return commands.New()
|
||||
case "serve":
|
||||
return commands.Serve()
|
||||
rootDir := "."
|
||||
if len(os.Args) > 2 {
|
||||
rootDir = os.Args[2]
|
||||
}
|
||||
return commands.Serve(rootDir)
|
||||
default:
|
||||
// TODO print usage
|
||||
return errors.New("unknown subcommand")
|
||||
|
|
87
site/site.go
87
site/site.go
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/facundoolano/blorg/config"
|
||||
"github.com/facundoolano/blorg/templates"
|
||||
"golang.org/x/net/html"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
@ -19,7 +20,7 @@ import (
|
|||
const FILE_RW_MODE = 0777
|
||||
|
||||
type Site struct {
|
||||
config map[string]string // may need to make this interface{} if config gets sophisticated
|
||||
Config config.Config
|
||||
layouts map[string]templates.Template
|
||||
posts []map[string]interface{}
|
||||
pages []map[string]interface{}
|
||||
|
@ -30,34 +31,33 @@ type Site struct {
|
|||
templates map[string]*templates.Template
|
||||
}
|
||||
|
||||
func Load(srcDir string, layoutsDir string, dataDir string) (*Site, error) {
|
||||
// TODO load config from config.yml
|
||||
func Load(config config.Config) (*Site, error) {
|
||||
site := Site{
|
||||
layouts: make(map[string]templates.Template),
|
||||
templates: make(map[string]*templates.Template),
|
||||
config: make(map[string]string),
|
||||
Config: config,
|
||||
tags: make(map[string][]map[string]interface{}),
|
||||
data: make(map[string]interface{}),
|
||||
templateEngine: templates.NewEngine(),
|
||||
templateEngine: templates.NewEngine(config.SiteUrl),
|
||||
}
|
||||
|
||||
if err := site.loadDataFiles(dataDir); err != nil {
|
||||
if err := site.loadDataFiles(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := site.loadLayouts(layoutsDir); err != nil {
|
||||
if err := site.loadLayouts(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := site.loadTemplates(srcDir); err != nil {
|
||||
if err := site.loadTemplates(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &site, nil
|
||||
}
|
||||
|
||||
func (site *Site) loadLayouts(layoutsDir string) error {
|
||||
files, err := os.ReadDir(layoutsDir)
|
||||
func (site *Site) loadLayouts() error {
|
||||
files, err := os.ReadDir(site.Config.LayoutsDir)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
|
@ -68,7 +68,7 @@ func (site *Site) loadLayouts(layoutsDir string) error {
|
|||
for _, entry := range files {
|
||||
if !entry.IsDir() {
|
||||
filename := entry.Name()
|
||||
path := filepath.Join(layoutsDir, filename)
|
||||
path := filepath.Join(site.Config.LayoutsDir, filename)
|
||||
templ, err := templates.Parse(site.templateEngine, path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -82,8 +82,8 @@ func (site *Site) loadLayouts(layoutsDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) loadDataFiles(dataDir string) error {
|
||||
files, err := os.ReadDir(dataDir)
|
||||
func (site *Site) loadDataFiles() error {
|
||||
files, err := os.ReadDir(site.Config.DataDir)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
|
@ -94,7 +94,7 @@ func (site *Site) loadDataFiles(dataDir string) error {
|
|||
for _, entry := range files {
|
||||
if !entry.IsDir() {
|
||||
filename := entry.Name()
|
||||
path := filepath.Join(dataDir, filename)
|
||||
path := filepath.Join(site.Config.DataDir, filename)
|
||||
|
||||
yamlContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
@ -114,15 +114,15 @@ func (site *Site) loadDataFiles(dataDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) loadTemplates(srcDir string) error {
|
||||
_, err := os.ReadDir(srcDir)
|
||||
func (site *Site) loadTemplates() error {
|
||||
_, err := os.ReadDir(site.Config.SrcDir)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("missing %s directory", srcDir)
|
||||
return fmt.Errorf("missing %s directory", site.Config.SrcDir)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("couldn't read %s", srcDir)
|
||||
return fmt.Errorf("couldn't read %s", site.Config.SrcDir)
|
||||
}
|
||||
|
||||
err = filepath.WalkDir(srcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
err = filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if !entry.IsDir() {
|
||||
templ, err := templates.Parse(site.templateEngine, path)
|
||||
// if something fails or this is not a template, skip
|
||||
|
@ -131,10 +131,10 @@ func (site *Site) loadTemplates(srcDir string) error {
|
|||
}
|
||||
|
||||
// set site related (?) metadata. Not sure if this should go elsewhere
|
||||
relPath, _ := filepath.Rel(srcDir, path)
|
||||
relPath, _ := filepath.Rel(site.Config.SrcDir, path)
|
||||
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath)) + templ.Ext()
|
||||
templ.Metadata["path"] = relPath
|
||||
templ.Metadata["url"] = "/" + strings.TrimSuffix(relPath, ".html")
|
||||
templ.Metadata["url"] = "/" + strings.TrimSuffix(strings.TrimSuffix(relPath, "index.html"), ".html")
|
||||
templ.Metadata["dir"] = "/" + filepath.Dir(relPath)
|
||||
|
||||
// posts are templates that can be chronologically sorted --that have a date.
|
||||
|
@ -182,19 +182,18 @@ func (site *Site) loadTemplates(srcDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO consider making minify and reload site.config values
|
||||
func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload bool) error {
|
||||
func (site *Site) Build() error {
|
||||
// clear previous target contents
|
||||
os.RemoveAll(targetDir)
|
||||
os.Mkdir(srcDir, FILE_RW_MODE)
|
||||
os.RemoveAll(site.Config.TargetDir)
|
||||
os.Mkdir(site.Config.SrcDir, FILE_RW_MODE)
|
||||
|
||||
// walk the source directory, creating directories and files at the target dir
|
||||
return filepath.WalkDir(srcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
return filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subpath, _ := filepath.Rel(srcDir, path)
|
||||
targetPath := filepath.Join(targetDir, subpath)
|
||||
subpath, _ := filepath.Rel(site.Config.SrcDir, path)
|
||||
targetPath := filepath.Join(site.Config.TargetDir, subpath)
|
||||
|
||||
// if it's a directory, just create the same at the target
|
||||
if entry.IsDir() {
|
||||
|
@ -202,7 +201,22 @@ func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload
|
|||
}
|
||||
|
||||
var contentReader io.Reader
|
||||
if templ, found := site.templates[path]; found {
|
||||
templ, found := site.templates[path]
|
||||
if !found {
|
||||
// if no template found at location, treat the file as static write its contents to target
|
||||
if site.Config.LinkStatic {
|
||||
// dev optimization: link static files instead of copying them
|
||||
abs, _ := filepath.Abs(path)
|
||||
return os.Symlink(abs, targetPath)
|
||||
}
|
||||
|
||||
srcFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
contentReader = srcFile
|
||||
} else {
|
||||
content, err := site.render(templ)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -210,25 +224,16 @@ func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload
|
|||
|
||||
targetPath = strings.TrimSuffix(targetPath, filepath.Ext(targetPath)) + templ.Ext()
|
||||
contentReader = bytes.NewReader(content)
|
||||
} else {
|
||||
// if no template found at location, treat the file as static
|
||||
// write its contents to target
|
||||
srcFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
contentReader = srcFile
|
||||
}
|
||||
|
||||
targetExt := filepath.Ext(targetPath)
|
||||
// if live reload is enabled, inject the reload snippet to html files
|
||||
if htmlReload && targetExt == ".html" {
|
||||
if site.Config.LiveReload && targetExt == ".html" {
|
||||
// TODO inject live reload snippet
|
||||
}
|
||||
|
||||
// if enabled, minify web files
|
||||
if minify && (targetExt == ".html" || targetExt == ".css" || targetExt == ".js") {
|
||||
if site.Config.Minify && (targetExt == ".html" || targetExt == ".css" || targetExt == ".js") {
|
||||
// TODO minify output
|
||||
}
|
||||
|
||||
|
@ -241,7 +246,7 @@ func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload
|
|||
func (site Site) render(templ *templates.Template) ([]byte, error) {
|
||||
ctx := map[string]interface{}{
|
||||
"site": map[string]interface{}{
|
||||
"config": site.config,
|
||||
"config": site.Config.AsContext(),
|
||||
"posts": site.posts,
|
||||
"tags": site.tags,
|
||||
"pages": site.pages,
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"github.com/facundoolano/blorg/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadAndRenderTemplates(t *testing.T) {
|
||||
root, layouts, src, data := newProject()
|
||||
defer os.RemoveAll(root)
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
// add two layouts
|
||||
content := `---
|
||||
|
@ -19,7 +20,7 @@ func TestLoadAndRenderTemplates(t *testing.T) {
|
|||
{{content}}
|
||||
</body>
|
||||
</html>`
|
||||
file := newFile(layouts, "base.html", content)
|
||||
file := newFile(config.LayoutsDir, "base.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
|
@ -28,7 +29,7 @@ layout: base
|
|||
<h1>{{page.title}}</h1>
|
||||
<h2>{{page.subtitle}}</h2>
|
||||
{{content}}`
|
||||
file = newFile(layouts, "post.html", content)
|
||||
file = newFile(config.LayoutsDir, "post.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
// add two posts
|
||||
|
@ -39,7 +40,7 @@ subtitle: my first post
|
|||
date: 2024-01-01
|
||||
---
|
||||
<p>Hello world!</p>`
|
||||
file = newFile(src, "hello.html", content)
|
||||
file = newFile(config.SrcDir, "hello.html", content)
|
||||
helloPath := file.Name()
|
||||
defer os.Remove(helloPath)
|
||||
|
||||
|
@ -50,7 +51,7 @@ subtitle: my last post
|
|||
date: 2024-02-01
|
||||
---
|
||||
<p>goodbye world!</p>`
|
||||
file = newFile(src, "goodbye.html", content)
|
||||
file = newFile(config.SrcDir, "goodbye.html", content)
|
||||
goodbyePath := file.Name()
|
||||
defer os.Remove(goodbyePath)
|
||||
|
||||
|
@ -60,15 +61,15 @@ layout: base
|
|||
title: about
|
||||
---
|
||||
<p>about this site</p>`
|
||||
file = newFile(src, "about.html", content)
|
||||
file = newFile(config.SrcDir, "about.html", content)
|
||||
aboutPath := file.Name()
|
||||
defer os.Remove(aboutPath)
|
||||
|
||||
// add a static file (no front matter)
|
||||
content = `go away!`
|
||||
file = newFile(src, "robots.txt", content)
|
||||
file = newFile(config.SrcDir, "robots.txt", content)
|
||||
|
||||
site, err := Load(src, layouts, data)
|
||||
site, err := Load(*config)
|
||||
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
|
@ -115,15 +116,15 @@ title: about
|
|||
}
|
||||
|
||||
func TestRenderArchive(t *testing.T) {
|
||||
root, layouts, src, data := newProject()
|
||||
defer os.RemoveAll(root)
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
content := `---
|
||||
title: hello world!
|
||||
date: 2024-01-01
|
||||
---
|
||||
<p>Hello world!</p>`
|
||||
file := newFile(src, "hello.html", content)
|
||||
file := newFile(config.SrcDir, "hello.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
|
@ -131,7 +132,7 @@ title: goodbye!
|
|||
date: 2024-02-01
|
||||
---
|
||||
<p>goodbye world!</p>`
|
||||
file = newFile(src, "goodbye.html", content)
|
||||
file = newFile(config.SrcDir, "goodbye.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
|
@ -139,7 +140,7 @@ title: an oldie!
|
|||
date: 2023-01-01
|
||||
---
|
||||
<p>oldie</p>`
|
||||
file = newFile(src, "an-oldie.html", content)
|
||||
file = newFile(config.SrcDir, "an-oldie.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
// add a page (no date)
|
||||
|
@ -149,10 +150,10 @@ date: 2023-01-01
|
|||
<li>{{ post.date | date: "%Y-%m-%d" }} <a href="{{ post.url }}">{{post.title}}</a></li>{%endfor%}
|
||||
</ul>`
|
||||
|
||||
file = newFile(src, "about.html", content)
|
||||
file = newFile(config.SrcDir, "about.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, err := Load(src, layouts, data)
|
||||
site, err := Load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<ul>
|
||||
|
@ -163,8 +164,8 @@ date: 2023-01-01
|
|||
}
|
||||
|
||||
func TestRenderTags(t *testing.T) {
|
||||
root, layouts, src, data := newProject()
|
||||
defer os.RemoveAll(root)
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
content := `---
|
||||
title: hello world!
|
||||
|
@ -172,7 +173,7 @@ date: 2024-01-01
|
|||
tags: [web, software]
|
||||
---
|
||||
<p>Hello world!</p>`
|
||||
file := newFile(src, "hello.html", content)
|
||||
file := newFile(config.SrcDir, "hello.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
|
@ -181,7 +182,7 @@ date: 2024-02-01
|
|||
tags: [web]
|
||||
---
|
||||
<p>goodbye world!</p>`
|
||||
file = newFile(src, "goodbye.html", content)
|
||||
file = newFile(config.SrcDir, "goodbye.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
|
@ -190,7 +191,7 @@ date: 2023-01-01
|
|||
tags: [software]
|
||||
---
|
||||
<p>oldie</p>`
|
||||
file = newFile(src, "an-oldie.html", content)
|
||||
file = newFile(config.SrcDir, "an-oldie.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
// add a page (no date)
|
||||
|
@ -202,10 +203,10 @@ tags: [software]
|
|||
{% endfor %}
|
||||
`
|
||||
|
||||
file = newFile(src, "about.html", content)
|
||||
file = newFile(config.SrcDir, "about.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, err := Load(src, layouts, data)
|
||||
site, err := Load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<h1>software</h1>
|
||||
|
@ -222,28 +223,28 @@ hello world!
|
|||
}
|
||||
|
||||
func TestRenderPagesInDir(t *testing.T) {
|
||||
root, layouts, src, data := newProject()
|
||||
defer os.RemoveAll(root)
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
content := `---
|
||||
title: "1. hello world!"
|
||||
---
|
||||
<p>Hello world!</p>`
|
||||
file := newFile(src, "01-hello.html", content)
|
||||
file := newFile(config.SrcDir, "01-hello.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
title: "3. goodbye!"
|
||||
---
|
||||
<p>goodbye world!</p>`
|
||||
file = newFile(src, "03-goodbye.html", content)
|
||||
file = newFile(config.SrcDir, "03-goodbye.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
content = `---
|
||||
title: "2. an oldie!"
|
||||
---
|
||||
<p>oldie</p>`
|
||||
file = newFile(src, "02-an-oldie.html", content)
|
||||
file = newFile(config.SrcDir, "02-an-oldie.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
// add a page (no date)
|
||||
|
@ -253,10 +254,10 @@ title: "2. an oldie!"
|
|||
<li><a href="{{ page.url }}">{{page.title}}</a></li>{%endfor%}
|
||||
</ul>`
|
||||
|
||||
file = newFile(src, "index.html", content)
|
||||
file = newFile(config.SrcDir, "index.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, err := Load(src, layouts, data)
|
||||
site, err := Load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<ul>
|
||||
|
@ -271,8 +272,8 @@ func TestRenderArchiveWithExcerpts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRenderDataFile(t *testing.T) {
|
||||
root, layouts, src, data := newProject()
|
||||
defer os.RemoveAll(root)
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
content := `
|
||||
- name: feedi
|
||||
|
@ -280,7 +281,7 @@ func TestRenderDataFile(t *testing.T) {
|
|||
- name: blorg
|
||||
url: https://github.com/facundoolano/blorg
|
||||
`
|
||||
file := newFile(data, "projects.yml", content)
|
||||
file := newFile(config.DataDir, "projects.yml", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
// add a page (no date)
|
||||
|
@ -290,10 +291,10 @@ func TestRenderDataFile(t *testing.T) {
|
|||
<li><a href="{{ project.url }}">{{project.name}}</a></li>{%endfor%}
|
||||
</ul>`
|
||||
|
||||
file = newFile(src, "projects.html", content)
|
||||
file = newFile(config.SrcDir, "projects.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, err := Load(src, layouts, data)
|
||||
site, err := Load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<ul>
|
||||
|
@ -304,7 +305,7 @@ func TestRenderDataFile(t *testing.T) {
|
|||
|
||||
// ------ HELPERS --------
|
||||
|
||||
func newProject() (string, string, string, string) {
|
||||
func newProject() *config.Config {
|
||||
projectDir, _ := os.MkdirTemp("", "root")
|
||||
layoutsDir := filepath.Join(projectDir, "layouts")
|
||||
srcDir := filepath.Join(projectDir, "src")
|
||||
|
@ -313,7 +314,9 @@ func newProject() (string, string, string, string) {
|
|||
os.Mkdir(srcDir, 0777)
|
||||
os.Mkdir(dataDir, 0777)
|
||||
|
||||
return projectDir, layoutsDir, srcDir, dataDir
|
||||
config, _ := config.Load(projectDir)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func newFile(dir string, filename string, contents string) *os.File {
|
||||
|
|
|
@ -3,6 +3,8 @@ package templates
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"encoding/xml"
|
||||
|
@ -18,7 +20,7 @@ import (
|
|||
// a lot of the filters and tags available at jekyll aren't default liquid manually adding them here
|
||||
// copied from https://github.com/osteele/gojekyll/blob/f1794a874890bfb601cae767a0cce15d672e9058/filters/filters.go
|
||||
// MIT License: https://github.com/osteele/gojekyll/blob/f1794a874890bfb601cae767a0cce15d672e9058/LICENSE
|
||||
func loadJekyllFilters(e *liquid.Engine) {
|
||||
func loadJekyllFilters(e *liquid.Engine, siteUrl string) {
|
||||
e.RegisterFilter("filter", filter)
|
||||
e.RegisterFilter("group_by", groupByFilter)
|
||||
e.RegisterFilter("group_by_exp", groupByExpFilter)
|
||||
|
@ -32,15 +34,17 @@ func loadJekyllFilters(e *liquid.Engine) {
|
|||
var buf bytes.Buffer
|
||||
err := goldmark.Convert([]byte(s), &buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
return buf.String()
|
||||
})
|
||||
|
||||
e.RegisterFilter("absolute_url", func(s string) string {
|
||||
// FIXME implement after adding a config struct, using the url
|
||||
// return utils.URLJoin(c.AbsoluteURL, c.BaseURL, s)
|
||||
return s
|
||||
e.RegisterFilter("absolute_url", func(path string) string {
|
||||
url, err := url.JoinPath(siteUrl, path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
e.RegisterFilter("date_to_rfc822", func(date time.Time) string {
|
||||
|
|
|
@ -25,9 +25,11 @@ type Template struct {
|
|||
liquidTemplate liquid.Template
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
// Create a new template engine, with custom liquid filters.
|
||||
// The `siteUrl` is necessary to provide context for the absolute_url filter.
|
||||
func NewEngine(siteUrl string) *Engine {
|
||||
e := liquid.NewEngine()
|
||||
loadJekyllFilters(e)
|
||||
loadJekyllFilters(e, siteUrl)
|
||||
return e
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ tags: ["software", "web"]
|
|||
file := newFile("test*.html", input)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
templ, err := Parse(NewEngine(), file.Name())
|
||||
templ, err := Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
assertEqual(t, templ.Metadata["title"], "my new post")
|
||||
|
@ -42,7 +42,7 @@ subtitle: a blog post
|
|||
file := newFile("test*.html", input)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
_, err := Parse(NewEngine(), file.Name())
|
||||
_, err := Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// not first thing in file, leaving as is
|
||||
|
@ -57,7 +57,7 @@ tags: ["software", "web"]
|
|||
file = newFile("test*.html", input)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
_, err = Parse(NewEngine(), file.Name())
|
||||
_, err = Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assertEqual(t, err, nil)
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ tags: ["software", "web"]
|
|||
`
|
||||
file := newFile("test*.html", input)
|
||||
defer os.Remove(file.Name())
|
||||
_, err := Parse(NewEngine(), file.Name())
|
||||
_, err := Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
|
||||
assertEqual(t, err.Error(), "front matter not closed")
|
||||
|
||||
|
@ -81,7 +81,7 @@ tags: ["software", "web"]
|
|||
|
||||
file = newFile("test*.html", input)
|
||||
defer os.Remove(file.Name())
|
||||
_, err = Parse(NewEngine(), file.Name())
|
||||
_, err = Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assert(t, strings.Contains(err.Error(), "invalid yaml"))
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ tags: ["software", "web"]
|
|||
file := newFile("test*.html", input)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
templ, err := Parse(NewEngine(), file.Name())
|
||||
templ, err := Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assertEqual(t, err, nil)
|
||||
ctx := map[string]interface{}{"page": templ.Metadata}
|
||||
content, err := templ.Render(ctx)
|
||||
|
@ -130,7 +130,7 @@ tags: ["software", "web"]
|
|||
file := newFile("test*.org", input)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
templ, err := Parse(NewEngine(), file.Name())
|
||||
templ, err := Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
content, err := templ.Render(nil)
|
||||
|
@ -172,7 +172,7 @@ tags: ["software", "web"]
|
|||
file := newFile("test*.md", input)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
templ, err := Parse(NewEngine(), file.Name())
|
||||
templ, err := Parse(NewEngine("https://olano.dev"), file.Name())
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
content, err := templ.Render(nil)
|
||||
|
|
Loading…
Reference in a new issue