d2js: add support for target, animateInterval, salt and noXMLTag
This commit is contained in:
parent
e42620ee15
commit
9a16c9ec18
6 changed files with 231 additions and 23 deletions
|
|
@ -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 ⛑️
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 `(<?xml ...?>)` from output SVG files. Useful when generating SVGs for direct HTML embedding.
|
||||
|
||||
### `CompileResult`
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="target-toggle" class="option-toggle-box" />
|
||||
<span>Target</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<input type="text" id="target-input" class="text-input" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="animate-interval-toggle"
|
||||
class="option-toggle-box"
|
||||
/>
|
||||
<span>Animate Interval</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<input
|
||||
type="number"
|
||||
id="animate-interval-input"
|
||||
value="0"
|
||||
step="100"
|
||||
min="0"
|
||||
class="number-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<span>Salt</span>
|
||||
<input type="text" id="salt-input" class="text-input" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="compile()">Compile</button>
|
||||
</div>
|
||||
|
|
@ -305,6 +357,13 @@
|
|||
const scale = document.getElementById("scale-toggle").checked
|
||||
? Number(document.getElementById("scale-input").value)
|
||||
: null;
|
||||
const target = document.getElementById("target-toggle").checked
|
||||
? String(document.getElementById("target-input").value)
|
||||
: null;
|
||||
const animateInterval = document.getElementById("animate-interval-toggle").checked
|
||||
? Number(document.getElementById("animate-interval-input").value)
|
||||
: null;
|
||||
const salt = String(document.getElementById("salt-input").value);
|
||||
try {
|
||||
const result = await d2.compile(input, {
|
||||
layout,
|
||||
|
|
@ -315,6 +374,10 @@
|
|||
pad,
|
||||
center,
|
||||
forceAppendix,
|
||||
target,
|
||||
animateInterval,
|
||||
salt,
|
||||
noXmlTag: true,
|
||||
});
|
||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
|
|
|
|||
|
|
@ -103,6 +103,14 @@ x -> y
|
|||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("no XML tag works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
const svg = await d2.render(result.diagram, { noXMLTag: true });
|
||||
expect(svg).not.toContain('<?xml version="1.0"');
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("force appendix works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x: {tooltip: x appendix}", { forceAppendix: true });
|
||||
|
|
|
|||
Loading…
Reference in a new issue