Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/scriggo: add the build command to build a template #960

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions cmd/scriggo/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// 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)

err = fs.WalkDir(srcFS, ".", func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
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
}
37 changes: 37 additions & 0 deletions cmd/scriggo/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,6 +43,40 @@ 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. 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:

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]

Expand Down
22 changes: 22 additions & 0 deletions cmd/scriggo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ var commandsHelp = map[string]func(){
`The report includes useful system information.`,
)
},
"build": func() {
txtToHelp(helpBuild)
},
"import": func() {
txtToHelp(helpImport)
},
Expand Down Expand Up @@ -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.")
Expand Down