// Package cli wires the mopac-pdf pipeline: front-matter + markdown in, // typst template composition, pure-Go chart assets, digest-pinned engine // container, PDF bytes out. // // Exit codes: 0 ok, 1 usage/input error, 2 engine error. package cli import ( "fmt" "io" "os" "path/filepath" "strings" "ukrrs.com/mopac/pdf/internal/chart" "ukrrs.com/mopac/pdf/internal/engine" "ukrrs.com/mopac/pdf/internal/frontmatter" "ukrrs.com/mopac/pdf/internal/markdown" "ukrrs.com/mopac/pdf/internal/pdfinfo" "ukrrs.com/mopac/pdf/internal/typdoc" "ukrrs.com/mopac/pdf/templates" ) // Version is the CLI version. const Version = "0.1.0" const usage = `mopac-pdf (v` + Version + `) — markdown + front-matter in, beautiful PDF out Usage: mopac-pdf [flags] [INPUT.md] (default input: stdin) Flags: -o FILE write PDF to FILE (default: stdout) -t NAME template: report | brief (default: front-matter "template" key, else "report") -T DIR template directory override (DIR/NAME.typ wins over the embedded shipped templates) -V print version and exit -pages FILE print the page count of an existing PDF and exit (verification helper; tiny /Pages /Count parser) Input is markdown with optional front-matter (title, subtitle, author, date, classification, template). Fenced blocks tagged "chart" render as figures: ` + "```" + `chart type: bar title: Revenue by quarter unit: $K Q1: 120 Q2: 180 ` + "```" + ` Engine: digest-pinned typst container (override: MOPAC_PDF_ENGINE; docker binary override: MOPAC_PDF_DOCKER; keep compile root: MOPAC_PDF_KEEP=1). Exit codes: 0 ok, 1 usage/input error, 2 engine error. ` // Run is the CLI entry point; returns the process exit code. func Run(argv []string, stdout, stderr io.Writer) int { args := argv var outPath, tmplName, tmplDir, pagesPath string var showVersion bool rest := []string{} for i := 0; i < len(args); i++ { switch args[i] { case "-o": if i+1 >= len(args) { fmt.Fprintln(stderr, "mopac-pdf: -o needs a value") return 1 } i++ outPath = args[i] case "-t": if i+1 >= len(args) { fmt.Fprintln(stderr, "mopac-pdf: -t needs a value") return 1 } i++ tmplName = args[i] case "-T": if i+1 >= len(args) { fmt.Fprintln(stderr, "mopac-pdf: -T needs a value") return 1 } i++ tmplDir = args[i] case "-pages": if i+1 >= len(args) { fmt.Fprintln(stderr, "mopac-pdf: -pages needs a value") return 1 } i++ pagesPath = args[i] case "-V", "--version": showVersion = true case "-h", "--help": fmt.Fprint(stdout, usage) return 0 case "help": fmt.Fprint(stdout, usage) return 0 default: if strings.HasPrefix(args[i], "-") && args[i] != "-" { fmt.Fprintf(stderr, "mopac-pdf: unknown flag %q\n\n%s", args[i], usage) return 1 } rest = append(rest, args[i]) } } if showVersion { fmt.Fprintf(stdout, "mopac-pdf %s\n", Version) return 0 } if len(rest) > 1 { fmt.Fprintf(stderr, "mopac-pdf: unexpected argument %q (one input file)\n\n%s", rest[1], usage) return 1 } inPath := "-" if len(rest) == 1 { inPath = rest[0] } if pagesPath != "" { data, err := os.ReadFile(pagesPath) if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } n, err := pdfinfo.Pages(data) if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } fmt.Fprintf(stdout, "%d\n", n) return 0 } // Input. var input []byte var err error baseDir := "." if inPath == "-" && pagesPath == "" { input, err = io.ReadAll(os.Stdin) } else if inPath != "-" { input, err = os.ReadFile(inPath) baseDir = filepath.Dir(inPath) } if err != nil { fmt.Fprintf(stderr, "mopac-pdf: read input: %v\n", err) return 1 } meta, body, err := frontmatter.Split(string(input)) if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } if tmplName == "" { tmplName = meta.Template } if tmplName == "" { tmplName = "report" } tmplSrc, err := templates.Source(tmplDir, tmplName) if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } // Markdown -> typst (+ assets). blocks := markdown.Parse(body) assets := typdoc.Assets{} rendered, err := typdoc.RenderBody(blocks, assets, typdoc.Funcs{ RenderChart: func(c *markdown.Chart, _ string) ([]byte, error) { return chart.Bar(c) }, StageImage: func(path, _ string) ([]byte, error) { return os.ReadFile(filepath.Join(baseDir, path)) }, }) if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } doc := typdoc.Compose(tmplName, meta, rendered) // Compile root + engine. root, err := os.MkdirTemp("", "mopac-pdf-") if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } if os.Getenv("MOPAC_PDF_KEEP") == "1" { fmt.Fprintf(stderr, "mopac-pdf: compile root kept at %s\n", root) } else { defer os.RemoveAll(root) } if err := os.WriteFile(filepath.Join(root, "template.typ"), []byte(tmplSrc), 0o644); err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } if err := os.WriteFile(filepath.Join(root, "doc.typ"), []byte(doc), 0o644); err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 1 } pdf, err := engine.New().Compile(root, "doc.typ", assets) if err != nil { fmt.Fprintf(stderr, "mopac-pdf: %v\n", err) return 2 } // Output. if outPath != "" { if err := os.WriteFile(outPath, pdf, 0o644); err != nil { fmt.Fprintf(stderr, "mopac-pdf: write output: %v\n", err) return 1 } } else { if _, err := stdout.Write(pdf); err != nil { fmt.Fprintf(stderr, "mopac-pdf: write output: %v\n", err) return 1 } } return 0 }