From adf5556f584311966ccdc3e5936bd8e671e1b081 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 04:09:34 +0000 Subject: [PATCH 1/2] Move ltml/server to cmd/serve-ltml; add cmd/render-ltml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganise binary subprojects under the idiomatic cmd/ directory: - Rename ltml/server → cmd/serve-ltml (PLAN.md moved as-is) - Add cmd/render-ltml: CLI that parses an .ltml file, renders it to PDF via the ltml/ltpdf library, and writes to a file or stdout. Supports -assets , -output , and repeatable -extra flags for supplying assets to the renderer. https://claude.ai/code/session_017EMmKQBWPtydCTAk4YGYxk --- cmd/render-ltml/main.go | 159 ++++++++++++++++++++++++ {ltml/server => cmd/serve-ltml}/PLAN.md | 0 2 files changed, 159 insertions(+) create mode 100644 cmd/render-ltml/main.go rename {ltml/server => cmd/serve-ltml}/PLAN.md (100%) diff --git a/cmd/render-ltml/main.go b/cmd/render-ltml/main.go new file mode 100644 index 0000000..1b81742 --- /dev/null +++ b/cmd/render-ltml/main.go @@ -0,0 +1,159 @@ +// Copyright 2026 Brent Rowland. +// Use of this source code is governed the Apache License, Version 2.0, as described in the LICENSE file. + +package main + +import ( + "flag" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/rowland/leadtype/ltml" + "github.com/rowland/leadtype/ltml/ltpdf" +) + +type multiFlag []string + +func (m *multiFlag) String() string { + if m == nil || len(*m) == 0 { + return "" + } + return fmt.Sprintf("%v", []string(*m)) +} + +func (m *multiFlag) Set(v string) error { + *m = append(*m, v) + return nil +} + +func main() { + var assetsDir string + var outputPath string + var extraFiles multiFlag + + flag.StringVar(&assetsDir, "assets", "", "path to asset `directory`") + flag.StringVar(&outputPath, "output", "", "output `file` (default: stdout)") + flag.Var(&extraFiles, "extra", "additional asset `file` (may be repeated)") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: render-ltml [flags] \n\nFlags:\n") + flag.PrintDefaults() + } + flag.Parse() + + if flag.NArg() != 1 { + flag.Usage() + os.Exit(2) + } + + if err := run(flag.Arg(0), assetsDir, outputPath, []string(extraFiles)); err != nil { + fmt.Fprintf(os.Stderr, "render-ltml: %v\n", err) + os.Exit(1) + } +} + +func run(inputFile, assetsDir, outputPath string, extraFiles []string) error { + // Resolve paths before potentially changing directory. + absInput, err := filepath.Abs(inputFile) + if err != nil { + return fmt.Errorf("resolving input: %w", err) + } + + var absOutput string + if outputPath != "" { + absOutput, err = filepath.Abs(outputPath) + if err != nil { + return fmt.Errorf("resolving output: %w", err) + } + } + + // Set up asset working directory when assets or extra files are provided. + if assetsDir != "" || len(extraFiles) > 0 { + workDir, cleanup, err := setupWorkDir(assetsDir, extraFiles) + if err != nil { + return err + } + defer cleanup() + if err := os.Chdir(workDir); err != nil { + return fmt.Errorf("chdir to work dir: %w", err) + } + } + + // Parse LTML. + doc, err := ltml.ParseFile(absInput) + if err != nil { + return fmt.Errorf("parsing %s: %w", inputFile, err) + } + + // Render to PDF. + w := ltpdf.NewDocWriter() + if err := doc.Print(w); err != nil { + return fmt.Errorf("rendering: %w", err) + } + + // Write output. + var out io.Writer + if absOutput != "" { + f, err := os.Create(absOutput) + if err != nil { + return fmt.Errorf("creating output: %w", err) + } + defer f.Close() + out = f + } else { + out = os.Stdout + } + + if _, err := w.WriteTo(out); err != nil { + return fmt.Errorf("writing output: %w", err) + } + return nil +} + +// setupWorkDir creates a temporary directory populated with symlinks to the +// contents of assetsDir and each file in extraFiles. The caller must invoke +// the returned cleanup function when rendering is complete. +func setupWorkDir(assetsDir string, extraFiles []string) (string, func(), error) { + tmpDir, err := os.MkdirTemp("", "render-ltml-*") + if err != nil { + return "", nil, fmt.Errorf("creating work dir: %w", err) + } + cleanup := func() { os.RemoveAll(tmpDir) } + + if assetsDir != "" { + absAssets, err := filepath.Abs(assetsDir) + if err != nil { + cleanup() + return "", nil, fmt.Errorf("resolving assets dir: %w", err) + } + entries, err := os.ReadDir(absAssets) + if err != nil { + cleanup() + return "", nil, fmt.Errorf("reading assets dir: %w", err) + } + for _, entry := range entries { + src := filepath.Join(absAssets, entry.Name()) + dst := filepath.Join(tmpDir, entry.Name()) + if err := os.Symlink(src, dst); err != nil { + cleanup() + return "", nil, fmt.Errorf("linking asset %s: %w", entry.Name(), err) + } + } + } + + for _, f := range extraFiles { + abs, err := filepath.Abs(f) + if err != nil { + cleanup() + return "", nil, fmt.Errorf("resolving extra file %s: %w", f, err) + } + dst := filepath.Join(tmpDir, filepath.Base(f)) + if err := os.Symlink(abs, dst); err != nil { + cleanup() + return "", nil, fmt.Errorf("linking extra file %s: %w", filepath.Base(f), err) + } + } + + return tmpDir, cleanup, nil +} diff --git a/ltml/server/PLAN.md b/cmd/serve-ltml/PLAN.md similarity index 100% rename from ltml/server/PLAN.md rename to cmd/serve-ltml/PLAN.md From dfd51b8acf0e63493b27441a7ebe7e3e0109a450 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 04:33:44 +0000 Subject: [PATCH 2/2] Add short flag aliases -a, -o, -e to render-ltml https://claude.ai/code/session_017EMmKQBWPtydCTAk4YGYxk --- cmd/render-ltml/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/render-ltml/main.go b/cmd/render-ltml/main.go index 1b81742..e80db30 100644 --- a/cmd/render-ltml/main.go +++ b/cmd/render-ltml/main.go @@ -34,8 +34,11 @@ func main() { var extraFiles multiFlag flag.StringVar(&assetsDir, "assets", "", "path to asset `directory`") + flag.StringVar(&assetsDir, "a", "", "path to asset `directory` (shorthand)") flag.StringVar(&outputPath, "output", "", "output `file` (default: stdout)") + flag.StringVar(&outputPath, "o", "", "output `file` (shorthand)") flag.Var(&extraFiles, "extra", "additional asset `file` (may be repeated)") + flag.Var(&extraFiles, "e", "additional asset `file` (shorthand)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: render-ltml [flags] \n\nFlags:\n") flag.PrintDefaults()