package main import ( "bytes" "embed" "flag" "fmt" "html/template" "io" "io/fs" "log/slog" "os" "path/filepath" "strings" "github.com/aquilax/cooklang-go" ) const ( stepPrefix = "

" stepSuffix = "

" ingredientPrefix = "" ingredientSuffix = "" cookwarePrefix = "" cookwareSuffix = "" timerPrefix = "" timerSuffix = "" ) //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() { recipesFlag := flag.String("recipes", "recipes", "Path to the directory with the recipes.") outputFlag := flag.String("output", "dist", "Path to the directory where the files will be generated.") debugFlag := flag.Bool("debug", false, "Enables debug information.") flag.Parse() if *debugFlag { slog.SetLogLoggerLevel(slog.LevelDebug) } fi, err := os.Stat(*outputFlag) if err != nil && !os.IsNotExist(err) { panic(err) } if !os.IsNotExist(err) && !fi.IsDir() { panic(fmt.Errorf("path %q is not a directory", *outputFlag)) } else { if err := os.RemoveAll(*outputFlag); err != nil { panic(err) } } if err := os.Mkdir(*outputFlag, 0755); err != nil { panic(err) } walkErr := filepath.WalkDir(*recipesFlag, func(path string, d fs.DirEntry, err error) error { 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() { dirpath := filepath.Join(*outputFlag, relpath) 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" { dstPath := filepath.Join(*outputFlag, relpath) 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 { panic(err) } 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)) recipeDistPath := filepath.Join(*outputFlag, filepath.Dir(relpath), fmt.Sprintf("%s.html", recipeName)) data := map[string]any{"name": recipeName, "recipe": recipe} slog.Debug("Executing template", "recipeName", recipeName, "recipeWebPath", recipeDistPath) if err := executeTemplateToFile("recipe", data, recipeDistPath); err != nil { panic(err) } return nil }) if walkErr != nil { panic(walkErr) } }