sandwitchboard/sandwitchboard.go

211 lines
5.8 KiB
Go
Raw Normal View History

2024-07-02 01:06:25 +01:00
package main
import (
"bytes"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"log/slog"
"os"
"path/filepath"
"strings"
2024-07-03 19:14:38 +01:00
"github.com/alecthomas/kong"
2024-07-02 01:06:25 +01:00
"github.com/aquilax/cooklang-go"
)
const (
stepPrefix = "<p>"
stepSuffix = "</p>"
ingredientPrefix = "<span class=\"ingredient\">"
ingredientSuffix = "</span>"
cookwarePrefix = "<span class=\"cookware\">"
cookwareSuffix = "</span>"
timerPrefix = "<span class=\"timer\">"
timerSuffix = "</span>"
)
//go:embed templates
var embedTmpl embed.FS
var funcMap = template.FuncMap{
"recipeSteps": func(r cooklang.Recipe) template.HTML {
var str = ""
for _, step := range r.Steps {
d := step.Directions
ingredients := make([]string, len(step.Ingredients))
for i, ingredient := range step.Ingredients {
ingredients[i] = ingredient.Name
}
d = hydrate(d, ingredients, ingredientPrefix, ingredientSuffix)
cookwareEls := make([]string, len(step.Cookware))
for i, cookware := range step.Cookware {
cookwareEls[i] = cookware.Name
}
d = hydrate(d, cookwareEls, cookwarePrefix, cookwareSuffix)
timers := make([]string, len(step.Timers))
for i, timer := range step.Timers {
timers[i] = fmt.Sprintf("%v %s", timer.Duration, timer.Unit)
}
d = hydrate(d, timers, timerPrefix, timerSuffix)
str += stepPrefix + d + stepSuffix
}
return template.HTML(str)
},
}
func hydrate(text string, elements []string, prefix, suffix string) string {
cursor := 0
for _, el := range elements {
slog.Debug("Chasing element", "cursor", cursor, "element", el)
idx := strings.Index(text[cursor:], el)
first := text[:cursor+idx]
rest := text[cursor+idx+len(el):]
text = first + prefix + el + suffix + rest
cursor += idx + len(prefix+el+suffix)
slog.Debug("Parsed element", "newCursor", cursor, "first", first, "rest", rest)
}
return text
}
func executeTemplate(name string, data any) ([]byte, error) {
path := filepath.Join("templates", fmt.Sprintf("%s.html.tmpl", name))
b, err := embedTmpl.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("cannot read embedded file %q: %w", path, err)
}
tmpl, err := template.New("").Funcs(funcMap).Parse(string(b))
if err != nil {
return nil, fmt.Errorf("cannot parse template %q: %w", path, err)
}
var strb bytes.Buffer
if err := tmpl.Execute(&strb, data); err != nil {
return nil, fmt.Errorf("cannot execute template %q: %w", path, err)
}
return strb.Bytes(), nil
}
func executeTemplateToFile(tmplName string, data any, filename string) error {
contents, err := executeTemplate(tmplName, data)
if err != nil {
return fmt.Errorf("cannot execute %q template: %w", tmplName, err)
}
if err := os.WriteFile(filename, contents, 0755); err != nil {
return fmt.Errorf("cannot write %q contents to %q: %w", tmplName, filename, err)
}
return nil
}
func copyFile(srcpath, dstpath string) error {
src, err := os.Open(srcpath)
if err != nil {
return fmt.Errorf("cannot open src file %q: %w", srcpath, err)
}
defer src.Close()
dst, err := os.Create(dstpath)
if err != nil {
return fmt.Errorf("cannot open dst file %q: %w", dstpath, err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return fmt.Errorf("cannot copy file from %q to %q: %w", srcpath, dstpath, err)
}
return dst.Sync()
}
func main() {
2024-07-03 19:14:38 +01:00
ctx := kong.Parse(&cli)
if cli.Debug {
2024-07-02 01:06:25 +01:00
slog.SetLogLoggerLevel(slog.LevelDebug)
}
2024-07-03 19:14:38 +01:00
if err := ctx.Run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
os.Exit(1)
}
}
func (g *generateCmd) Run() error {
fi, err := os.Stat(g.Output)
2024-07-02 01:06:25 +01:00
if err != nil && !os.IsNotExist(err) {
2024-07-03 19:14:38 +01:00
return fmt.Errorf("cannot stat %q: %w", g.Output, err)
2024-07-02 01:06:25 +01:00
}
if !os.IsNotExist(err) && !fi.IsDir() {
2024-07-03 19:14:38 +01:00
return fmt.Errorf("path %q is not a directory", g.Output)
2024-07-02 01:06:25 +01:00
} else {
2024-07-03 19:14:38 +01:00
if err := os.RemoveAll(g.Output); err != nil {
return fmt.Errorf("cannot remove %q: %w", g.Output, err)
2024-07-02 01:06:25 +01:00
}
}
2024-07-03 19:14:38 +01:00
if err := os.Mkdir(g.Output, 0755); err != nil {
return fmt.Errorf("cannot create directory on path %q: %w", g.Output, err)
2024-07-02 01:06:25 +01:00
}
2024-07-03 19:14:38 +01:00
walkErr := filepath.WalkDir(g.Path, func(path string, d fs.DirEntry, err error) error {
2024-07-02 01:06:25 +01:00
slog.Debug("Walking through file", "path", path)
relpath := filepath.Join(strings.Split(path, "/")[1:]...)
ext := filepath.Ext(d.Name())
recipeName := strings.TrimSuffix(d.Name(), ext)
if d.IsDir() {
2024-07-03 19:14:38 +01:00
dirpath := filepath.Join(g.Output, relpath)
2024-07-02 01:06:25 +01:00
slog.Debug("Directory found, creating output dir", "path", path, "dirpath", dirpath)
if err := os.MkdirAll(dirpath, 0755); err != nil {
return fmt.Errorf("cannot create directory %s: %w", dirpath, err)
}
return nil
}
if ext != ".cook" {
2024-07-03 19:14:38 +01:00
dstPath := filepath.Join(g.Output, relpath)
2024-07-02 01:06:25 +01:00
slog.Debug("Non-recipe file found, copying", "src", path, "dst", dstPath)
if err := copyFile(path, dstPath); err != nil {
return fmt.Errorf("cannot copy file from %q to %q: %w", path, dstPath, err)
}
slog.Debug("File copy successful", "src", path, "dst", dstPath)
return nil
}
slog.Debug("Parsing file", "name", path)
recipe, err := cooklang.ParseFile(path)
if err != nil {
2024-07-03 19:14:38 +01:00
return fmt.Errorf("cannot parse file %q: %w", path, err)
2024-07-02 01:06:25 +01:00
}
if len(recipe.Steps) == 0 {
slog.Debug("File is not a recipe", "name", path)
return nil
}
slog.Debug("Parsed file", "path", path, "steps", len(recipe.Steps))
2024-07-03 19:14:38 +01:00
recipeDistPath := filepath.Join(g.Output, filepath.Dir(relpath), fmt.Sprintf("%s.html", recipeName))
2024-07-02 01:06:25 +01:00
data := map[string]any{"name": recipeName, "recipe": recipe}
slog.Debug("Executing template", "recipeName", recipeName, "recipeWebPath", recipeDistPath)
if err := executeTemplateToFile("recipe", data, recipeDistPath); err != nil {
2024-07-03 19:14:38 +01:00
return fmt.Errorf("cannot execute template \"recipe\" to file %q: %w", recipeDistPath, err)
2024-07-02 01:06:25 +01:00
}
return nil
})
if walkErr != nil {
2024-07-03 19:14:38 +01:00
return fmt.Errorf("error while walking directory %q: %w", g.Path, walkErr)
2024-07-02 01:06:25 +01:00
}
2024-07-03 19:14:38 +01:00
return nil
2024-07-02 01:06:25 +01:00
}