Skip to content

Commit

Permalink
Merge pull request #1725 from landmaj/board_flag
Browse files Browse the repository at this point in the history
cli: add --target flag
  • Loading branch information
gavin-ts authored Nov 28, 2023
2 parents 6865181 + de4b088 commit 9925980
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 28 deletions.
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- ELK now routes `sql_table` edges to the exact columns (ty @landmaj) [#1681](https://github.com/terrastruct/d2/pull/1681)
- Adds new unfilled triangle arrowhead. [#1711](https://github.com/terrastruct/d2/pull/1711)
- Grid containers can now have custom label positions. [#1715](https://github.com/terrastruct/d2/pull/1715)
- A single board from a multi-board diagram can now be rendered with `--target` flag. [#1725](https://github.com/terrastruct/d2/pull/1725)

#### Improvements 🧹

Expand Down
5 changes: 5 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ Bundle all assets and layers into the output svg
.It Fl -force-appendix Ar false
An appendix for tooltips and links is added to PNG exports since they are not interactive. Setting this to true adds an appendix to SVG exports as well
.Ns .
.It Fl -target
Target board to render. Pass an empty string to target root board. If target ends with '*', it will be rendered
with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. --target=''
to render root board only or --target='layers.x.*' to render layer 'x' with all of its children
.Ns .
.It Fl d , -debug
Print debug logs
.Ns .
Expand Down
62 changes: 55 additions & 7 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
targetFlag := ms.Opts.String("", "target", "", "*", "target board to render. Pass an empty string to target root board. If target ends with '*', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. --target='' to render root board only or --target='layers.x.*' to render layer 'x' with all of its children.")

fontRegularFlag := ms.Opts.String("D2_FONT_REGULAR", "font-regular", "", "", "path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.")
fontItalicFlag := ms.Opts.String("D2_FONT_ITALIC", "font-italic", "", "", "path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used.")
Expand Down Expand Up @@ -312,6 +313,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
}
if *targetFlag != "*" {
return xmain.UsageErrorf("-w[atch] cannot be combined with --target")
}
w, err := newWatcher(ctx, ms, watcherOpts{
plugins: plugins,
layout: layoutFlag,
Expand All @@ -332,10 +336,30 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
return w.run()
}

var boardPath []string
var noChildren bool
switch *targetFlag {
case "*":
case "":
noChildren = true
default:
target := *targetFlag
if strings.HasSuffix(target, ".*") {
target = target[:len(target)-2]
} else {
noChildren = true
}
key, err := d2parser.ParseKey(target)
if err != nil {
return xmain.UsageErrorf("invalid target: %s", *targetFlag)
}
boardPath = key.IDA()
}

ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
defer cancel()

_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, "", *bundleFlag, *forceAppendixFlag, pw.Page)
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
if err != nil {
if written {
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
Expand Down Expand Up @@ -410,7 +434,7 @@ func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu
}
}

func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath, boardPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
start := time.Now()
input, err := ms.ReadPath(inputPath)
if err != nil {
Expand Down Expand Up @@ -463,6 +487,16 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
}
cancel()

diagram = diagram.GetBoard(boardPath)
if diagram == nil {
return nil, false, fmt.Errorf(`render target "%s" not found`, strings.Join(boardPath, "."))
}
if noChildren {
diagram.Layers = nil
diagram.Scenarios = nil
diagram.Steps = nil
}

plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)

if animateInterval > 0 {
Expand Down Expand Up @@ -566,12 +600,13 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
}
}

board := diagram.GetBoard(boardPath)
if board == nil {
return nil, false, fmt.Errorf(`Diagram with path "%s" not found. Did you mean to specify a board like "layers.%s"?`, boardPath, boardPath)
var boards [][]byte
var err error
if noChildren {
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
} else {
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
}

boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, board)
if err != nil {
return nil, false, err
}
Expand Down Expand Up @@ -788,6 +823,19 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
return boards, nil
}

func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
start := time.Now()
out, err := _render(ctx, ms, plugin, opts, outputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return [][]byte{}, err
}
dur := compileDur + time.Since(start)
if opts.MasterID == "" {
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
}
return [][]byte{out}, nil
}

