From 68b36c4fc17187c11902af48af1e7c71d83508c0 Mon Sep 17 00:00:00 2001 From: delfino Date: Fri, 14 Feb 2025 03:03:17 +0000 Subject: [PATCH 1/4] adding support for d2-config in vars --- d2js/d2wasm/functions.go | 12 +- d2js/d2wasm/types.go | 15 +- d2js/js/examples/customizable.html | 482 ++++++++++++++++------------- d2js/js/src/index.js | 13 +- d2js/js/src/platform.js | 2 +- 5 files changed, 296 insertions(+), 228 deletions(-) diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go index 23e2f4c7e..d2aa115e9 100644 --- a/d2js/d2wasm/functions.go +++ b/d2js/d2wasm/functions.go @@ -232,13 +232,23 @@ func Compile(args []js.Value) (interface{}, error) { return nil, &WASMError{Message: err.Error(), Code: 500} } + mergedRenderOpts := RenderOptions{ + ThemeID: renderOpts.ThemeID, + DarkThemeID: renderOpts.DarkThemeID, + Sketch: renderOpts.Sketch, + Pad: renderOpts.Pad, + Center: renderOpts.Center, + ForceAppendix: input.Opts.ForceAppendix, + }; + input.FS["index"] = d2format.Format(g.AST) return CompileResponse{ FS: input.FS, Diagram: *diagram, Graph: *g, - }, nil + RenderOpts: mergedRenderOpts, + }, nil } func Render(args []js.Value) (interface{}, error) { diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go index 615db1104..2437924d8 100644 --- a/d2js/d2wasm/types.go +++ b/d2js/d2wasm/types.go @@ -33,11 +33,10 @@ 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"` Pad *int64 `json:"pad"` Sketch *bool `json:"sketch"` Center *bool `json:"center"` @@ -47,10 +46,16 @@ type RenderOptions struct { ForceAppendix *bool `json:"forceAppendix"` } +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"` + RenderOpts RenderOptions `json:"renderOpts"` } type CompletionResponse struct { diff --git a/d2js/js/examples/customizable.html b/d2js/js/examples/customizable.html index fa9fcbf7f..e13fa60c1 100644 --- a/d2js/js/examples/customizable.html +++ b/d2js/js/examples/customizable.html @@ -1,241 +1,301 @@ - - - - -
- -
-
- Layout: + + + + + + +
+ +
+
+
+ +
+
-
+
+
+
-
+
+
+ + +
+
+
+
+
-
+
+
+ + +
+
+
+
+
-
-
+
+
+
-
-
+
+
+
-
+
+ +
+
+
+
+ +
+
-
-
- +
+
+
+
+ +
+
+
-
-
- - + +
+
+ + + diff --git a/d2js/js/src/index.js b/d2js/js/src/index.js index 8fd09fc7d..a2c28adbf 100644 --- a/d2js/js/src/index.js +++ b/d2js/js/src/index.js @@ -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) { diff --git a/d2js/js/src/platform.js b/d2js/js/src/platform.js index 1a607e21d..fdcbaa051 100644 --- a/d2js/js/src/platform.js +++ b/d2js/js/src/platform.js @@ -1 +1 @@ -export * from "./platform.node.js"; +export * from "./platform.node.js"; \ No newline at end of file From 49992148d7ed2c81f256b733f6b93174681deabc Mon Sep 17 00:00:00 2001 From: delfino Date: Fri, 14 Feb 2025 14:26:03 +0000 Subject: [PATCH 2/4] fixing scale --- d2js/d2wasm/functions.go | 1 + d2js/js/examples/customizable.html | 620 +++++++++++++++-------------- 2 files changed, 325 insertions(+), 296 deletions(-) diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go index d2aa115e9..45e3cae0c 100644 --- a/d2js/d2wasm/functions.go +++ b/d2js/d2wasm/functions.go @@ -238,6 +238,7 @@ func Compile(args []js.Value) (interface{}, error) { Sketch: renderOpts.Sketch, Pad: renderOpts.Pad, Center: renderOpts.Center, + Scale: input.Opts.Scale, ForceAppendix: input.Opts.ForceAppendix, }; diff --git a/d2js/js/examples/customizable.html b/d2js/js/examples/customizable.html index e13fa60c1..3855a1841 100644 --- a/d2js/js/examples/customizable.html +++ b/d2js/js/examples/customizable.html @@ -1,301 +1,329 @@ - - - - - - -
- -
-
-
- -
-
-
- - -
-
-
-
-
- -
-
-
- - -
-
-
-
-
- -
-
-
- - -
-
-
-
-
- -
-
-
- - -
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
-
- - + .controls { + display: flex; + flex-direction: column; + gap: 12px; + width: 400px; + } + + textarea { + width: 100%; + height: 300px; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + font-family: monospace; + } + + .options-group { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + border: 1px solid #eee; + border-radius: 4px; + } + + .option:not(:has(.option-toggle-box:checked)) .option-select { + opacity: 0.5; + pointer-events: none; + } + + .option { + display: flex; + gap: 16px; + align-items: center; + } + + .input-label, + .checkbox-label, + .select-label { + display: flex; + gap: 4px; + align-items: center; + cursor: pointer; + } + + .number-input { + width: 3rem; + } + + button { + padding: 8px 16px; + background: #0066cc; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + button:hover { + background: #0052a3; + } + + #output { + flex: 1; + overflow: auto; + border: 1px solid #eee; + border-radius: 4px; + padding: 16px; + } + + #output svg { + max-width: 100%; + max-height: 90vh; + } + + + + +
+ +
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+ + From f6ec0247e1ddcfe9c30de869530766a84ca2a0e9 Mon Sep 17 00:00:00 2001 From: delfino Date: Fri, 14 Feb 2025 22:04:30 +0000 Subject: [PATCH 3/4] add support for d2-config layouts --- d2js/d2wasm/functions.go | 80 ++++++++++++++++-------------- d2js/d2wasm/types.go | 14 +++--- d2js/js/examples/customizable.html | 2 +- d2js/js/src/worker.browser.js | 10 ++-- d2js/js/src/worker.js | 10 ++-- d2js/js/src/worker.node.js | 10 ++-- 6 files changed, 64 insertions(+), 62 deletions(-) diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go index 45e3cae0c..f0a8b71e7 100644 --- a/d2js/d2wasm/functions.go +++ b/d2js/d2wasm/functions.go @@ -154,6 +154,26 @@ func GetELKGraph(args []js.Value) (interface{}, error) { return elk, nil } +func layoutResolver() func(engine string) (d2graph.LayoutGraph, error) { + cached := make(map[string]d2graph.LayoutGraph) + return func(engine string) (d2graph.LayoutGraph, error) { + if c, ok := cached[engine]; ok { + return c, nil + } + var layout d2graph.LayoutGraph + switch engine { + case "dagre": + layout = d2dagrelayout.DefaultLayout + case "elk": + layout = d2elklayout.DefaultLayout + default: + return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", engine), Code: 400} + } + cached[engine] = layout + return layout, nil + } +} + func Compile(args []js.Value) (interface{}, error) { if len(args) < 1 { return nil, &WASMError{Message: "missing JSON argument", Code: 400} @@ -171,35 +191,29 @@ 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, + LayoutResolver: layoutResolver(), + } + + 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) + compileOpts.FontFamily = go2.Pointer(d2fonts.HandDrawn) renderOpts.Sketch = input.Opts.Sketch } if input.Opts != nil && input.Opts.Pad != nil { @@ -217,13 +231,9 @@ func Compile(args []js.Value) (interface{}, error) { if input.Opts != nil && input.Opts.Scale != nil { renderOpts.Scale = input.Opts.Scale } - diagram, g, err := d2lib.Compile(ctx, input.FS["index"], &d2lib.CompileOptions{ - UTF16Pos: true, - FS: fs, - Ruler: ruler, - LayoutResolver: layoutResolver, - FontFamily: fontFamily, - }, renderOpts) + + 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) @@ -232,24 +242,22 @@ func Compile(args []js.Value) (interface{}, error) { return nil, &WASMError{Message: err.Error(), Code: 500} } - mergedRenderOpts := RenderOptions{ - ThemeID: renderOpts.ThemeID, - DarkThemeID: renderOpts.DarkThemeID, - Sketch: renderOpts.Sketch, - Pad: renderOpts.Pad, - Center: renderOpts.Center, - Scale: input.Opts.Scale, - ForceAppendix: input.Opts.ForceAppendix, - }; - input.FS["index"] = d2format.Format(g.AST) return CompileResponse{ FS: input.FS, Diagram: *diagram, Graph: *g, - RenderOpts: mergedRenderOpts, - }, nil + Options: RenderOptions{ + ThemeID: renderOpts.ThemeID, + DarkThemeID: renderOpts.DarkThemeID, + Sketch: renderOpts.Sketch, + Pad: renderOpts.Pad, + Center: renderOpts.Center, + Scale: renderOpts.Scale, + ForceAppendix: input.Opts.ForceAppendix, + }, + }, nil } func Render(args []js.Value) (interface{}, error) { diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go index 2437924d8..6346292a1 100644 --- a/d2js/d2wasm/types.go +++ b/d2js/d2wasm/types.go @@ -33,7 +33,7 @@ type BoardPositionResponse struct { type CompileRequest struct { FS map[string]string `json:"fs"` - Opts *CompileOptions `json:"options"` + Opts *CompileOptions `json:"options"` } type RenderOptions struct { @@ -47,15 +47,15 @@ type RenderOptions struct { } type CompileOptions struct { - RenderOptions - Layout *string `json:"layout"` + RenderOptions + Layout *string `json:"layout"` } type CompileResponse struct { - FS map[string]string `json:"fs"` - Diagram d2target.Diagram `json:"diagram"` - Graph d2graph.Graph `json:"graph"` - RenderOpts RenderOptions `json:"renderOpts"` + FS map[string]string `json:"fs"` + Diagram d2target.Diagram `json:"diagram"` + Graph d2graph.Graph `json:"graph"` + Options RenderOptions `json:"options"` } type CompletionResponse struct { diff --git a/d2js/js/examples/customizable.html b/d2js/js/examples/customizable.html index 3855a1841..c0947ea5f 100644 --- a/d2js/js/examples/customizable.html +++ b/d2js/js/examples/customizable.html @@ -316,7 +316,7 @@ center, forceAppendix, }); - const svg = await d2.render(result.diagram, result.renderOpts); + const svg = await d2.render(result.diagram, result.options); document.getElementById("output").innerHTML = svg; } catch (err) { console.error(err); diff --git a/d2js/js/src/worker.browser.js b/d2js/js/src/worker.browser.js index 6dbb758ff..6a00c59ca 100644 --- a/d2js/js/src/worker.browser.js +++ b/d2js/js/src/worker.browser.js @@ -30,12 +30,10 @@ 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") { - 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); diff --git a/d2js/js/src/worker.js b/d2js/js/src/worker.js index b615fd569..427fbfffb 100644 --- a/d2js/js/src/worker.js +++ b/d2js/js/src/worker.js @@ -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); diff --git a/d2js/js/src/worker.node.js b/d2js/js/src/worker.node.js index b615fd569..427fbfffb 100644 --- a/d2js/js/src/worker.node.js +++ b/d2js/js/src/worker.node.js @@ -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); From 288b2927660eb7e707ef5f88e4932318f0214855 Mon Sep 17 00:00:00 2001 From: delfino Date: Fri, 14 Feb 2025 22:47:32 +0000 Subject: [PATCH 4/4] updating readme --- ci/release/changelogs/next.md | 2 +- d2js/js/README.md | 37 +++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 908542d89..f152d8600 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -2,6 +2,6 @@ #### Improvements 🧹 -d2js: Support additional render options (`themeID`, `darkThemeID`, `center`, `pad`, `scale` and `forceAppendix`) [#2343](https://github.com/terrastruct/d2/pull/2343) +d2js: Support additional render options (`themeID`, `darkThemeID`, `center`, `pad`, `scale` and `forceAppendix`). Support `d2-config`. [#2343](https://github.com/terrastruct/d2/pull/2343) #### Bugfixes ⛑️ diff --git a/d2js/js/README.md b/d2js/js/README.md index 4e3c80d23..4c474e85a 100644 --- a/d2js/js/README.md +++ b/d2js/js/README.md @@ -42,19 +42,44 @@ 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); +``` + +Additional Configuration: + +```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.options); ``` ## API Reference ### `new D2()` + Creates a new D2 instance. ### `compile(input: string, options?: CompileOptions): Promise` + Compiles D2 markup into an intermediate representation. -Options: +### `render(diagram: Diagram, options?: RenderOptions): Promise` + +Renders a compiled diagram to SVG. + +### `CompileOptions` + +All `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 @@ -63,8 +88,12 @@ Options: - `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] -### `render(diagram: Diagram, options?: RenderOptions): Promise` -Renders a compiled diagram to SVG. +### `CompileResult` + +- `diagram`: `Diagram`: Compiled D2 diagram +- `options`: `RenderOptions`: Render options merged with configuration set in diagram +- `fs` +- `graph` ## Development