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/d2wasm/functions.go b/d2js/d2wasm/functions.go index 23e2f4c7e..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) @@ -238,6 +248,15 @@ func Compile(args []js.Value) (interface{}, error) { FS: input.FS, Diagram: *diagram, Graph: *g, + 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 } diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go index 615db1104..6346292a1 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"` + Options RenderOptions `json:"options"` } type CompletionResponse struct { 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 diff --git a/d2js/js/examples/customizable.html b/d2js/js/examples/customizable.html index fa9fcbf7f..c0947ea5f 100644 --- a/d2js/js/examples/customizable.html +++ b/d2js/js/examples/customizable.html @@ -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,34 +35,31 @@ border: 1px solid #eee; border-radius: 4px; } - .layout-toggle, - .sketch-toggle, - .center-toggle, - .appendix-toggle, - .theme-select, - .dark-theme-select, - .padding-input, - .scale-input { + + .option:not(:has(.option-toggle-box:checked)) .option-select { + opacity: 0.5; + pointer-events: none; + } + + .option { display: flex; gap: 16px; align-items: center; } - .radio-group { - display: flex; - gap: 12px; - } + .input-label, - .select-label, - .radio-label, - .checkbox-label { + .checkbox-label, + .select-label { display: flex; gap: 4px; align-items: center; cursor: pointer; } + .number-input { width: 3rem; } + button { padding: 8px 16px; background: #0066cc; @@ -68,9 +68,11 @@ border-radius: 4px; cursor: pointer; } + button:hover { background: #0052a3; } + #output { flex: 1; overflow: auto; @@ -78,51 +80,107 @@ border-radius: 4px; padding: 16px; } + #output svg { max-width: 100%; max-height: 90vh; } +
-
- Layout: -
- -