From cf0737b99d954b0e0507fbd9ceb5d23e37b9fe42 Mon Sep 17 00:00:00 2001 From: Marco Gazerro Date: Thu, 5 Dec 2024 11:28:01 +0100 Subject: [PATCH 1/3] cmd/scriggo: add the `build` command to build a template This change adds the `build` command to build a template directory: scriggo build [-o output] [dir] --- cmd/scriggo/build.go | 184 +++++++++++++++++++++++++++++++++++++++++++ cmd/scriggo/help.go | 36 +++++++++ cmd/scriggo/main.go | 22 ++++++ 3 files changed, 242 insertions(+) create mode 100644 cmd/scriggo/build.go diff --git a/cmd/scriggo/build.go b/cmd/scriggo/build.go new file mode 100644 index 000000000..726e2dcca --- /dev/null +++ b/cmd/scriggo/build.go @@ -0,0 +1,184 @@ +// Copyright 2024 The Scriggo Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "fmt" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/open2b/scriggo" + "github.com/open2b/scriggo/native" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" +) + +// build the template. +func build(dir, o string) error { + + start := time.Now() + + srcDir := dir + if srcDir == "" { + srcDir = "." + } + + publicDir := "public" + if o != "" { + st, err := os.Stat(o) + if !errors.Is(err, fs.ErrNotExist) { + if err != nil { + return fmt.Errorf("cannot stat output directory %q: %s", o, err) + } + if !st.IsDir() { + return fmt.Errorf("path %q exists but is not a directory", o) + } + } + publicDir = o + } + publicDir, err := filepath.Abs(publicDir) + if err != nil { + return err + } + + dstDir, err := os.MkdirTemp(filepath.Dir(publicDir), "public-temp-*") + if err != nil { + return err + } + defer func() { + err = os.RemoveAll(dstDir) + if err != nil { + log.Print(err) + } + }() + + md := goldmark.New( + goldmark.WithRendererOptions(html.WithUnsafe()), + goldmark.WithParserOptions(parser.WithAutoHeadingID()), + goldmark.WithExtensions(extension.GFM), + goldmark.WithExtensions(extension.Footnote)) + + buildOptions := &scriggo.BuildOptions{ + Globals: make(native.Declarations, len(globals)+1), + MarkdownConverter: func(src []byte, out io.Writer) error { + return md.Convert(src, out) + }, + } + for n, v := range globals { + buildOptions.Globals[n] = v + } + + srcFS := os.DirFS(srcDir) + + dstBase := filepath.Base(dstDir) + publicBase := filepath.Base(publicDir) + println("dstBase:", dstBase) + + err = fs.WalkDir(srcFS, ".", func(name string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + println(name, d.IsDir()) + if name[0] == '.' { + return nil + } + // If it is a directory with the same base name as the public directory name, + // skip it if it is effectively the public directory. It is considered the public + // directory if the corresponding destination temporary directory also exists. + if name == publicBase && d.IsDir() { + st, err := srcFS.(fs.StatFS).Stat(dstBase) + if !errors.Is(err, fs.ErrNotExist) { + if err != nil { + return err + } + if st.IsDir() { + return fs.SkipAll + } + } + } + // If it is the destination temporary directory, skip it. + if name == dstBase && d.IsDir() { + return fs.SkipAll + } + if d.IsDir() { + return os.MkdirAll(filepath.Join(dstDir, name), 0700) + } + ext := filepath.Ext(name) + switch ext { + case ".html": + var dir string + if p := strings.Index(name, "/"); p > 0 { + dir = name[0:p] + } + switch dir { + case "imports", "layouts", "partials": + return nil + } + fallthrough + case ".md": + fpath := strings.TrimSuffix(name, ext) + buildOptions.Globals["filepath"] = fpath + template, err := scriggo.BuildTemplate(srcFS, name, buildOptions) + if err != nil { + return err + } + name := filepath.Join(dstDir, fpath) + ".html" + fi, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + err = template.Run(fi, nil, nil) + if err == nil { + err = fi.Close() + } + default: + src, err := srcFS.Open(name) + if err != nil { + return err + } + name := filepath.Join(dstDir, name) + err = os.MkdirAll(filepath.Dir(name), 0700) + if err != nil { + return err + } + dst, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + _, err = io.Copy(dst, src) + if err == nil { + _ = src.Close() + err = dst.Close() + } + } + return err + }) + if err != nil { + return err + } + + err = os.RemoveAll(publicDir) + if err != nil { + return err + } + err = os.Rename(dstDir, publicDir) + if err != nil { + return err + } + + buildTime := time.Since(start) + _, _ = fmt.Fprintf(os.Stderr, "Build took %s\n", buildTime) + + return nil +} diff --git a/cmd/scriggo/help.go b/cmd/scriggo/help.go index 1e91b4fb2..e30b77d89 100644 --- a/cmd/scriggo/help.go +++ b/cmd/scriggo/help.go @@ -20,6 +20,9 @@ The commands are: serve run a web server and serve the template rooted at the current directory + build generate static files from the template rooted at the current + directory, writing them to './public' by default + init initialize an interpreter for Go programs import generate the source for an importer used by Scriggo to import @@ -40,6 +43,39 @@ Additional help topics: ` +const helpBuild = ` +usage: scriggo build [-o output] [dir] + +Build processes the template rooted at the current directory and writes the +generated files to the './public' directory by default. Non-template files, such +as CSS and JavaScript, are copied as-is. If the './public' directory already +exists, it is first deleted and then recreated empty. If a directory 'dir' is +specified, the template rooted at that directory is built instead of the current +directory. + +For example: + + scriggo build + +generates a static version of the template rooted at the current directory, +processing all template files (e.g., HTML, Markdown) and generating their final +output in the './public' directory. Non-template files from the source +directory, such as stylesheets and scripts, are copied without modification, +resulting in a complete static site ready for deployment. + +The -o flag allows specifying an alternative output directory instead of the +default './public'. + +Examples: + + scriggo build src + + scriggo build -o ../public + + scriggo build -o /var/www site + +` + const helpInit = ` usage: scriggo init [dir] diff --git a/cmd/scriggo/main.go b/cmd/scriggo/main.go index 73d45bb77..100046852 100644 --- a/cmd/scriggo/main.go +++ b/cmd/scriggo/main.go @@ -114,6 +114,9 @@ var commandsHelp = map[string]func(){ `The report includes useful system information.`, ) }, + "build": func() { + txtToHelp(helpBuild) + }, "import": func() { txtToHelp(helpImport) }, @@ -155,6 +158,25 @@ var commands = map[string]func(){ fmt.Fprintf(os.Stdout, "If you encountered an issue, report it at:\n\n\thttps://github.com/open2b/scriggo/issues/new\n\n") exit(0) }, + "build": func() { + flag.Usage = commandsHelp["build"] + o := flag.String("o", "", "write the resulting files to the named directory instead of './public'.") + flag.Parse() + var dir string + switch n := len(flag.Args()); n { + case 0: + case 1: + dir = flag.Arg(0) + default: + flag.Usage() + exitError(`bad number of arguments`) + } + err := build(dir, *o) + if err != nil { + exitError("%s", err) + } + exit(0) + }, "init": func() { flag.Usage = commandsHelp["init"] f := flag.String("f", "", "path of the Scriggofile.") From 47f7cc6172c3f9ef381fda63aa6fc1143a9617e2 Mon Sep 17 00:00:00 2001 From: Marco Gazerro Date: Thu, 5 Dec 2024 11:54:02 +0100 Subject: [PATCH 2/3] Improve documentation --- cmd/scriggo/help.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/scriggo/help.go b/cmd/scriggo/help.go index e30b77d89..910614d42 100644 --- a/cmd/scriggo/help.go +++ b/cmd/scriggo/help.go @@ -47,11 +47,12 @@ const helpBuild = ` usage: scriggo build [-o output] [dir] Build processes the template rooted at the current directory and writes the -generated files to the './public' directory by default. Non-template files, such -as CSS and JavaScript, are copied as-is. If the './public' directory already -exists, it is first deleted and then recreated empty. If a directory 'dir' is -specified, the template rooted at that directory is built instead of the current -directory. +generated files to the 'public' directory by default. If the 'public' directory +already exists, it is deleted along with all its content before writing the new +files. If a directory 'dir' is specified, the template rooted at that directory +is built instead of the current directory. + +Non-template files, such as CSS and JavaScript, are copied as-is. For example: @@ -59,12 +60,12 @@ For example: generates a static version of the template rooted at the current directory, processing all template files (e.g., HTML, Markdown) and generating their final -output in the './public' directory. Non-template files from the source -directory, such as stylesheets and scripts, are copied without modification, -resulting in a complete static site ready for deployment. +output in the 'public' directory. Non-template files from the source directory, +such as stylesheets and scripts, are copied without modification, resulting in a +complete static site ready for deployment. The -o flag allows specifying an alternative output directory instead of the -default './public'. +default 'public'. Examples: From 747c74db7206932b967579c6019fa0ee1510a90d Mon Sep 17 00:00:00 2001 From: Marco Gazerro Date: Thu, 5 Dec 2024 14:08:30 +0100 Subject: [PATCH 3/3] Remove debug prints --- cmd/scriggo/build.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/scriggo/build.go b/cmd/scriggo/build.go index 726e2dcca..15e028e21 100644 --- a/cmd/scriggo/build.go +++ b/cmd/scriggo/build.go @@ -83,13 +83,11 @@ func build(dir, o string) error { dstBase := filepath.Base(dstDir) publicBase := filepath.Base(publicDir) - println("dstBase:", dstBase) err = fs.WalkDir(srcFS, ".", func(name string, d fs.DirEntry, err error) error { if err != nil { return err } - println(name, d.IsDir()) if name[0] == '.' { return nil }