Skip to content

Commit 45b4b78

Browse files
committed
feat(render): add multi-format output and enhance JSON serialization
Add support for rendering to multiple output formats simultaneously and improve JSON output to enable complete round-trip serialization. Multi-format output (-f/--format flag): - Support comma-separated formats: svg, json, pdf, png - Combine with -t for multiple types: -t tower,nodelink -f svg,json - Smart output path handling: - No -o: derives from input (input.json → input.<format>) - Single format: uses exact path - Multiple formats: strips extension, adds format per file - Gracefully skip unsupported type/format combinations (e.g. nodelink/json) JSON sink enhancements for reproducibility: - Add render options: style, seed, randomize, merged - Add node flags: auxiliary, synthetic (for text skip logic) - Add URL field to Nebraska package rankings - These fields enable external tools to reproduce exact visual output Other changes: - Remove maintainer count from popup metadata (simplify PopupData struct) - Update README with Output Formats section and examples - Add "Adding a New Output Format" developer guide to README
1 parent 04217e2 commit 45b4b78

File tree

5 files changed

+515
-56
lines changed

5 files changed

+515
-56
lines changed

README.md

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,34 @@ stacktower render examples/real/yargs.json -t nodelink -o yargs.svg
160160
stacktower render examples/real/flask.json -t tower,nodelink -o flask
161161
```
162162

163+
#### Output Formats
164+
165+
```bash
166+
# SVG output (default)
167+
stacktower render examples/real/flask.json -o flask.svg
168+
169+
# JSON layout export (for external tools or re-rendering)
170+
stacktower render examples/real/flask.json -f json -o flask.json
171+
172+
# PDF output (requires librsvg: brew install librsvg)
173+
stacktower render examples/real/flask.json -f pdf -o flask.pdf
174+
175+
# PNG output with 2x scale (requires librsvg)
176+
stacktower render examples/real/flask.json -f png -o flask.png
177+
178+
# Multiple formats at once (outputs flask.svg, flask.json, flask.pdf)
179+
stacktower render examples/real/flask.json -f svg,json,pdf -o flask
180+
181+
# Combine multiple types and formats
182+
stacktower render examples/real/flask.json -t tower,nodelink -f svg,json
183+
```
184+
185+
Output path behavior:
186+
- **No `-o`**: Derives from input (`input.json``input.<format>`)
187+
- **Single format**: Uses exact path (`-o out.svg``out.svg`)
188+
- **Multiple formats**: Strips extension, adds format (`-o out.svg -f svg,json``out.svg`, `out.json`)
189+
- **Multiple types**: Adds type suffix (`-t tower,nodelink``out_tower.svg`, `out_nodelink.svg`)
190+
163191
### Included Examples
164192

165193
The repository ships with pre-parsed graphs so you can experiment immediately:
@@ -200,8 +228,9 @@ stacktower render examples/test/diamond.json -o diamond.svg
200228

201229
| Flag | Description |
202230
|------|-------------|
203-
| `-o`, `--output` | Output file or base path for multiple types |
204-
| `-t`, `--type` | Visualization type: `tower` (default), `nodelink` |
231+
| `-o`, `--output` | Output file or base path for multiple types/formats |
232+
| `-t`, `--type` | Visualization type(s): `tower` (default), `nodelink` (comma-separated) |
233+
| `-f`, `--format` | Output format(s): `svg` (default), `json`, `pdf`, `png` (comma-separated) |
205234
| `--normalize` | Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) |
206235

207236
#### Tower Options
@@ -436,6 +465,70 @@ func (p *MyLockParser) Parse(path string, opts deps.Options) (*deps.ManifestResu
436465

437466
The `deps.Registry` handles concurrent fetching with configurable depth/node limits and metadata enrichment automatically.
438467

468+
### Adding a New Output Format
469+
470+
Output formats are implemented as "sinks" in `pkg/render/tower/sink/`. Each sink takes a computed `layout.Layout` and renders it to bytes.
471+
472+
1. **Create a sink file** in `pkg/render/tower/sink/<format>.go`:
473+
474+
```go
475+
package sink
476+
477+
import "github.com/matzehuels/stacktower/pkg/render/tower/layout"
478+
479+
type MyFormatOption func(*myFormatRenderer)
480+
481+
type myFormatRenderer struct {
482+
// Configuration fields
483+
}
484+
485+
func WithMyFormatOption(val string) MyFormatOption {
486+
return func(r *myFormatRenderer) { r.option = val }
487+
}
488+
489+
func RenderMyFormat(l layout.Layout, opts ...MyFormatOption) ([]byte, error) {
490+
r := myFormatRenderer{}
491+
for _, opt := range opts {
492+
opt(&r)
493+
}
494+
495+
// Access layout data:
496+
// - l.FrameWidth, l.FrameHeight: canvas dimensions
497+
// - l.MarginX, l.MarginY: margins
498+
// - l.Blocks: map[string]Block with position data
499+
// - l.RowOrders: node ordering per row
500+
501+
// Generate output bytes
502+
return []byte("..."), nil
503+
}
504+
```
505+
506+
2. **Register in CLI** in `internal/cli/render.go`:
507+
508+
```go
509+
// Add to validFormats map
510+
var validFormats = map[string]bool{
511+
"svg": true, "json": true, "pdf": true, "png": true,
512+
"myformat": true, // Add your format
513+
}
514+
515+
// Add case in renderTower()
516+
switch format {
517+
case "myformat":
518+
logger.Info("Rendering tower as MyFormat")
519+
return sink.RenderMyFormat(l, buildMyFormatOpts(g, opts)...)
520+
// ...
521+
}
522+
```
523+
524+
The existing sinks provide examples:
525+
- **`svg.go`**: Full-featured SVG with styles, interactivity, and popups
526+
- **`json.go`**: Layout data export for external tools (round-trip capable)
527+
- **`pdf.go`**: Wrapper that converts SVG via `rsvg-convert`
528+
- **`png.go`**: Wrapper that converts SVG via `rsvg-convert` with scaling
529+
530+
The JSON format is designed as a complete serialization—it includes all render options (`style`, `seed`, `randomize`, `merged`) and node flags (`auxiliary`, `synthetic`) needed to reproduce the exact visual output.
531+
439532
## Development
440533

441534
```bash

0 commit comments

Comments
 (0)