package cmd import ( "embed" "flag" "fmt" "io" "log/slog" "os" "os/exec" "path/filepath" "text/template" "time" "codeflow.dananglin.me.uk/apollo/spruce/internal/cv" tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs" ) //go:embed templates/tex/* var templates embed.FS type GenerateCommand struct { *flag.FlagSet input string output string employmentHistory int verbose bool } func NewGenerateCommand() *GenerateCommand { gc := GenerateCommand{ FlagSet: flag.NewFlagSet("generate", flag.ExitOnError), } gc.StringVar(&gc.input, "input", "", "specify the CV JSON file that you want to input to the builder.") gc.StringVar(&gc.output, "output", "", "specify the name of the output CV file.") gc.IntVar(&gc.employmentHistory, "employment-history", 10, "show employment history within these number of years.") gc.BoolVar(&gc.verbose, "verbose", false, "set to true to enable verbose logging") return &gc } func (g *GenerateCommand) Run() error { historyLimit := time.Now().AddDate(-1*g.employmentHistory, 0, 0) tempDir, err := os.MkdirTemp("/tmp", "cv-builder-") if err != nil { return fmt.Errorf("unable to create a temporary directory; %w", err) } defer func() { err := os.RemoveAll(tempDir) if err != nil { slog.Warn(fmt.Sprintf("WARN: An error occurred when removing the temporary directory; %v", err)) } }() texFile, err := tex(g.input, tempDir, historyLimit) if err != nil { return fmt.Errorf("unable to create the tex file; %w", err) } if err := pdf(tempDir, texFile, g.output, g.verbose); err != nil { return fmt.Errorf("unable to create the PDF file; %w", err) } return nil } // tex generates the CV document as a Tex file. func tex(input, tempDir string, historyLimit time.Time) (string, error) { slog.Info("Creating the Tex file.") c, err := cv.NewCVFromFile(input) if err != nil { return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err) } output := filepath.Join(tempDir, "cv.tex") file, err := os.Create(output) if err != nil { return "", fmt.Errorf("unable to create output file %s; %w", output, err) } defer file.Close() fmap := template.FuncMap{ "notLastElement": tf.NotLastElement, "join": tf.JoinSentences, "durationToString": tf.FormatDuration, "withinTimePeriod": tf.WithinTimePeriod(historyLimit), } t := template.Must(template.New("cv.tmpl.tex"). Funcs(fmap). Delims("<<", ">>"). ParseFS(templates, "templates/tex/*.tmpl.tex"), ) if err = t.Execute(file, c); err != nil { return "", fmt.Errorf("unable to execute the CV template. %w", err) } slog.Info("Tex file successfully created.", "filename", output) return output, nil } // pdf generates the CV document as a PDF file from the tex file. func pdf(tempDir, texFile, output string, verbose bool) error { slog.Info("Creating the PDF document.") pathArg := "--path=" + tempDir command := exec.Command("mtxrun", pathArg, "--script", "context", texFile) if verbose { command.Stderr = os.Stderr command.Stdout = os.Stdout } if err := command.Run(); err != nil { return err } if output == "" { output = "./cv.pdf" } if err := copyfile(filepath.Join(tempDir, "cv.pdf"), output); err != nil { return err } slog.Info("PDF document successfully created.", "filename", output) return nil } func copyfile(input, output string) error { inputFile, err := os.Open(input) if err != nil { return fmt.Errorf("unable to open %s; %w", input, err) } defer inputFile.Close() outputFile, err := os.Create(output) if err != nil { return fmt.Errorf("unable to create %s; %w", output, err) } defer outputFile.Close() _, err = io.Copy(outputFile, inputFile) if err != nil { return fmt.Errorf("unable to copy %s to %s; %w", input, output, err) } return nil }