Merge pull request #2343 from x-delfino/wasm-options

d2js: Additional render options
This commit is contained in:
Alexander Wang 2025-02-25 07:31:57 -08:00 committed by GitHub
commit c09d29fb17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 703 additions and 85 deletions

View file

@ -4,6 +4,18 @@
#### Improvements 🧹
- d2js: Support `d2-config`. Support additional options: [#2343](https://github.com/terrastruct/d2/pull/2343)
- `themeID`
- `darkThemeID`
- `center`
- `pad`
- `scale`
- `forceAppendix`
- `target`
- `animateInterval`
- `salt`
- `noXMLTag`
#### Bugfixes ⛑️
- Compiler:

View file

@ -19,8 +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"
@ -170,47 +173,61 @@ func Compile(args []js.Value) (interface{}, error) {
return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400}
}
fs, err := memfs.New(input.FS)
compileOpts := &d2lib.CompileOptions{
UTF16Pos: true,
}
compileOpts.LayoutResolver = func(engine string) (d2graph.LayoutGraph, error) {
switch engine {
case "dagre":
return d2dagrelayout.DefaultLayout, nil
case "elk":
return d2elklayout.DefaultLayout, nil
default:
return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", engine), Code: 400}
}
}
var err error
compileOpts.FS, err = memfs.New(input.FS)
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
}
ruler, err := textmeasure.NewRuler()
compileOpts.Ruler, err = textmeasure.NewRuler()
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
}
ctx := log.WithDefault(context.Background())
layoutFunc := d2dagrelayout.DefaultLayout
if input.Opts != nil && input.Opts.Layout != nil {
switch *input.Opts.Layout {
case "dagre":
layoutFunc = d2dagrelayout.DefaultLayout
case "elk":
layoutFunc = d2elklayout.DefaultLayout
default:
return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", *input.Opts.Layout), Code: 400}
}
}
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
return layoutFunc, nil
compileOpts.Layout = input.Opts.Layout
}
renderOpts := &d2svg.RenderOpts{}
var fontFamily *d2fonts.FontFamily
if input.Opts != nil && input.Opts.Sketch != nil && *input.Opts.Sketch {
fontFamily = go2.Pointer(d2fonts.HandDrawn)
if input.Opts != nil && input.Opts.Sketch != nil {
renderOpts.Sketch = input.Opts.Sketch
if *input.Opts.Sketch {
compileOpts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
}
}
if input.Opts != nil && input.Opts.Pad != nil {
renderOpts.Pad = input.Opts.Pad
}
if input.Opts != nil && input.Opts.Center != nil {
renderOpts.Center = input.Opts.Center
}
if input.Opts != nil && input.Opts.ThemeID != nil {
renderOpts.ThemeID = input.Opts.ThemeID
}
diagram, g, err := d2lib.Compile(ctx, input.FS["index"], &d2lib.CompileOptions{
UTF16Pos: true,
FS: fs,
Ruler: ruler,
LayoutResolver: layoutResolver,
FontFamily: fontFamily,
}, renderOpts)
if input.Opts != nil && input.Opts.DarkThemeID != nil {
renderOpts.DarkThemeID = input.Opts.DarkThemeID
}
if input.Opts != nil && input.Opts.Scale != nil {
renderOpts.Scale = input.Opts.Scale
}
ctx := log.WithDefault(context.Background())
diagram, g, err := d2lib.Compile(ctx, input.FS["index"], compileOpts, renderOpts)
if err != nil {
if pe, ok := err.(*d2parser.ParseError); ok {
errs, _ := json.Marshal(pe.Errors)
@ -225,6 +242,19 @@ func Compile(args []js.Value) (interface{}, error) {
FS: input.FS,
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,
Target: input.Opts.Target,
AnimateInterval: input.Opts.AnimateInterval,
Salt: input.Opts.Salt,
NoXMLTag: input.Opts.NoXMLTag,
},
}, nil
}
@ -241,21 +271,159 @@ func Render(args []js.Value) (interface{}, error) {
return nil, &WASMError{Message: "missing 'diagram' field in input JSON", Code: 400}
}
animateInterval := 0
if input.Opts != nil && input.Opts.AnimateInterval != nil && *input.Opts.AnimateInterval > 0 {
animateInterval = int(*input.Opts.AnimateInterval)
}
var boardPath []string
noChildren := true
if input.Opts.Target != nil {
switch *input.Opts.Target {
case "*":
noChildren = false
case "":
default:
target := *input.Opts.Target
if strings.HasSuffix(target, ".*") {
target = target[:len(target)-2]
noChildren = false
}
key, err := d2parser.ParseKey(target)
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("target '%s' not recognized", target), Code: 400}
}
boardPath = key.StringIDA()
}
if !noChildren && animateInterval <= 0 {
return nil, &WASMError{Message: fmt.Sprintf("target '%s' only supported for animated SVGs", *input.Opts.Target), Code: 500}
}
}
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
}
if animateInterval > 0 {
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}
}
if input.Opts != nil && input.Opts.Sketch != nil {
renderOpts.Sketch = input.Opts.Sketch
}
if input.Opts != nil && input.Opts.Pad != nil {
renderOpts.Pad = input.Opts.Pad
}
if input.Opts != nil && input.Opts.Center != nil {
renderOpts.Center = input.Opts.Center
}
if input.Opts != nil && input.Opts.ThemeID != nil {
renderOpts.ThemeID = input.Opts.ThemeID
}
out, err := d2svg.Render(input.Diagram, renderOpts)
if input.Opts != nil && input.Opts.DarkThemeID != nil {
renderOpts.DarkThemeID = input.Opts.DarkThemeID
}
if input.Opts != nil && input.Opts.Scale != nil {
renderOpts.Scale = input.Opts.Scale
}
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}
}
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...)
}
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) {
if len(args) < 3 {
return nil, &WASMError{Message: "missing required arguments", Code: 400}

View file

@ -33,19 +33,33 @@ type BoardPositionResponse struct {
type CompileRequest struct {
FS map[string]string `json:"fs"`
Opts *RenderOptions `json:"options"`
Opts *CompileOptions `json:"options"`
}
type RenderOptions struct {
Layout *string `json:"layout"`
Sketch *bool `json:"sketch"`
ThemeID *int64 `json:"themeID"`
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 {
RenderOptions
Layout *string `json:"layout"`
}
type CompileResponse struct {
FS map[string]string `json:"fs"`
Diagram d2target.Diagram `json:"diagram"`
Graph d2graph.Graph `json:"graph"`
FS map[string]string `json:"fs"`
Diagram d2target.Diagram `json:"diagram"`
Graph d2graph.Graph `json:"graph"`
RenderOptions RenderOptions `json:"renderOptions"`
}
type CompletionResponse struct {

View file

@ -42,24 +42,63 @@ import { D2 } from '@terrastruct/d2';
const d2 = new D2();
const result = await d2.compile('x -> y');
const svg = await d2.render(result.diagram);
const svg = await d2.render(result.diagram, result.options);
```
Configuring render options (see [CompileOptions](#compileoptions) for all available options):
```javascript
import { D2 } from '@terrastruct/d2';
const d2 = new D2();
const result = await d2.compile('x -> y', {
sketch: true,
});
const svg = await d2.render(result.diagram, result.renderOptions);
```
## API Reference
### `new D2()`
Creates a new D2 instance.
### `compile(input: string, options?: CompileOptions): Promise<CompileResult>`
Compiles D2 markup into an intermediate representation.
Options:
- `layout`: Layout engine to use ('dagre' | 'elk') [default: 'dagre']
- `sketch`: Enable sketch mode [default: false]
### `render(diagram: Diagram, options?: RenderOptions): Promise<string>`
Renders a compiled diagram to SVG.
### `CompileOptions`
All [RenderOptions](#renderoptions) properties in addition to:
- `layout`: Layout engine to use ('dagre' | 'elk') [default: 'dagre']
### `RenderOptions`
- `sketch`: Enable sketch mode [default: false]
- `themeID`: Theme ID to use [default: 0]
- `darkThemeID`: Theme ID to use when client is in dark mode
- `center`: Center the SVG in the containing viewbox [default: false]
- `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/s to render. 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: 'layers.x.*'` to render layer 'x' with all of its children. Pass '*' to render all scenarios, steps, and layers. By default, only the root board is rendered. Multi-board outputs are currently only supported for animated SVGs and so `animateInterval` must be set to a value greater than 0 when targeting multiple boards.
- `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`
- `diagram`: `Diagram`: Compiled D2 diagram
- `options`: `RenderOptions`: Render options merged with configuration set in diagram
- `fs`
- `graph`
## Development
D2.js uses Bun, so install this first.

View file

@ -10,12 +10,14 @@
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
}
.controls {
display: flex;
flex-direction: column;
gap: 12px;
width: 400px;
}
textarea {
width: 100%;
height: 300px;
@ -24,6 +26,7 @@
border-radius: 4px;
font-family: monospace;
}
.options-group {
display: flex;
flex-direction: column;
@ -32,23 +35,36 @@
border: 1px solid #eee;
border-radius: 4px;
}
.layout-toggle,
.sketch-toggle {
.option:has(.option-toggle-box:not(:checked)) .option-select {
opacity: 0.5;
pointer-events: none;
}
.option {
display: flex;
gap: 16px;
align-items: center;
}
.radio-group {
display: flex;
gap: 12px;
}
.radio-label,
.checkbox-label {
.input-label,
.checkbox-label,
.select-label {
display: flex;
gap: 4px;
align-items: center;
}
.checkbox-label,
.select-label {
cursor: pointer;
}
.text-input,
.number-input {
width: 3rem;
}
button {
padding: 8px 16px;
background: #0066cc;
@ -57,9 +73,11 @@
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0052a3;
}
#output {
flex: 1;
overflow: auto;
@ -67,34 +85,239 @@
border-radius: 4px;
padding: 16px;
}
#output svg {
max-width: 100%;
min-width: 100%;
max-height: 90vh;
}
</style>
</head>
<body>
<div class="controls">
<textarea id="input">x -> y</textarea>
<div class="options-group">
<div class="layout-toggle">
<span>Layout:</span>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="layout" value="dagre" checked />
Dagre
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="layout-toggle" class="option-toggle-box" />
<span>Layout</span>
</label>
<label class="radio-label">
<input type="radio" name="layout" value="elk" />
ELK
</div>
<div class="option-select">
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="layout-select" value="dagre" checked />
Dagre
</label>
<label class="radio-label">
<input type="radio" name="layout-select" value="elk" />
ELK
</label>
</div>
</div>
</div>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="sketch-toggle" class="option-toggle-box" />
<span>Sketch Mode</span>
</label>
</div>
<div class="option-select">
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="sketch-select" value="true" checked />
Enabled
</label>
<label class="radio-label">
<input type="radio" name="sketch-select" value="false" />
Disabled
</label>
</div>
</div>
</div>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="center-toggle" class="option-toggle-box" />
<span>Centered</span>
</label>
</div>
<div class="option-select">
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="center-select" value="true" checked />
Enabled
</label>
<label class="radio-label">
<input type="radio" name="center-select" value="false" />
Disabled
</label>
</div>
</div>
</div>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="appendix-toggle" class="option-toggle-box" />
<span>Force Appendix</span>
</label>
</div>
<div class="option-select">
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="appendix-select" value="true" checked />
Enabled
</label>
<label class="radio-label">
<input type="radio" name="appendix-select" value="false" />
Disabled
</label>
</div>
</div>
</div>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="theme-toggle" class="option-toggle-box" />
<span>Theme</span>
</label>
</div>
<div class="option-select">
<select id="theme-select">
<option selected value="0">Default</option>
<option value="1">Neutral grey</option>
<option value="3">Flagship Terrastruct</option>
<option value="4">Cool classics</option>
<option value="5">Mixed berry blue</option>
<option value="6">Grape soda</option>
<option value="7">Aubergine</option>
<option value="8">Colorblind clear</option>
<option value="100">Vanilla nitro cola</option>
<option value="101">Orange creamsicle</option>
<option value="102">Shirley temple</option>
<option value="103">Earth tones</option>
<option value="104">Everglade green</option>
<option value="105">Buttered toast</option>
<option value="200">Dark mauve</option>
<option value="300">Terminal</option>
<option value="301">Terminal grayscale</option>
</select>
</div>
</div>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="dark-theme-toggle" class="option-toggle-box" />
<span>Dark Theme</span>
</label>
</div>
<div class="option-select">
<select id="dark-theme-select">
<option selected value="0">Default</option>
<option value="1">Neutral grey</option>
<option value="3">Flagship Terrastruct</option>
<option value="4">Cool classics</option>
<option value="5">Mixed berry blue</option>
<option value="6">Grape soda</option>
<option value="7">Aubergine</option>
<option value="8">Colorblind clear</option>
<option value="100">Vanilla nitro cola</option>
<option value="101">Orange creamsicle</option>
<option value="102">Shirley temple</option>
<option value="103">Earth tones</option>
<option value="104">Everglade green</option>
<option value="105">Buttered toast</option>
<option value="200">Dark mauve</option>
<option value="300">Terminal</option>
<option value="301">Terminal grayscale</option>
</select>
</div>
</div>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="pad-toggle" class="option-toggle-box" />
<span>Padding</span>
</label>
</div>
<div class="option-select">
<label class="input-label">
<input
type="number"
id="pad-input"
value="20"
step="10"
class="number-input"
/>
</label>
</div>
</div>
<div class="sketch-toggle">
<label class="checkbox-label">
<input type="checkbox" id="sketch" />
Sketch mode
</label>
<div class="option">
<div class="option-toggle">
<label class="checkbox-label">
<input type="checkbox" id="scale-toggle" class="option-toggle-box" />
<span>Scale</span>
</label>
</div>
<div class="option-select">
<label class="input-label">
<input
type="number"
id="scale-input"
value="1"
step="0.1"
min="0"
class="number-input"
/>
</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>
@ -105,11 +328,56 @@
const d2 = new D2();
window.compile = async () => {
const input = document.getElementById("input").value;
const layout = document.querySelector('input[name="layout"]:checked').value;
const sketch = document.getElementById("sketch").checked;
const layout = document.getElementById("layout-toggle").checked
? document.querySelector('input[name="layout-select"]:checked').value
: null;
const sketch = document.getElementById("sketch-toggle").checked
? document.querySelector('input[name="sketch-select"]:checked').value == "true"
: null;
const center = document.getElementById("center-toggle").checked
? document.querySelector('input[name="center-select"]:checked').value == "true"
: null;
const forceAppendix = document.getElementById("appendix-toggle").checked
? document.querySelector('input[name="appendix-select"]:checked').value ==
"true"
: null;
const themeSelector = document.getElementById("theme-select");
const themeId = document.getElementById("theme-toggle").checked
? Number(themeSelector.options[themeSelector.selectedIndex].value)
: null;
const darkThemeSelector = document.getElementById("dark-theme-select");
const darkThemeId = document.getElementById("dark-theme-toggle").checked
? Number(darkThemeSelector.options[darkThemeSelector.selectedIndex].value)
: null;
const pad = document.getElementById("pad-toggle").checked
? Number(document.getElementById("pad-input").value)
: null;
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, sketch });
const svg = await d2.render(result.diagram, { sketch });
const result = await d2.compile(input, {
layout,
sketch,
themeId,
darkThemeId,
scale,
pad,
center,
forceAppendix,
target,
animateInterval,
salt,
noXmlTag: true,
});
const svg = await d2.render(result.diagram, result.renderOptions);
document.getElementById("output").innerHTML = svg;
} catch (err) {
console.error(err);

View file

@ -1,10 +1,5 @@
import { createWorker, loadFile } from "./platform.js";
const DEFAULT_OPTIONS = {
layout: "dagre",
sketch: false,
};
export class D2 {
constructor() {
this.ready = this.init();
@ -86,17 +81,15 @@ export class D2 {
}
async compile(input, options = {}) {
const opts = { ...DEFAULT_OPTIONS, ...options };
const request =
typeof input === "string"
? { fs: { index: input }, options: opts }
: { ...input, options: { ...opts, ...input.options } };
? { fs: { index: input }, options }
: { ...input, options: { ...options, ...input.options } };
return this.sendMessage("compile", request);
}
async render(diagram, options = {}) {
const opts = { ...DEFAULT_OPTIONS, ...options };
return this.sendMessage("render", { diagram, options: opts });
return this.sendMessage("render", { diagram, options });
}
async encode(script) {

View file

@ -1 +1 @@
export * from "./platform.node.js";
export * from "./platform.node.js";

View file

@ -30,13 +30,14 @@ export function setupMessageHandler(isNode, port, initWasm) {
// single-threaded WASM call cannot complete without giving control back
// So we compute it, store it here, then during elk layout, instead
// of computing again, we use this variable (and unset it for next call)
if (data.options.layout === "elk") {
// If the layout option has not been set, we generate the elk layout now
// anyway to support `layout-engine: elk` in d2-config vars
if (data.options.layout === "elk" || data.options.layout == null) {
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
const elkGraph2 = JSON.parse(elkGraph).data;
const layout = await elk.layout(elkGraph2);
globalThis.elkResult = layout;
}
const result = await d2.compile(JSON.stringify(data));
const response = JSON.parse(result);
if (response.error) throw new Error(response.error.message);

View file

@ -27,12 +27,10 @@ export function setupMessageHandler(isNode, port, initWasm) {
case "compile":
try {
if (data.options.layout === "elk") {
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
const elkGraph2 = JSON.parse(elkGraph).data;
const layout = await elk.layout(elkGraph2);
globalThis.elkResult = layout;
}
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
const elkGraph2 = JSON.parse(elkGraph).data;
const layout = await elk.layout(elkGraph2);
globalThis.elkResult = layout;
const result = await d2.compile(JSON.stringify(data));
const response = JSON.parse(result);
if (response.error) throw new Error(response.error.message);

View file

@ -27,7 +27,7 @@ export function setupMessageHandler(isNode, port, initWasm) {
case "compile":
try {
if (data.options.layout === "elk") {
if (data.options.layout === "elk" || data.options.layout == null) {
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
const elkGraph2 = JSON.parse(elkGraph).data;
const layout = await elk.layout(elkGraph2);

View file

@ -25,6 +25,64 @@ describe("D2 Unit Tests", () => {
await d2.worker.terminate();
}, 20000);
test("d2-config read correctly", async () => {
const d2 = new D2();
const result = await d2.compile(
`
vars: {
d2-config: {
theme-id: 4
dark-theme-id: 200
pad: 10
center: true
sketch: true
layout-engine: elk
}
}
x -> y
`
);
expect(result.renderOptions.sketch).toBe(true);
expect(result.renderOptions.themeID).toBe(4);
expect(result.renderOptions.darkThemeID).toBe(200);
expect(result.renderOptions.center).toBe(true);
expect(result.renderOptions.pad).toBe(10);
await d2.worker.terminate();
}, 20000);
test("render options take priority", async () => {
const d2 = new D2();
const result = await d2.compile(
`
vars: {
d2-config: {
theme-id: 4
dark-theme-id: 200
pad: 10
center: true
sketch: true
layout-engine: elk
}
}
x -> y
`,
{
sketch: false,
themeID: 100,
darkThemeID: 300,
center: false,
pad: 0,
layout: "dagre",
}
);
expect(result.renderOptions.sketch).toBe(false);
expect(result.renderOptions.themeID).toBe(100);
expect(result.renderOptions.darkThemeID).toBe(300);
expect(result.renderOptions.center).toBe(false);
expect(result.renderOptions.pad).toBe(0);
await d2.worker.terminate();
}, 20000);
test("sketch render works", async () => {
const d2 = new D2();
const result = await d2.compile("x -> y", { sketch: true });
@ -35,6 +93,52 @@ describe("D2 Unit Tests", () => {
await d2.worker.terminate();
}, 20000);
test("center render works", async () => {
const d2 = new D2();
const result = await d2.compile("x -> y", { center: true });
const svg = await d2.render(result.diagram, { center: true });
expect(svg).toContain("<svg");
expect(svg).toContain("</svg>");
expect(svg).toContain("xMidYMid meet");
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 });
const svg = await d2.render(result.diagram, { forceAppendix: true });
expect(svg).toContain("<svg");
expect(svg).toContain("</svg>");
expect(svg).toContain('class="appendix"');
await d2.worker.terminate();
}, 20000);
test("animated multi-board works", async () => {
const d2 = new D2();
const source = `
x -> y
layers: {
numbers: {
1 -> 2
}
}
`;
const options = { target: "*", animateInterval: 1000 };
const result = await d2.compile(source, options);
const svg = await d2.render(result.diagram, result.renderOptions);
expect(svg).toContain("<svg");
expect(svg).toContain("</svg>");
await d2.worker.terminate();
}, 20000);
test("latex works", async () => {
const d2 = new D2();
const result = await d2.compile("x: |latex \\frac{f(x+h)-f(x)}{h} |");
@ -55,4 +159,25 @@ describe("D2 Unit Tests", () => {
}
await d2.worker.terminate();
}, 20000);
test("handles unanimated multi-board error correctly", async () => {
const d2 = new D2();
const source = `
x -> y
layers: {
numbers: {
1 -> 2
}
}
`;
const result = await d2.compile(source);
try {
await d2.render(result.diagram, { target: "*" });
throw new Error("Should have thrown compile error");
} catch (err) {
expect(err).toBeDefined();
expect(err.message).not.toContain("Should have thrown compile error");
}
await d2.worker.terminate();
}, 20000);
});