clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

refactor/root-tui-model
puzzletea / cmd / export_pdf.go
export_pdf.go 4.6K
1package cmd23import (4 "fmt"5 "path/filepath"6 "strings"7 "time"89 "github.com/FelineStateMachine/puzzletea/export/builtinprint"10 "github.com/FelineStateMachine/puzzletea/export/pack"11 "github.com/FelineStateMachine/puzzletea/export/pdf"12 "github.com/FelineStateMachine/puzzletea/registry"1314 "github.com/spf13/cobra"15)1617var (18 flagPDFOutput string19 flagPDFTitle string20 flagPDFHeader string21 flagPDFVolume int22 flagPDFAdvert string23 flagPDFShuffleSeed string24 flagPDFSheetLayout string25)2627var exportPDFCmd = &cobra.Command{28 Use: "export-pdf <input.jsonl> [more.jsonl ...]",29 Short: "Convert one or more PuzzleTea JSONL exports into a printable PDF",30 Long: "Parse one or more JSONL export files, order puzzles by progressive difficulty with seeded mixing, and render either a half-letter PDF or an imposed duplex-booklet PDF with a title page, one puzzle per logical half-letter page, and automatic page-count padding to a multiple of 4 for booklet printing. The duplex-booklet layout includes the 4-page black-ink cover block automatically.",31 Args: cobra.MinimumNArgs(1),32 RunE: runExportPDF,33}3435func init() {36 exportPDFCmd.Flags().StringVarP(&flagPDFOutput, "output", "o", "", "write output PDF path (defaults to <first-input>-print.pdf)")37 exportPDFCmd.Flags().StringVar(&flagPDFTitle, "title", "", "subtitle shown on the title page (and on the outside cover when enabled)")38 exportPDFCmd.Flags().StringVar(&flagPDFHeader, "header", "", "optional intro paragraph shown on the title page under 'PuzzleTea Puzzle Pack'")39 exportPDFCmd.Flags().IntVar(&flagPDFVolume, "volume", 1, "volume number shown on the title page (and on the outside cover when enabled) (must be >= 1)")40 exportPDFCmd.Flags().StringVar(&flagPDFAdvert, "advert", "Find more puzzles at github.com/FelineStateMachine/puzzletea", "advert text shown on the title page")41 exportPDFCmd.Flags().StringVar(&flagPDFShuffleSeed, "shuffle-seed", "", "seed for deterministic within-band difficulty mixing")42 exportPDFCmd.Flags().StringVar(&flagPDFSheetLayout, "sheet-layout", "half-letter", "physical PDF layout: half-letter or duplex-booklet (landscape US Letter with two half-letter pages per sheet side; print duplex on short edge). duplex-booklet automatically includes the 4-page cover block")43}4445func runExportPDF(cmd *cobra.Command, args []string) error {46 builtinprint.Register()4748 docs, err := pdfexport.ParseJSONLFiles(args)49 if err != nil {50 return err51 }5253 puzzles := flattenPuzzles(docs)54 if len(puzzles) == 0 {55 return nil56 }5758 packexport.AnnotatePuzzlesForPrint(puzzles, registry.Definitions())5960 shuffleSeed := strings.TrimSpace(flagPDFShuffleSeed)61 if shuffleSeed == "" {62 shuffleSeed = time.Now().Format(time.RFC3339Nano)63 }64 ordered := pdfexport.OrderPuzzlesForPrint(puzzles, shuffleSeed)6566 output := strings.TrimSpace(flagPDFOutput)67 if output == "" {68 base := filepath.Base(args[0])69 output = strings.TrimSuffix(base, filepath.Ext(base)) + "-print.pdf"70 }71 if !strings.EqualFold(filepath.Ext(output), ".pdf") {72 return fmt.Errorf("--output must use a .pdf extension")73 }7475 cfg, err := buildRenderConfigForPDF(docs, shuffleSeed, time.Now())76 if err != nil {77 return err78 }79 if err := pdfexport.WritePDF(output, docs, ordered, cfg); err != nil {80 return err81 }8283 _, err = fmt.Fprintf(cmd.OutOrStdout(), "wrote %s with %d puzzles\n", output, len(ordered))84 return err85}8687func flattenPuzzles(docs []pdfexport.PackDocument) []pdfexport.Puzzle {88 puzzles := []pdfexport.Puzzle{}89 for _, doc := range docs {90 puzzles = append(puzzles, doc.Puzzles...)91 }92 return puzzles93}9495func defaultPDFTitle(docs []pdfexport.PackDocument) string {96 if len(docs) == 1 {97 category := strings.TrimSpace(docs[0].Metadata.Category)98 if category != "" {99 return fmt.Sprintf("%s Puzzle Pack", category)100 }101 }102 return "PuzzleTea Mixed Puzzle Pack"103}104105func validatePDFVolume(volume int) error {106 if volume < 1 {107 return fmt.Errorf("--volume must be >= 1")108 }109 return nil110}111112func buildRenderConfigForPDF(docs []pdfexport.PackDocument, shuffleSeed string, now time.Time) (pdfexport.RenderConfig, error) {113 if err := validatePDFVolume(flagPDFVolume); err != nil {114 return pdfexport.RenderConfig{}, err115 }116 return packexport.BuildRenderConfig(packexport.RenderOptions{117 Title: defaultIfBlank(flagPDFTitle, defaultPDFTitle(docs)),118 Header: flagPDFHeader,119 Advert: flagPDFAdvert,120 Volume: flagPDFVolume,121 SheetLayout: flagPDFSheetLayout,122 ShuffleSeed: shuffleSeed,123 GeneratedAt: now,124 }, docs)125}126127func defaultIfBlank(value, fallback string) string {128 value = strings.TrimSpace(value)129 if value != "" {130 return value131 }132 return fallback133}