Don't require an empty target dir for static site

This commit is contained in:
Alex Schroeder
2024-03-12 23:46:43 +01:00
parent df9439c356
commit 7d4530383e
4 changed files with 49 additions and 48 deletions

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-STATIC" "1" "2024-03-07"
.TH "ODDMU-STATIC" "1" "2024-03-12"
.PP
.SH NAME
.PP
@@ -18,8 +18,8 @@ oddmu-static - create a static copy of the site
.SH DESCRIPTION
.PP
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given destination directory.\& The destination
directory must not exist.\&
directory and saves them in the given destination directory.\& Existing files are
only overwritten if they are older than the source file.\&
.PP
All pages (files with the ".\&md" extension) are turned into HTML files (with the
".\&html" extension) using the "static.\&html" template.\& Links pointing to existing
@@ -53,11 +53,13 @@ original, too.\&
.PP
.SH EXAMPLE
.PP
Generate a static copy of the site:
Generate a static copy of the site, but only loading language detection for
German and English, significantly reducing the time it takes to generate the
static site:
.PP
.nf
.RS 4
oddmu static \&.\&./archive
env ODDMU_LANGUAGES=de,en oddmu static \&.\&./archive
.fi
.RE
.PP

View File

@@ -11,8 +11,8 @@ oddmu-static - create a static copy of the site
# DESCRIPTION
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given destination directory. The destination
directory must not exist.
directory and saves them in the given destination directory. Existing files are
only overwritten if they are older than the source file.
All pages (files with the ".md" extension) are turned into HTML files (with the
".html" extension) using the "static.html" template. Links pointing to existing
@@ -46,10 +46,12 @@ original, too.
# EXAMPLE
Generate a static copy of the site:
Generate a static copy of the site, but only loading language detection for
German and English, significantly reducing the time it takes to generate the
static site:
```
oddmu static ../archive
env ODDMU_LANGUAGES=de,en oddmu static ../archive
```
# LIMITATIONS

View File

@@ -42,15 +42,13 @@ func (cmd *staticCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
return subcommands.ExitFailure
}
dir := filepath.Clean(args[0])
_, err := os.Stat(dir)
if err == nil {
fmt.Fprintf(os.Stderr, "%s already exists\n", dir)
return subcommands.ExitFailure
}
return staticCli(dir, cmd.jobs, false)
}
type args struct { path string; info fs.FileInfo }
type args struct {
source, target string
info fs.FileInfo
}
// staticCli generates a static site in the designated directory. The quiet flag is used to suppress output when running
// tests.
@@ -65,7 +63,7 @@ func staticCli(dir string, jobs int, quiet bool) subcommands.ExitStatus {
done := make(chan bool)
stop := make(chan error)
for i := 0; i < jobs; i++ {
go staticWorker(dir, tasks, results, done)
go staticWorker(tasks, results, done)
}
go staticWalk(dir, tasks, stop)
go staticWatch(jobs, results, done)
@@ -108,10 +106,15 @@ func staticWalk (dir string, tasks chan(args), stop chan(error)) {
return nil
}
// recreate subdirectories
target := filepath.Join(dir, path)
if info.IsDir() {
return os.Mkdir(filepath.Join(dir, path), 0755)
return os.Mkdir(target, 0755)
}
// do the task if the target file doesn't exist or if the source file is newer
other, err := os.Stat(target)
if err != nil || info.ModTime().After(other.ModTime()) {
tasks <- args{ source: path, target: target, info: info }
}
tasks <- args{ path: path, info: info }
return nil
}
})
@@ -129,10 +132,10 @@ func staticWatch(jobs int, results chan(error), done chan(bool)) {
// staticWorker takes arguments off the tasks channel (the file to process) and put results in the results channel (any
// errors encountered); when they're done they send true on the done channel.
func staticWorker(dir string, tasks chan(args), results chan(error), done chan(bool)) {
func staticWorker(tasks chan(args), results chan(error), done chan(bool)) {
task, ok := <- tasks
for ok {
results <- staticFile(dir, task.path, task.info)
results <- staticFile(task.source, task.target, task.info)
task, ok = <- tasks
}
done <- true
@@ -164,28 +167,27 @@ func staticProgressIndicator(results chan(error), stop chan(error), quiet bool)
// staticFile is used to walk the file trees and do the right thing for the destination directory: create
// subdirectories, link files, render HTML files.
func staticFile(dir, path string, info fs.FileInfo) error {
func staticFile(source, target string, info fs.FileInfo) error {
// render pages
if strings.HasSuffix(path, ".md") {
p, err := staticPage(path, dir)
if strings.HasSuffix(source, ".md") {
p, err := staticPage(source[:len(source)-3], target[:len(target)-3] + ".html")
if err != nil {
return err
}
return staticFeed(path, dir, p, info.ModTime())
return staticFeed(source[:len(source)-3], target[:len(target)-3] + ".rss", p, info.ModTime())
}
// remaining files are linked unless this is a template
if slices.Contains(templateFiles, filepath.Base(path)) {
if slices.Contains(templateFiles, filepath.Base(source)) {
return nil
}
return os.Link(path, filepath.Join(dir, path))
return os.Link(source, target)
}
// staticPage takes the filename of a page (ending in ".md") and generates a static HTML page.
func staticPage(path, dir string) (*Page, error) {
name := strings.TrimSuffix(path, ".md")
p, err := loadPage(filepath.ToSlash(name))
func staticPage(source, target string) (*Page, error) {
p, err := loadPage(filepath.ToSlash(source))
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", name, err)
fmt.Fprintf(os.Stderr, "Cannot load %s: %s\n", source, err)
return nil, err
}
p.handleTitle(true)
@@ -202,19 +204,18 @@ func staticPage(path, dir string) (*Page, error) {
p.Html = unsafeBytes(maybeUnsafeHTML)
p.Language = language(p.plainText())
p.Hashtags = *hashtags
return p, write(p, filepath.Join(dir, name+".html"), "", "static.html")
return p, write(p, target, "", "static.html")
}
// staticFeed writes a .rss file for a page, but only if it's an index page or a page that might be used as a hashtag
func staticFeed(path, dir string, p *Page, ti time.Time) error {
func staticFeed(source, target string, p *Page, ti time.Time) error {
// render feed, maybe
name := strings.TrimSuffix(path, ".md")
base := filepath.Base(name)
base := filepath.Base(source)
_, ok := index.token["#"+strings.ToLower(base)]
if base == "index" || ok {
f := feed(p, ti)
if len(f.Items) > 0 {
return write(f, filepath.Join(dir, name + ".rss"), `<?xml version="1.0" encoding="UTF-8"?>`, "feed.html" )
return write(f, target, `<?xml version="1.0" encoding="UTF-8"?>`, "feed.html")
}
}
return nil
@@ -246,26 +247,22 @@ func staticLinks(node ast.Node, entering bool) ast.WalkStatus {
}
// write a page or feed with an appropriate template to a specific destination, overwriting it.
func write(data any, destination, prefix, templateFile string) error {
_, err := os.Stat(destination)
if err == nil {
fmt.Fprintf(os.Stderr, "%s already exists\n", destination)
return nil
}
dst, err := os.Create(destination)
func write(data any, path, prefix, templateFile string) error {
file, err := os.Create(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot create %s: %s\n", destination, err)
fmt.Fprintf(os.Stderr, "Cannot create %s: %s\n", path, err)
return err
}
_, err = dst.Write([]byte(prefix))
_, err = file.Write([]byte(prefix))
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot write prefix %s: %s\n", path, err)
return err
}
templates.RLock()
defer templates.RUnlock()
err = templates.template[templateFile].Execute(dst, data)
err = templates.template[templateFile].Execute(file, data)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot execute %s template for %s: %s\n", templateFile, destination, err)
fmt.Fprintf(os.Stderr, "Cannot execute %s template for %s: %s\n", templateFile, path, err)
return err
}
return nil

View File

@@ -7,7 +7,7 @@ import (
"testing"
)
func TestStatusCmd(t *testing.T) {
func TestStaticCmd(t *testing.T) {
cleanup(t, "testdata/static")
s := staticCli("testdata/static", 2, true)
assert.Equal(t, subcommands.ExitSuccess, s)
@@ -19,7 +19,7 @@ func TestStatusCmd(t *testing.T) {
assert.FileExists(t, "testdata/static/static_cmd_test.go")
}
func TestFeedStatusCmd(t *testing.T) {
func TestFeedStaticCmd(t *testing.T) {
cleanup(t, "testdata/static-feed")
cleanup(t, "testdata/static-feed-out")
p := &Page{Name: "testdata/static-feed/Haiku", Body: []byte("# Haiku\n")}