func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := getExportExtension(outputPath) == PNG
var scale *float64
Expand Down
6 changes: 5 additions & 1 deletion d2cli/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,11 @@ func (w *watcher) compileLoop(ctx context.Context) error {

fs := trackedFS{}
w.boardpathMu.Lock()
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, w.boardPath, w.bundle, w.forceAppendix, w.pw.Page)
var boardPath []string
if w.boardPath != "" {
boardPath = strings.Split(w.boardPath, string(os.PathSeparator))
}
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page)
w.boardpathMu.Unlock()
errs := ""
if err != nil {
Expand Down
27 changes: 7 additions & 20 deletions d2target/d2target.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"hash/fnv"
"math"
"net/url"
"os"
"strings"

"oss.terrastruct.com/util-go/go2"
Expand Down Expand Up @@ -73,19 +72,7 @@ type Diagram struct {
Steps []*Diagram `json:"steps,omitempty"`
}

// boardPath comes in the form of "x/layers/z/scenarios/a"
// or "layers/z/scenarios/a"
// or "x/z/a"
func (d *Diagram) GetBoard(boardPath string) *Diagram {
path := strings.Split(boardPath, string(os.PathSeparator))
if len(path) == 0 || len(boardPath) == 0 {
return d
}

return d.getBoard(path)
}

func (d *Diagram) getBoard(boardPath []string) *Diagram {
func (d *Diagram) GetBoard(boardPath []string) *Diagram {
if len(boardPath) == 0 {
return d
}
Expand All @@ -103,7 +90,7 @@ func (d *Diagram) getBoard(boardPath []string) *Diagram {
}
for _, b := range d.Layers {
if b.Name == boardPath[1] {
return b.getBoard(boardPath[2:])
return b.GetBoard(boardPath[2:])
}
}
case "scenarios":
Expand All @@ -112,7 +99,7 @@ func (d *Diagram) getBoard(boardPath []string) *Diagram {
}
for _, b := range d.Scenarios {
if b.Name == boardPath[1] {
return b.getBoard(boardPath[2:])
return b.GetBoard(boardPath[2:])
}
}
case "steps":
Expand All @@ -121,24 +108,24 @@ func (d *Diagram) getBoard(boardPath []string) *Diagram {
}
for _, b := range d.Steps {
if b.Name == boardPath[1] {
return b.getBoard(boardPath[2:])
return b.GetBoard(boardPath[2:])
}
}
}

for _, b := range d.Layers {
if b.Name == head {
return b.getBoard(boardPath[1:])
return b.GetBoard(boardPath[1:])
}
}
for _, b := range d.Scenarios {
if b.Name == head {
return b.getBoard(boardPath[1:])
return b.GetBoard(boardPath[1:])
}
}
for _, b := range d.Steps {
if b.Name == head {
return b.getBoard(boardPath[1:])
return b.GetBoard(boardPath[1:])
}
}
return nil
Expand Down
60 changes: 60 additions & 0 deletions e2etests-cli/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,66 @@ You provided: .png`)
testdataIgnoreDiff(t, ".png", png)
},
},
{
name: "target-root",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "target-root.d2", `title: {
label: Main Plan
}
scenarios: {
b: {
title.label: Backup Plan
}
}`)
err := runTestMain(t, ctx, dir, env, "--target", "", "target-root.d2", "target-root.svg")
assert.Success(t, err)
svg := readFile(t, dir, "target-root.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "target-b",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "target-b.d2", `title: {
label: Main Plan
}
scenarios: {
b: {
title.label: Backup Plan
}
}`)
err := runTestMain(t, ctx, dir, env, "--target", "b", "target-b.d2", "target-b.svg")
assert.Success(t, err)
svg := readFile(t, dir, "target-b.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "target-nested-with-special-chars",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "target-nested-with-special-chars.d2", `layers: {
a: {
layers: {
"x / y . z": {
mad
}
}
}
}`)
err := runTestMain(t, ctx, dir, env, "--target", `layers.a.layers."x / y . z"`, "target-nested-with-special-chars.d2", "target-nested-with-special-chars.svg")
assert.Success(t, err)
svg := readFile(t, dir, "target-nested-with-special-chars.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "target-invalid",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "target-invalid.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "--target", "b", "target-invalid.d2", "target-invalid.svg")
assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: failed to compile target-invalid.d2: render target "b" not found`)
},
},
{
name: "multiboard/life",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
Expand Down
Loading

0 comments on commit 9925980

Please sign in to comment.