diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index c7fb38b55..35f5b8b2a 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -4,7 +4,17 @@ #### Improvements 🧹 -d2js: Support additional render options (`themeID`, `darkThemeID`, `center`, `pad`, `scale` and `forceAppendix`). Support `d2-config`. [#2343](https://github.com/terrastruct/d2/pull/2343) +- d2js: Support `d2-config`. Support additional render options: [#2343](https://github.com/terrastruct/d2/pull/2343) + - `themeID` + - `darkThemeID` + - `center` + - `pad` + - `scale` + - `forceAppendix` + - `target` + - `animateInterval` + - `salt` + - `noXMLTag` #### Bugfixes ⛑️ diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go index 3b76f50e2..97834161c 100644 --- a/d2js/d2wasm/functions.go +++ b/d2js/d2wasm/functions.go @@ -19,9 +19,11 @@ import ( "oss.terrastruct.com/d2/d2lsp" "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/d2renderers/d2animate" "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/d2svg/appendix" + "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/log" "oss.terrastruct.com/d2/lib/memfs" "oss.terrastruct.com/d2/lib/textmeasure" @@ -241,13 +243,17 @@ func Compile(args []js.Value) (interface{}, error) { Diagram: *diagram, Graph: *g, RenderOptions: RenderOptions{ - ThemeID: renderOpts.ThemeID, - DarkThemeID: renderOpts.DarkThemeID, - Sketch: renderOpts.Sketch, - Pad: renderOpts.Pad, - Center: renderOpts.Center, - Scale: renderOpts.Scale, - ForceAppendix: input.Opts.ForceAppendix, + ThemeID: renderOpts.ThemeID, + DarkThemeID: renderOpts.DarkThemeID, + Sketch: renderOpts.Sketch, + Pad: renderOpts.Pad, + Center: renderOpts.Center, + Scale: renderOpts.Scale, + ForceAppendix: input.Opts.ForceAppendix, + Target: input.Opts.Target, + AnimateInterval: input.Opts.AnimateInterval, + Salt: input.Opts.Salt, + NoXMLTag: input.Opts.NoXMLTag, }, }, nil } @@ -265,12 +271,60 @@ func Render(args []js.Value) (interface{}, error) { return nil, &WASMError{Message: "missing 'diagram' field in input JSON", Code: 400} } + var boardPath []string + var noChildren bool + + if input.Opts.Target != nil { + switch *input.Opts.Target { + case "*": + case "": + noChildren = true + default: + target := *input.Opts.Target + if strings.HasSuffix(target, ".*") { + target = target[:len(target)-2] + } else { + noChildren = true + } + key, err := d2parser.ParseKey(target) + if err != nil { + return nil, &WASMError{Message: fmt.Sprintf("target '%s' not recognized", target), Code: 400} + } + boardPath = key.StringIDA() + } + } + + diagram := input.Diagram.GetBoard(boardPath) + if diagram == nil { + return nil, &WASMError{Message: fmt.Sprintf("render target '%s' not found", strings.Join(boardPath, ".")), Code: 400} + } + if noChildren { + diagram.Layers = nil + diagram.Scenarios = nil + diagram.Steps = nil + } + + renderOpts := &d2svg.RenderOpts{} + + if input.Opts != nil && input.Opts.Salt != nil { + renderOpts.Salt = input.Opts.Salt + } + + var animateInterval = 0 + if input.Opts != nil && input.Opts.AnimateInterval != nil && *input.Opts.AnimateInterval > 0 { + animateInterval = int(*input.Opts.AnimateInterval) + masterID, err := diagram.HashID(renderOpts.Salt) + if err != nil { + return nil, &WASMError{Message: fmt.Sprintf("cannot process animate interval: %s", err.Error()), Code: 500} + } + renderOpts.MasterID = masterID + } + ruler, err := textmeasure.NewRuler() if err != nil { return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500} } - renderOpts := &d2svg.RenderOpts{} if input.Opts != nil && input.Opts.Sketch != nil { renderOpts.Sketch = input.Opts.Sketch } @@ -289,15 +343,80 @@ func Render(args []js.Value) (interface{}, error) { if input.Opts != nil && input.Opts.Scale != nil { renderOpts.Scale = input.Opts.Scale } - out, err := d2svg.Render(input.Diagram, renderOpts) + if input.Opts != nil && input.Opts.NoXMLTag != nil { + renderOpts.NoXMLTag = input.Opts.NoXMLTag + } + + forceAppendix := input.Opts != nil && input.Opts.ForceAppendix != nil && *input.Opts.ForceAppendix + + var boards [][]byte + if noChildren { + var board []byte + board, err = renderSingleBoard(renderOpts, forceAppendix, ruler, diagram) + boards = [][]byte{board} + } else { + boards, err = renderBoards(renderOpts, forceAppendix, ruler, diagram) + } if err != nil { return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500} } - if input.Opts != nil && input.Opts.ForceAppendix != nil && *input.Opts.ForceAppendix { - out = appendix.Append(input.Diagram, renderOpts, ruler, out) + + var out []byte + if len(boards) > 0 { + out = boards[0] + if animateInterval > 0 { + out, err = d2animate.Wrap(diagram, boards, *renderOpts, animateInterval) + if err != nil { + return nil, &WASMError{Message: fmt.Sprintf("animation failed: %s", err.Error()), Code: 500} + } + } + } + return out, nil +} + +func renderSingleBoard(opts *d2svg.RenderOpts, forceAppendix bool, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) { + out, err := d2svg.Render(diagram, opts) + if err != nil { + return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500} + } + if forceAppendix { + out = appendix.Append(diagram, opts, ruler, out) + } + return out, nil +} + +func renderBoards(opts *d2svg.RenderOpts, forceAppendix bool, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) { + var boards [][]byte + for _, dl := range diagram.Layers { + childrenBoards, err := renderBoards(opts, forceAppendix, ruler, dl) + if err != nil { + return nil, err + } + boards = append(boards, childrenBoards...) + } + for _, dl := range diagram.Scenarios { + childrenBoards, err := renderBoards(opts, forceAppendix, ruler, dl) + if err != nil { + return nil, err + } + boards = append(boards, childrenBoards...) + } + for _, dl := range diagram.Steps { + childrenBoards, err := renderBoards(opts, forceAppendix, ruler, dl) + if err != nil { + return nil, err + } + boards = append(boards, childrenBoards...) } - return out, nil + if !diagram.IsFolderOnly { + out, err := renderSingleBoard(opts, forceAppendix, ruler, diagram) + if err != nil { + return boards, err + } + boards = append([][]byte{out}, boards...) + } + return boards, nil } func GetBoardAtPosition(args []js.Value) (interface{}, error) { diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go index 592facbf1..70b3e1154 100644 --- a/d2js/d2wasm/types.go +++ b/d2js/d2wasm/types.go @@ -37,13 +37,17 @@ type CompileRequest struct { } type RenderOptions struct { - Pad *int64 `json:"pad"` - Sketch *bool `json:"sketch"` - Center *bool `json:"center"` - ThemeID *int64 `json:"themeID"` - DarkThemeID *int64 `json:"darkThemeID"` - Scale *float64 `json:"scale"` - ForceAppendix *bool `json:"forceAppendix"` + Pad *int64 `json:"pad"` + Sketch *bool `json:"sketch"` + Center *bool `json:"center"` + ThemeID *int64 `json:"themeID"` + DarkThemeID *int64 `json:"darkThemeID"` + Scale *float64 `json:"scale"` + ForceAppendix *bool `json:"forceAppendix"` + Target *string `json:"target"` + AnimateInterval *int64 `json:"animateInterval"` + Salt *string `json:"salt"` + NoXMLTag *bool `json:"noXMLTag"` } type CompileOptions struct { diff --git a/d2js/js/README.md b/d2js/js/README.md index df290a82d..e9fd6194c 100644 --- a/d2js/js/README.md +++ b/d2js/js/README.md @@ -87,6 +87,10 @@ All [RenderOptions](#renderoptions) properties in addition to: - `pad`: Pixels padded around the rendered diagram [default: 100] - `scale`: Scale the output. E.g., 0.5 to halve the default size. The default will render SVG's that will fit to screen. Setting to 1 turns off SVG fitting to screen. - `forceAppendix`: Adds an appendix for tooltips and links [default: false] +- `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. +- `animateInterval`: If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). +- `salt`: Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate IDs do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output. +- `noXMLTag`: Omit XML tag `()` from output SVG files. Useful when generating SVGs for direct HTML embedding. ### `CompileResult` diff --git a/d2js/js/examples/customizable.html b/d2js/js/examples/customizable.html index b268bc548..780bdcefe 100644 --- a/d2js/js/examples/customizable.html +++ b/d2js/js/examples/customizable.html @@ -36,9 +36,11 @@ border-radius: 4px; } - .option:not(:has(.option-toggle-box:checked)) .option-select { - opacity: 0.5; - pointer-events: none; + .option(:has(.option-toggle-box)) { + .option:not(:has(.option-toggle-box:checked)) .option-select { + opacity: 0.5; + pointer-events: none; + } } .option { @@ -53,9 +55,14 @@ display: flex; gap: 4px; align-items: center; + } + + .checkbox-label, + .select-label { cursor: pointer; } + .text-input, .number-input { width: 3rem; } @@ -269,6 +276,51 @@ +