Merge pull request #2343 from x-delfino/wasm-options
d2js: Additional render options
This commit is contained in:
commit
c09d29fb17
11 changed files with 703 additions and 85 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from "./platform.node.js";
|
||||
export * from "./platform.node.js";
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue