forked from mirror/oddmu
Don't require an empty target dir for static site
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")}
|
||||
|
||||
Reference in New Issue
Block a user