add support for d2-config layouts

This commit is contained in:
delfino 2025-02-14 22:04:30 +00:00
parent 49992148d7
commit f6ec0247e1
No known key found for this signature in database
GPG key ID: CFE0DD6A770BF48C
6 changed files with 64 additions and 62 deletions

View file

@ -154,6 +154,26 @@ func GetELKGraph(args []js.Value) (interface{}, error) {
return elk, nil 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) { func Compile(args []js.Value) (interface{}, error) {
if len(args) < 1 { if len(args) < 1 {
return nil, &WASMError{Message: "missing JSON argument", Code: 400} 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} 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 { if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400} 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 { if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500} 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 { if input.Opts != nil && input.Opts.Layout != nil {
switch *input.Opts.Layout { compileOpts.Layout = 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
} }
renderOpts := &d2svg.RenderOpts{} renderOpts := &d2svg.RenderOpts{}
var fontFamily *d2fonts.FontFamily
if input.Opts != nil && input.Opts.Sketch != nil && *input.Opts.Sketch { 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 renderOpts.Sketch = input.Opts.Sketch
} }
if input.Opts != nil && input.Opts.Pad != nil { 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 { if input.Opts != nil && input.Opts.Scale != nil {
renderOpts.Scale = input.Opts.Scale renderOpts.Scale = input.Opts.Scale
} }
diagram, g, err := d2lib.Compile(ctx, input.FS["index"], &d2lib.CompileOptions{
UTF16Pos: true, ctx := log.WithDefault(context.Background())
FS: fs, diagram, g, err := d2lib.Compile(ctx, input.FS["index"], compileOpts, renderOpts)
Ruler: ruler,
LayoutResolver: layoutResolver,
FontFamily: fontFamily,
}, renderOpts)
if err != nil { if err != nil {
if pe, ok := err.(*d2parser.ParseError); ok { if pe, ok := err.(*d2parser.ParseError); ok {
errs, _ := json.Marshal(pe.Errors) errs, _ := json.Marshal(pe.Errors)
@ -232,23 +242,21 @@ func Compile(args []js.Value) (interface{}, error) {
return nil, &WASMError{Message: err.Error(), Code: 500} 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) input.FS["index"] = d2format.Format(g.AST)
return CompileResponse{ return CompileResponse{
FS: input.FS, FS: input.FS,
Diagram: *diagram, Diagram: *diagram,
Graph: *g, Graph: *g,
RenderOpts: mergedRenderOpts, 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 }, nil
} }

View file

@ -55,7 +55,7 @@ type CompileResponse struct {
FS map[string]string `json:"fs"` FS map[string]string `json:"fs"`
Diagram d2target.Diagram `json:"diagram"` Diagram d2target.Diagram `json:"diagram"`
Graph d2graph.Graph `json:"graph"` Graph d2graph.Graph `json:"graph"`
RenderOpts RenderOptions `json:"renderOpts"` Options RenderOptions `json:"options"`
} }
type CompletionResponse struct { type CompletionResponse struct {

View file

@ -316,7 +316,7 @@
center, center,
forceAppendix, forceAppendix,
}); });
const svg = await d2.render(result.diagram, result.renderOpts); const svg = await d2.render(result.diagram, result.options);
document.getElementById("output").innerHTML = svg; document.getElementById("output").innerHTML = svg;
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View file

@ -30,12 +30,10 @@ export function setupMessageHandler(isNode, port, initWasm) {
// single-threaded WASM call cannot complete without giving control back // single-threaded WASM call cannot complete without giving control back
// So we compute it, store it here, then during elk layout, instead // 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) // 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 elkGraph = await d2.getELKGraph(JSON.stringify(data));
const elkGraph2 = JSON.parse(elkGraph).data; const elkGraph2 = JSON.parse(elkGraph).data;
const layout = await elk.layout(elkGraph2); const layout = await elk.layout(elkGraph2);
globalThis.elkResult = layout; globalThis.elkResult = layout;
}
const result = await d2.compile(JSON.stringify(data)); const result = await d2.compile(JSON.stringify(data));
const response = JSON.parse(result); const response = JSON.parse(result);

View file

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

View file

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