From 7bea22e5049d149beb21e7d886a716dccee87d1f Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 28 Jul 2023 23:22:28 -0700 Subject: [PATCH 1/4] draft --- d2cli/main.go | 11 +++++--- d2cli/static/watch.js | 6 ++--- d2cli/watch.go | 18 ++++++++++--- d2target/d2target.go | 61 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 635a5e440..546a5a977 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -331,7 +331,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2) defer cancel() - _, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page) + _, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, "", *bundleFlag, *forceAppendixFlag, pw.Page) if err != nil { if written { return fmt.Errorf("failed to fully compile (partial render written): %w", err) @@ -366,7 +366,7 @@ func LayoutResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu } } -func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) { +func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath, boardPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) { start := time.Now() input, err := ms.ReadPath(inputPath) if err != nil { @@ -500,7 +500,12 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, la } } - boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram) + board := diagram.GetBoard(boardPath) + if board == nil { + return nil, false, fmt.Errorf("Diagram with path %s not found", boardPath) + } + + boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, board) if err != nil { return nil, false, err } diff --git a/d2cli/static/watch.js b/d2cli/static/watch.js index a95403971..9da3e630a 100644 --- a/d2cli/static/watch.js +++ b/d2cli/static/watch.js @@ -9,7 +9,7 @@ function init(reconnectDelay) { const devMode = document.body.dataset.d2DevMode === "true"; const ws = new WebSocket( - `ws://${window.location.host}${window.location.pathname}watch` + `ws://${window.location.host}/watch` ); let isInit = true; let ratio; @@ -28,7 +28,7 @@ function init(reconnectDelay) { // we can't just set `d2SVG.innerHTML = msg.svg` need to parse this as xml not html const parsedXML = new DOMParser().parseFromString(msg.svg, "text/xml"); d2SVG.replaceChildren(parsedXML.documentElement); - changeFavicon("./static/favicon.ico"); + changeFavicon("/static/favicon.ico"); const svgEl = d2SVG.querySelector("#d2-svg"); // just use inner SVG in watch mode svgEl.parentElement.replaceWith(svgEl); @@ -60,7 +60,7 @@ function init(reconnectDelay) { if (msg.err) { d2ErrDiv.innerText = msg.err; d2ErrDiv.style.display = "block"; - changeFavicon("./static/favicon-err.ico"); + changeFavicon("/static/favicon-err.ico"); d2ErrDiv.scrollIntoView(); } }; diff --git a/d2cli/watch.go b/d2cli/watch.go index 0fefabd61..b370a4d16 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "sync" "time" @@ -49,6 +50,7 @@ type watcherOpts struct { port string inputPath string outputPath string + boardPath string pwd string bundle bool forceAppendix bool @@ -362,7 +364,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { w.pw = newPW } - svg, _, err := compile(ctx, w.ms, w.plugins, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page) + svg, _, err := compile(ctx, w.ms, w.plugins, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, w.boardPath, w.bundle, w.forceAppendix, w.pw.Page) errs := "" if err != nil { if len(svg) > 0 { @@ -430,15 +432,23 @@ func (w *watcher) handleRoot(hw http.ResponseWriter, r *http.Request) { %s - - - + + +
`, filepath.Base(w.outputPath), w.devMode) + + // if path is "/x.svg", we just want "x" + split := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), ".") + boardPath := strings.Join(split[:len(split)-1], ".") + if boardPath != w.boardPath { + w.boardPath = boardPath + w.requestCompile() + } } func (w *watcher) handleWatch(hw http.ResponseWriter, r *http.Request) error { diff --git a/d2target/d2target.go b/d2target/d2target.go index 368e4c1a7..7bbfb63f8 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -6,6 +6,7 @@ import ( "hash/fnv" "math" "net/url" + "os" "strings" "oss.terrastruct.com/util-go/go2" @@ -72,6 +73,66 @@ type Diagram struct { Steps []*Diagram `json:"steps,omitempty"` } +// boardPath comes in the form of "x/layers/z/scenarios/a" +// or in the form of "layers/z/scenarios/a" +func (d *Diagram) GetBoard(boardPath string) *Diagram { + path := strings.Split(boardPath, string(os.PathSeparator)) + if len(path) == 0 || len(boardPath) == 0 { + return d + } + + head := path[0] + + if head == "index" { + return d + } + + switch head { + case "layers", "scenarios", "steps": + if len(path) < 2 { + return nil + } + } + + switch head { + case "layers": + for _, b := range d.Layers { + if b.Name == path[1] { + return b.GetBoard(strings.Join(path[2:], string(os.PathSeparator))) + } + } + case "scenarios": + for _, b := range d.Scenarios { + if b.Name == path[1] { + return b.GetBoard(strings.Join(path[2:], string(os.PathSeparator))) + } + } + case "steps": + for _, b := range d.Steps { + if b.Name == path[1] { + return b.GetBoard(strings.Join(path[2:], string(os.PathSeparator))) + } + } + } + + for _, b := range d.Layers { + if b.Name == head { + return b.GetBoard(strings.Join(path[1:], string(os.PathSeparator))) + } + } + for _, b := range d.Scenarios { + if b.Name == head { + return b.GetBoard(strings.Join(path[1:], string(os.PathSeparator))) + } + } + for _, b := range d.Steps { + if b.Name == head { + return b.GetBoard(strings.Join(path[1:], string(os.PathSeparator))) + } + } + return nil +} + func (diagram Diagram) Bytes() ([]byte, error) { b1, err := json.Marshal(diagram.Shapes) if err != nil { From 65f97fc7be23cdfa1bcad8dd6bdf31d1ff86c8ff Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 29 Jul 2023 09:16:45 -0700 Subject: [PATCH 2/4] pr comments --- d2cli/watch.go | 6 ++++-- d2target/d2target.go | 38 ++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/d2cli/watch.go b/d2cli/watch.go index b370a4d16..866a480b3 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -443,8 +443,10 @@ func (w *watcher) handleRoot(hw http.ResponseWriter, r *http.Request) { `, filepath.Base(w.outputPath), w.devMode) // if path is "/x.svg", we just want "x" - split := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), ".") - boardPath := strings.Join(split[:len(split)-1], ".") + boardPath := strings.TrimPrefix(r.URL.Path, "/") + if idx := strings.LastIndexByte(boardPath, '.'); idx != -1 { + boardPath = boardPath[:idx] + } if boardPath != w.boardPath { w.boardPath = boardPath w.requestCompile() diff --git a/d2target/d2target.go b/d2target/d2target.go index 7bbfb63f8..ea8e67b0b 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -81,53 +81,59 @@ func (d *Diagram) GetBoard(boardPath string) *Diagram { return d } - head := path[0] + return d.getBoard(path) +} + +func (d *Diagram) getBoard(boardPath []string) *Diagram { + head := boardPath[0] if head == "index" { return d } switch head { - case "layers", "scenarios", "steps": - if len(path) < 2 { + case "layers": + if len(boardPath) < 2 { return nil } - } - - switch head { - case "layers": for _, b := range d.Layers { - if b.Name == path[1] { - return b.GetBoard(strings.Join(path[2:], string(os.PathSeparator))) + if b.Name == boardPath[1] { + return b.getBoard(boardPath[2:]) } } case "scenarios": + if len(boardPath) < 2 { + return nil + } for _, b := range d.Scenarios { - if b.Name == path[1] { - return b.GetBoard(strings.Join(path[2:], string(os.PathSeparator))) + if b.Name == boardPath[1] { + return b.getBoard(boardPath[2:]) } } case "steps": + if len(boardPath) < 2 { + return nil + } for _, b := range d.Steps { - if b.Name == path[1] { - return b.GetBoard(strings.Join(path[2:], string(os.PathSeparator))) + if b.Name == boardPath[1] { + return b.getBoard(boardPath[2:]) } } } for _, b := range d.Layers { if b.Name == head { - return b.GetBoard(strings.Join(path[1:], string(os.PathSeparator))) + return b.getBoard(boardPath[2:]) } } for _, b := range d.Scenarios { if b.Name == head { - return b.GetBoard(strings.Join(path[1:], string(os.PathSeparator))) + return b.getBoard(boardPath[2:]) } } for _, b := range d.Steps { if b.Name == head { - return b.GetBoard(strings.Join(path[1:], string(os.PathSeparator))) + return b.getBoard(boardPath[2:]) } } return nil From 2ca39f8ed322c1190641496bd7318a504212616b Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 29 Jul 2023 09:17:24 -0700 Subject: [PATCH 3/4] fmt --- d2cli/static/watch.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/d2cli/static/watch.js b/d2cli/static/watch.js index 9da3e630a..4d91259eb 100644 --- a/d2cli/static/watch.js +++ b/d2cli/static/watch.js @@ -8,9 +8,7 @@ function init(reconnectDelay) { const d2SVG = window.document.querySelector("#d2-svg-container"); const devMode = document.body.dataset.d2DevMode === "true"; - const ws = new WebSocket( - `ws://${window.location.host}/watch` - ); + const ws = new WebSocket(`ws://${window.location.host}/watch`); let isInit = true; let ratio; ws.onopen = () => { From 11a436343e55456cf8cde8389a7f280aede11aca Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 29 Jul 2023 09:20:03 -0700 Subject: [PATCH 4/4] changelog --- ci/release/changelogs/next.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 67303ce99..b7032e194 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -8,6 +8,7 @@ Layout capability also takes a subtle but important step forward: you can now cu - Configure timeout value with D2_TIMEOUT env var [#1392](https://github.com/terrastruct/d2/pull/1392) - Scale renders and disable fit to screen with `--scale` flag [#1413](https://github.com/terrastruct/d2/pull/1413) - `null` keyword can be used to un-declare. See [docs](https://d2lang.com/tour/overrides#null) [#1446](https://github.com/terrastruct/d2/pull/1446) +- Develop multi-board diagrams in watch mode (links to layers/scenarios/steps work in `--watch`) [#1503](https://github.com/terrastruct/d2/pull/1503) #### Improvements 🧹