mirror of
https://github.com/facundoolano/jorge.git
synced 2024-09-28 03:20:50 +02:00
Add more content for the serve command post (#23)
* document site api * merge site.Load and site.Build into a single function * fix tests * make Site a private struct * doc * remove superfluous operation * smaller tab width in source examples * remove unnecessary temp var * add some code snippet * improve http fs naming * add a few more code snippets * cleanup serve code * add watcher sample to blog post * improve worker pool sample * restore watcher close * remove unused prefix * some live reload samples in post * remove outdated command
This commit is contained in:
parent
af09593474
commit
ff28ece092
6 changed files with 334 additions and 117 deletions
|
@ -25,12 +25,7 @@ func (cmd *Build) Run(ctx *kong.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
site, err := site.Load(*config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return site.Build()
|
||||
return site.Build(*config)
|
||||
}
|
||||
|
||||
// Prompt the user for a string value
|
||||
|
|
|
@ -34,7 +34,7 @@ func (cmd *Serve) Run(ctx *kong.Context) error {
|
|||
}
|
||||
|
||||
// watch for changes in src and layouts, and trigger a rebuild
|
||||
watcher, broker, err := setupWatcher(config)
|
||||
watcher, broker, err := runWatcher(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func (cmd *Serve) Run(ctx *kong.Context) error {
|
|||
|
||||
// serve the target dir with a file server
|
||||
fs := http.FileServer(HTMLFileSystem{http.Dir(config.TargetDir)})
|
||||
http.Handle("/", http.StripPrefix("/", fs))
|
||||
http.Handle("/", fs)
|
||||
|
||||
if config.LiveReload {
|
||||
// handle client requests to listen to server-sent events
|
||||
|
@ -83,11 +83,12 @@ func makeServerEventsHandler(broker *EventBroker) http.HandlerFunc {
|
|||
|
||||
// Sets up a watcher that will publish changes in the site source files
|
||||
// to the returned event broker.
|
||||
func setupWatcher(config *config.Config) (*fsnotify.Watcher, *EventBroker, error) {
|
||||
func runWatcher(config *config.Config) (*fsnotify.Watcher, *EventBroker, error) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer watchProjectFiles(watcher, config)
|
||||
|
||||
broker := newEventBroker()
|
||||
|
||||
|
@ -99,42 +100,27 @@ func setupWatcher(config *config.Config) (*fsnotify.Watcher, *EventBroker, error
|
|||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// chmod events are noisy, ignore them.
|
||||
// Also ignore dot file events, which are usually spurious (e.g .DS_Store, emacs temp files)
|
||||
isDotFile := strings.HasPrefix(filepath.Base(event.Name), ".")
|
||||
if event.Has(fsnotify.Chmod) || isDotFile {
|
||||
continue
|
||||
}
|
||||
|
||||
// Schedule a rebuild to trigger after a delay. If there was another one pending
|
||||
// it will be canceled.
|
||||
fmt.Printf("\nfile %s changed\n", event.Name)
|
||||
rebuildAfter.Stop()
|
||||
rebuildAfter.Reset(100 * time.Millisecond)
|
||||
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
fmt.Println("error:", err)
|
||||
for event := range watcher.Events {
|
||||
// chmod events are noisy, ignore them.
|
||||
// Also ignore dot file events, which are usually spurious (e.g .DS_Store, emacs temp files)
|
||||
isDotFile := strings.HasPrefix(filepath.Base(event.Name), ".")
|
||||
if event.Has(fsnotify.Chmod) || isDotFile {
|
||||
continue
|
||||
}
|
||||
|
||||
// Schedule a rebuild to trigger after a delay. If there was another one pending
|
||||
// it will be canceled.
|
||||
fmt.Printf("\nfile %s changed\n", event.Name)
|
||||
rebuildAfter.Stop()
|
||||
rebuildAfter.Reset(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
err = addAll(watcher, config)
|
||||
|
||||
return watcher, broker, err
|
||||
}
|
||||
|
||||
// Add the layouts and all source directories to the given watcher
|
||||
func addAll(watcher *fsnotify.Watcher, config *config.Config) error {
|
||||
// Configure the given watcher to notify for changes in the project source files
|
||||
func watchProjectFiles(watcher *fsnotify.Watcher, config *config.Config) error {
|
||||
watcher.Add(config.LayoutsDir)
|
||||
watcher.Add(config.DataDir)
|
||||
watcher.Add(config.IncludesDir)
|
||||
|
@ -153,17 +139,11 @@ func rebuildSite(config *config.Config, watcher *fsnotify.Watcher, broker *Event
|
|||
|
||||
// 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, config); err != nil {
|
||||
if err := watchProjectFiles(watcher, config); err != nil {
|
||||
fmt.Println("couldn't add watchers:", err)
|
||||
}
|
||||
|
||||
site, err := site.Load(*config)
|
||||
if err != nil {
|
||||
fmt.Println("load error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := site.Build(); err != nil {
|
||||
if err := site.Build(*config); err != nil {
|
||||
fmt.Println("build error:", err)
|
||||
return
|
||||
}
|
||||
|
@ -176,15 +156,15 @@ func rebuildSite(config *config.Config, watcher *fsnotify.Watcher, broker *Event
|
|||
// Tweaks the http file system to construct a server that hides the .html suffix from requests.
|
||||
// Based on https://stackoverflow.com/a/57281956/993769
|
||||
type HTMLFileSystem struct {
|
||||
d http.Dir
|
||||
dirFS http.Dir
|
||||
}
|
||||
|
||||
func (d HTMLFileSystem) Open(name string) (http.File, error) {
|
||||
func (htmlFS HTMLFileSystem) Open(name string) (http.File, error) {
|
||||
// Try name as supplied
|
||||
f, err := d.d.Open(name)
|
||||
f, err := htmlFS.dirFS.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
// Not found, try with .html
|
||||
if f, err := d.d.Open(name + ".html"); err == nil {
|
||||
if f, err := htmlFS.dirFS.Open(name + ".html"); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ draft: true
|
|||
#+OPTIONS: toc:nil num:1
|
||||
#+LANGUAGE: en
|
||||
|
||||
** 1. Introduction
|
||||
** Introduction
|
||||
|
||||
The core of the static site generator is the ~build~ command: take some input files, process them ---render templates, convert other markup formats into HTML--- and write the output for serving to the web. <This is where I started with ~jorge~, not only because it was the fundamental feature but because I needed to see the org-mode parsing output as early as possible to know whether I could reasonably expect this project to ultimately replace my Jekyll + org-export setup.
|
||||
|
||||
|
@ -20,27 +20,253 @@ I knew it was a feasible feature because other generators have it, but I didn't
|
|||
|
||||
The beauty of the ~serve~ command was that I could start with the most naive implementation and iterate towards the ideal one, keeping a usable command at every step. With ~build~ and ~serve~ out of the way, I'd be almost done with the project, the rest being nice to have features and UX improvements.
|
||||
|
||||
** 2. Context
|
||||
|
||||
** 3. Implementation
|
||||
** Implementation
|
||||
|
||||
*** A basic file server
|
||||
|
||||
- basic fs server implementaion
|
||||
#+begin_src go
|
||||
func Serve(config config.Config) error {
|
||||
// load and build the project
|
||||
if err := site.Build(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// serve target with file server
|
||||
fs := http.FileServer(http.Dir(config.TargetDir))
|
||||
http.Handle("/", fs)
|
||||
|
||||
fmt.Println("server listening at http://localhost:4001/")
|
||||
return http.ListenAndServe(":4001", nil)
|
||||
}
|
||||
#+end_src
|
||||
|
||||
- improve for directory and html handling
|
||||
https://stackoverflow.com/a/57281956/993769
|
||||
#+begin_src go
|
||||
type HTMLFileSystem struct {
|
||||
dirFS http.Dir
|
||||
}
|
||||
|
||||
func (htmlFS HTMLFileSystem) Open(name string) (http.File, error) {
|
||||
// Try name as supplied
|
||||
f, err := htmlFS.dirFS.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
// Not found, try with .html
|
||||
if f, err := htmlFS.dirFS.Open(name + ".html"); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
#+end_src
|
||||
|
||||
#+begin_src go
|
||||
fs := http.FileServer(HTMLFileSystem{http.Dir(config.TargetDir)})
|
||||
http.Handle("/", fs)
|
||||
#+end_src
|
||||
|
||||
*** Watching for changes
|
||||
- fsnotify to trigger builds
|
||||
- optimization: ln static files
|
||||
|
||||
#+begin_src go
|
||||
func runWatcher(config *config.Config) {
|
||||
watcher, _ := fsnotify.NewWatcher()
|
||||
defer watchProjectFiles(watcher, config)
|
||||
|
||||
go func() {
|
||||
for event := range watcher.Events {
|
||||
fmt.Printf("\nfile %s changed, rebuilding site\n", event.Name)
|
||||
|
||||
// new src directories could be triggering this event
|
||||
// so project files need to be re-added every time
|
||||
watchProjectFiles(watcher, config)
|
||||
|
||||
site.Build(*config)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Configure the given watcher to notify for changes
|
||||
// in the project source files
|
||||
func watchProjectFiles(watcher *fsnotify.Watcher, config *config.Config) {
|
||||
// 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 {
|
||||
if entry.IsDir() {
|
||||
watcher.Add(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*** Build optimizations
|
||||
- optimization: worker pool
|
||||
|
||||
#+begin_src go
|
||||
func (site *Site) Build() error {
|
||||
// clear previous target contents
|
||||
os.RemoveAll(site.Config.TargetDir)
|
||||
|
||||
// walk the source directory, creating directories and files at the target dir
|
||||
return filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
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() {
|
||||
return os.MkdirAll(targetPath, FILE_RW_MODE)
|
||||
}
|
||||
|
||||
// if it's a file render or copy it at the target
|
||||
return site.buildFile(path, targetPath)
|
||||
})
|
||||
}
|
||||
#+end_src
|
||||
|
||||
#+begin_src go
|
||||
// Create a channel to send paths to build and a worker pool to handle them concurrently
|
||||
func spawnBuildWorkers(site *site) (*sync.WaitGroup, chan string) {
|
||||
var wg sync.WaitGroup
|
||||
files := make(chan string, 20)
|
||||
|
||||
for range runtime.NumCPU() {
|
||||
wg.Add(1)
|
||||
go func(files <-chan string) {
|
||||
defer wg.Done()
|
||||
for path := range files {
|
||||
site.buildFile(path)
|
||||
}
|
||||
}(files)
|
||||
}
|
||||
return &wg, files
|
||||
}
|
||||
#+end_src
|
||||
|
||||
#+begin_src diff
|
||||
func (site *site) build() error {
|
||||
// clear previous target contents
|
||||
os.RemoveAll(site.Config.TargetDir)
|
||||
|
||||
+ wg, files := spawnBuildWorkers(site)
|
||||
+ defer wg.Wait()
|
||||
+ defer close(files)
|
||||
|
||||
// walk the source directory, creating directories and files at the target dir
|
||||
return filepath.WalkDir(site.config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
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() {
|
||||
return os.MkdirAll(targetPath, FILE_RW_MODE)
|
||||
}
|
||||
|
||||
- // if it's a file render or copy it at the target
|
||||
- return site.buildFile(path, targetPath)
|
||||
+ // if it's a file send the path to a worker
|
||||
+ // to render or copy it at the target
|
||||
+ files <- path
|
||||
+ return nil
|
||||
})
|
||||
}
|
||||
#+end_src
|
||||
|
||||
- optimization: ln static files
|
||||
|
||||
*** Live reload
|
||||
|
||||
- naive implementation
|
||||
- event broker
|
||||
- is this name right?
|
||||
- intro sse (vs ws)
|
||||
- sse boilerplate
|
||||
|
||||
*** Refinements
|
||||
- don't stop on errors
|
||||
- ignore chmod and temp file events
|
||||
#+begin_src diff
|
||||
fs := http.FileServer(HTMLFileSystem{http.Dir(config.TargetDir)})
|
||||
http.Handle("/", fs)
|
||||
+ http.Handle("/_events/", ServerEventsHandler)
|
||||
#+end_src
|
||||
|
||||
#+begin_src go
|
||||
func ServerEventsHandler (res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("Content-Type", "text/event-stream")
|
||||
res.Header().Set("Connection", "keep-alive")
|
||||
res.Header().Set("Cache-Control", "no-cache")
|
||||
res.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
// send an event to the connected client.
|
||||
// data\n\n just means send an empty, unnamed event
|
||||
fmt.Fprint(res, "data\n\n")
|
||||
res.(http.Flusher).Flush()
|
||||
case <-req.Context().Done():
|
||||
// client connection closed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
#+end_src
|
||||
|
||||
- client boilerplate
|
||||
|
||||
#+begin_src javascript
|
||||
var eventSource;
|
||||
|
||||
function newSSE() {
|
||||
console.log("connecting to server events");
|
||||
eventSource = new EventSource('http://localhost:4001/_events/');
|
||||
|
||||
// when the server sends an event, refresh the page
|
||||
eventSource.onmessage = function () {
|
||||
location.reload()
|
||||
};
|
||||
|
||||
// close connection before refreshing the page
|
||||
window.onbeforeunload = function() {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
// on errors disconnect and attempt reconnection after a delay
|
||||
// this handles server restarting, laptop sleeping, etc.
|
||||
eventSource.onerror = function (event) {
|
||||
console.error('an error occurred:', event);
|
||||
eventSource.close();
|
||||
setTimeout(newSSE, 5000)
|
||||
};
|
||||
}
|
||||
|
||||
newSSE();
|
||||
#+end_src
|
||||
|
||||
- event broker
|
||||
- explain need
|
||||
- is this name right?
|
||||
- show api + link implementation
|
||||
- show updated handler
|
||||
- show updated watcher
|
||||
|
||||
#+begin_src diff
|
||||
-func runWatcher(config *config.Config) {
|
||||
+func runWatcher(config *config.Config) *EventBroker {
|
||||
watcher, _ := fsnotify.NewWatcher()
|
||||
defer watchProjectFiles(watcher, config)
|
||||
+ broker := newEventBroker()
|
||||
|
||||
go func() {
|
||||
for event := range watcher.Events {
|
||||
fmt.Printf("\nfile %s changed, rebuilding site\n", event.Name)
|
||||
|
||||
// new src directories could be triggering this event
|
||||
// so project files need to be re-added every time
|
||||
watchProjectFiles(watcher, config)
|
||||
|
||||
site.Build(*config)
|
||||
+ broker.publish("rebuild")
|
||||
}
|
||||
}()
|
||||
|
||||
+ return broker
|
||||
}
|
||||
#+end_src
|
||||
- delay to prevent bursts
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
const FM_SEPARATOR = "---"
|
||||
const NO_SYNTAX_HIGHLIGHTING = ""
|
||||
const CODE_TABWIDTH = 4
|
||||
|
||||
type Engine = liquid.Engine
|
||||
|
||||
|
@ -168,6 +169,7 @@ func (templ Template) RenderWith(context map[string]interface{}, hlTheme string)
|
|||
if hlTheme != NO_SYNTAX_HIGHLIGHTING {
|
||||
options = append(options, goldmark.WithExtensions(gm_highlight.NewHighlighting(
|
||||
gm_highlight.WithStyle(hlTheme),
|
||||
gm_highlight.WithFormatOptions(html.TabWidth(CODE_TABWIDTH)),
|
||||
)))
|
||||
}
|
||||
md := goldmark.New(options...)
|
||||
|
@ -190,7 +192,9 @@ func highlightCodeBlock(hlTheme string) func(source string, lang string, inline
|
|||
}
|
||||
l = chroma.Coalesce(l)
|
||||
it, _ := l.Tokenise(nil, source)
|
||||
options := []html.Option{}
|
||||
options := []html.Option{
|
||||
html.TabWidth(CODE_TABWIDTH),
|
||||
}
|
||||
if params[":hl_lines"] != "" {
|
||||
ranges := org.ParseRanges(params[":hl_lines"])
|
||||
if ranges != nil {
|
||||
|
|
96
site/site.go
96
site/site.go
|
@ -22,8 +22,8 @@ import (
|
|||
const FILE_RW_MODE = 0666
|
||||
const DIR_RWE_MODE = 0777
|
||||
|
||||
type Site struct {
|
||||
Config config.Config
|
||||
type site struct {
|
||||
config config.Config
|
||||
layouts map[string]markup.Template
|
||||
posts []map[string]interface{}
|
||||
pages []map[string]interface{}
|
||||
|
@ -36,11 +36,25 @@ type Site struct {
|
|||
minifier markup.Minifier
|
||||
}
|
||||
|
||||
func Load(config config.Config) (*Site, error) {
|
||||
site := Site{
|
||||
// Load the site project pointed by `config`, then walk `config.SrcDir`
|
||||
// and recreate it at `config.TargetDir` by rendering template files and copying static ones.
|
||||
// The previous target dir contents are deleted.
|
||||
func Build(config config.Config) error {
|
||||
site, err := load(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return site.build()
|
||||
}
|
||||
|
||||
// Create a new site instance by scanning the project directories
|
||||
// pointed by `config`, loading layouts, templates and data files.
|
||||
func load(config config.Config) (*site, error) {
|
||||
site := site{
|
||||
layouts: make(map[string]markup.Template),
|
||||
templates: make(map[string]*markup.Template),
|
||||
Config: config,
|
||||
config: config,
|
||||
tags: make(map[string][]map[string]interface{}),
|
||||
data: make(map[string]interface{}),
|
||||
templateEngine: markup.NewEngine(config.SiteUrl, config.IncludesDir),
|
||||
|
@ -63,8 +77,8 @@ func Load(config config.Config) (*Site, error) {
|
|||
return &site, nil
|
||||
}
|
||||
|
||||
func (site *Site) loadLayouts() error {
|
||||
files, err := os.ReadDir(site.Config.LayoutsDir)
|
||||
func (site *site) loadLayouts() error {
|
||||
files, err := os.ReadDir(site.config.LayoutsDir)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
|
@ -75,7 +89,7 @@ func (site *Site) loadLayouts() error {
|
|||
for _, entry := range files {
|
||||
if !entry.IsDir() {
|
||||
filename := entry.Name()
|
||||
path := filepath.Join(site.Config.LayoutsDir, filename)
|
||||
path := filepath.Join(site.config.LayoutsDir, filename)
|
||||
templ, err := markup.Parse(site.templateEngine, path)
|
||||
if err != nil {
|
||||
return checkFileError(err)
|
||||
|
@ -89,8 +103,8 @@ func (site *Site) loadLayouts() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) loadDataFiles() error {
|
||||
files, err := os.ReadDir(site.Config.DataDir)
|
||||
func (site *site) loadDataFiles() error {
|
||||
files, err := os.ReadDir(site.config.DataDir)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
|
@ -101,7 +115,7 @@ func (site *Site) loadDataFiles() error {
|
|||
for _, entry := range files {
|
||||
if !entry.IsDir() {
|
||||
filename := entry.Name()
|
||||
path := filepath.Join(site.Config.DataDir, filename)
|
||||
path := filepath.Join(site.config.DataDir, filename)
|
||||
|
||||
yamlContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
@ -121,12 +135,12 @@ func (site *Site) loadDataFiles() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) loadTemplates() error {
|
||||
if _, err := os.Stat(site.Config.SrcDir); err != nil {
|
||||
func (site *site) loadTemplates() error {
|
||||
if _, err := os.Stat(site.config.SrcDir); err != nil {
|
||||
return fmt.Errorf("missing src directory")
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(site.Config.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 := markup.Parse(site.templateEngine, path)
|
||||
// if something fails or this is not a template, skip
|
||||
|
@ -135,8 +149,8 @@ func (site *Site) loadTemplates() error {
|
|||
}
|
||||
|
||||
// set site related (?) metadata. Not sure if this should go elsewhere
|
||||
relPath, _ := filepath.Rel(site.Config.SrcDir, path)
|
||||
srcPath, _ := filepath.Rel(site.Config.RootDir, path)
|
||||
relPath, _ := filepath.Rel(site.config.SrcDir, path)
|
||||
srcPath, _ := filepath.Rel(site.config.RootDir, path)
|
||||
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath)) + templ.TargetExt()
|
||||
templ.Metadata["src_path"] = srcPath
|
||||
templ.Metadata["path"] = relPath
|
||||
|
@ -145,7 +159,7 @@ func (site *Site) loadTemplates() error {
|
|||
|
||||
// if drafts are disabled, exclude from posts, page and tags indexes, but not from site.templates
|
||||
// we want to explicitly exclude the template from the target, rather than treating it as a non template file
|
||||
if !templ.IsDraft() || site.Config.IncludeDrafts {
|
||||
if !templ.IsDraft() || site.config.IncludeDrafts {
|
||||
// posts are templates that can be chronologically sorted --that have a date.
|
||||
// the rest are pages.
|
||||
if templ.IsPost() {
|
||||
|
@ -205,9 +219,9 @@ func (site *Site) loadTemplates() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) addPrevNext(posts []map[string]interface{}) {
|
||||
func (site *site) addPrevNext(posts []map[string]interface{}) {
|
||||
for i, post := range posts {
|
||||
path := filepath.Join(site.Config.RootDir, post["src_path"].(string))
|
||||
path := filepath.Join(site.config.RootDir, post["src_path"].(string))
|
||||
|
||||
// only consider them part of the same collection if they share the directory
|
||||
if i > 0 && post["dir"] == posts[i-1]["dir"] {
|
||||
|
@ -227,23 +241,23 @@ func (site *Site) addPrevNext(posts []map[string]interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
func (site *Site) Build() error {
|
||||
// Walk the `site.Config.SrcDir` directory and reproduce it at `site.Config.TargetDir`,
|
||||
// rendering template files and copying static ones.
|
||||
func (site *site) build() error {
|
||||
// clear previous target contents
|
||||
os.RemoveAll(site.Config.TargetDir)
|
||||
os.Mkdir(site.Config.
|
||||
SrcDir, DIR_RWE_MODE)
|
||||
os.RemoveAll(site.config.TargetDir)
|
||||
|
||||
wg, files := spawnBuildWorkers(site)
|
||||
defer wg.Wait()
|
||||
defer close(files)
|
||||
|
||||
// walk the source directory, creating directories and files at the target dir
|
||||
err := filepath.WalkDir(site.Config.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(site.Config.SrcDir, path)
|
||||
targetPath := filepath.Join(site.Config.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() {
|
||||
|
@ -253,12 +267,10 @@ func (site *Site) Build() error {
|
|||
files <- path
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a channel to send paths to build and a worker pool to handle them concurrently
|
||||
func spawnBuildWorkers(site *Site) (*sync.WaitGroup, chan string) {
|
||||
func spawnBuildWorkers(site *site) (*sync.WaitGroup, chan string) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
files := make(chan string, 20)
|
||||
|
@ -275,16 +287,16 @@ func spawnBuildWorkers(site *Site) (*sync.WaitGroup, chan string) {
|
|||
return &wg, files
|
||||
}
|
||||
|
||||
func (site *Site) buildFile(path string) error {
|
||||
subpath, _ := filepath.Rel(site.Config.SrcDir, path)
|
||||
targetPath := filepath.Join(site.Config.TargetDir, subpath)
|
||||
func (site *site) buildFile(path string) error {
|
||||
subpath, _ := filepath.Rel(site.config.SrcDir, path)
|
||||
targetPath := filepath.Join(site.config.TargetDir, subpath)
|
||||
|
||||
var contentReader io.Reader
|
||||
var err error
|
||||
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 {
|
||||
if site.config.LinkStatic {
|
||||
// dev optimization: link static files instead of copying them
|
||||
abs, _ := filepath.Abs(path)
|
||||
err = os.Symlink(abs, targetPath)
|
||||
|
@ -298,7 +310,7 @@ func (site *Site) buildFile(path string) error {
|
|||
defer srcFile.Close()
|
||||
contentReader = srcFile
|
||||
} else {
|
||||
if templ.IsDraft() && !site.Config.IncludeDrafts {
|
||||
if templ.IsDraft() && !site.config.IncludeDrafts {
|
||||
fmt.Println("skipping draft", targetPath)
|
||||
return nil
|
||||
}
|
||||
|
@ -322,7 +334,7 @@ func (site *Site) buildFile(path string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if site.Config.Minify {
|
||||
if site.config.Minify {
|
||||
contentReader = site.minifier.Minify(targetExt, contentReader)
|
||||
}
|
||||
|
||||
|
@ -330,10 +342,10 @@ func (site *Site) buildFile(path string) error {
|
|||
return writeToFile(targetPath, contentReader)
|
||||
}
|
||||
|
||||
func (site *Site) render(templ *markup.Template) ([]byte, error) {
|
||||
func (site *site) render(templ *markup.Template) ([]byte, error) {
|
||||
ctx := map[string]interface{}{
|
||||
"site": map[string]interface{}{
|
||||
"config": site.Config.AsContext(),
|
||||
"config": site.config.AsContext(),
|
||||
"posts": site.posts,
|
||||
"tags": site.tags,
|
||||
"pages": site.pages,
|
||||
|
@ -342,7 +354,7 @@ func (site *Site) render(templ *markup.Template) ([]byte, error) {
|
|||
}
|
||||
|
||||
ctx["page"] = templ.Metadata
|
||||
content, err := templ.RenderWith(ctx, site.Config.HighlightTheme)
|
||||
content, err := templ.RenderWith(ctx, site.config.HighlightTheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -353,7 +365,7 @@ func (site *Site) render(templ *markup.Template) ([]byte, error) {
|
|||
if layout_templ, ok := site.layouts[layout.(string)]; ok {
|
||||
ctx["layout"] = layout_templ.Metadata
|
||||
ctx["content"] = content
|
||||
content, err = layout_templ.RenderWith(ctx, site.Config.HighlightTheme)
|
||||
content, err = layout_templ.RenderWith(ctx, site.config.HighlightTheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -416,8 +428,8 @@ func getExcerpt(templ *markup.Template) string {
|
|||
}
|
||||
|
||||
// if live reload is enabled, inject the reload snippet to html files
|
||||
func (site *Site) injectLiveReload(extension string, contentReader io.Reader) (io.Reader, error) {
|
||||
if !site.Config.LiveReload || extension != ".html" {
|
||||
func (site *site) injectLiveReload(extension string, contentReader io.Reader) (io.Reader, error) {
|
||||
if !site.config.LiveReload || extension != ".html" {
|
||||
return contentReader, nil
|
||||
}
|
||||
|
||||
|
@ -440,6 +452,6 @@ function newSSE() {
|
|||
};
|
||||
}
|
||||
newSSE();`
|
||||
script := fmt.Sprintf(JS_SNIPPET, site.Config.SiteUrl)
|
||||
script := fmt.Sprintf(JS_SNIPPET, site.config.SiteUrl)
|
||||
return markup.InjectScript(contentReader, script)
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ title: about
|
|||
content = `go away!`
|
||||
newFile(config.SrcDir, "robots.txt", content)
|
||||
|
||||
site, err := Load(*config)
|
||||
site, err := load(*config)
|
||||
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
|
@ -166,7 +166,7 @@ date: 2024-02-03
|
|||
newFile(tutorial2, "another-entry.html", `---
|
||||
---`)
|
||||
|
||||
site, err := Load(*config)
|
||||
site, err := load(*config)
|
||||
// helper method to map a filename to its prev next keys (if any)
|
||||
getPrevNext := func(dir string, filename string) (interface{}, interface{}) {
|
||||
path := filepath.Join(dir, filename)
|
||||
|
@ -261,7 +261,7 @@ date: 2023-01-01
|
|||
file = newFile(config.SrcDir, "about.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, _ := Load(*config)
|
||||
site, _ := load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<ul>
|
||||
|
@ -314,7 +314,7 @@ tags: [software]
|
|||
file = newFile(config.SrcDir, "about.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, _ := Load(*config)
|
||||
site, _ := load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<h1>software</h1>
|
||||
|
@ -365,7 +365,7 @@ title: "2. an oldie!"
|
|||
file = newFile(config.SrcDir, "index.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, _ := Load(*config)
|
||||
site, _ := load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<ul>
|
||||
|
@ -420,7 +420,7 @@ tags: [software]
|
|||
file = newFile(config.SrcDir, "about.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, _ := Load(*config)
|
||||
site, _ := load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, strings.TrimSpace(string(output)), `goodbye! - an overridden excerpt
|
||||
|
@ -453,7 +453,7 @@ func TestRenderDataFile(t *testing.T) {
|
|||
file = newFile(config.SrcDir, "projects.html", content)
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
site, _ := Load(*config)
|
||||
site, _ := load(*config)
|
||||
output, err := site.render(site.templates[file.Name()])
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<ul>
|
||||
|
@ -505,9 +505,9 @@ layout: base
|
|||
newFile(config.SrcDir, "index.html", content)
|
||||
|
||||
// build site
|
||||
site, err := Load(*config)
|
||||
site, err := load(*config)
|
||||
assertEqual(t, err, nil)
|
||||
err = site.Build()
|
||||
err = site.build()
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test target files generated
|
||||
|
@ -574,9 +574,9 @@ layout: base
|
|||
|
||||
// build site with drafts
|
||||
config.IncludeDrafts = true
|
||||
site, err := Load(*config)
|
||||
site, err := load(*config)
|
||||
assertEqual(t, err, nil)
|
||||
err = site.Build()
|
||||
err = site.build()
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test target files generated
|
||||
|
@ -599,9 +599,9 @@ layout: base
|
|||
|
||||
// build site WITHOUT drafts
|
||||
config.IncludeDrafts = false
|
||||
site, err = Load(*config)
|
||||
site, err = load(*config)
|
||||
assertEqual(t, err, nil)
|
||||
err = site.Build()
|
||||
err = site.build()
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test only non drafts generated
|
||||
|
|
Loading…
Reference in a new issue