diff --git a/d2js/d2wasm/api.go b/d2js/d2wasm/api.go index b3c9f7274..e87386cd6 100644 --- a/d2js/d2wasm/api.go +++ b/d2js/d2wasm/api.go @@ -5,6 +5,7 @@ package d2wasm import ( "encoding/json" "fmt" + "runtime/debug" "syscall/js" ) @@ -36,7 +37,7 @@ func wrapWASMCall(fn func(args []js.Value) (interface{}, error)) js.Func { if r := recover(); r != nil { resp := WASMResponse{ Error: &WASMError{ - Message: fmt.Sprintf("panic recovered: %v", r), + Message: fmt.Sprintf("panic recovered: %v\n%s", r, debug.Stack()), Code: 500, }, } diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go index 25250880f..d82965a69 100644 --- a/d2js/d2wasm/functions.go +++ b/d2js/d2wasm/functions.go @@ -3,17 +3,30 @@ package d2wasm import ( + "context" "encoding/json" + "fmt" "strings" "syscall/js" "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2layouts/d2elklayout" + "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lsp" "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/d2renderers/d2fonts" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/lib/log" + "oss.terrastruct.com/d2/lib/memfs" + "oss.terrastruct.com/d2/lib/textmeasure" + "oss.terrastruct.com/d2/lib/urlenc" "oss.terrastruct.com/d2/lib/version" + "oss.terrastruct.com/util-go/go2" ) func GetParentID(args []js.Value) (interface{}, error) { @@ -96,13 +109,62 @@ func GetRefRanges(args []js.Value) (interface{}, error) { func Compile(args []js.Value) (interface{}, error) { if len(args) < 1 { - return nil, &WASMError{Message: "missing script argument", Code: 400} + return nil, &WASMError{Message: "missing JSON argument", Code: 400} + } + var input CompileRequest + if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil { + return nil, &WASMError{Message: "invalid JSON input", Code: 400} } - script := args[0].String() - g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ - UTF16Pos: true, - }) + if input.FS == nil { + return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400} + } + + if _, ok := input.FS["index"]; !ok { + return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400} + } + + 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() + 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 + } + + renderOpts := &d2svg.RenderOpts{} + var fontFamily *d2fonts.FontFamily + if input.Opts != nil && input.Opts.Sketch != nil { + fontFamily = go2.Pointer(d2fonts.HandDrawn) + renderOpts.Sketch = input.Opts.Sketch + } + 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 err != nil { if pe, ok := err.(*d2parser.ParseError); ok { return nil, &WASMError{Message: pe.Error(), Code: 400} @@ -110,12 +172,41 @@ func Compile(args []js.Value) (interface{}, error) { return nil, &WASMError{Message: err.Error(), Code: 500} } - newScript := d2format.Format(g.AST) - if script != newScript { - return map[string]string{"result": newScript}, nil + input.FS["index"] = d2format.Format(g.AST) + + return CompileResponse{ + FS: input.FS, + Diagram: *diagram, + Graph: *g, + }, nil +} + +func Render(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing JSON argument", Code: 400} + } + var input RenderRequest + if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil { + return nil, &WASMError{Message: "invalid JSON input", Code: 400} } - return nil, nil + if input.Diagram == nil { + return nil, &WASMError{Message: "missing 'diagram' field in input JSON", Code: 400} + } + + renderOpts := &d2svg.RenderOpts{} + if input.Opts != nil && input.Opts.Sketch != nil { + renderOpts.Sketch = input.Opts.Sketch + } + if input.Opts != nil && input.Opts.ThemeID != nil { + renderOpts.ThemeID = input.Opts.ThemeID + } + out, err := d2svg.Render(input.Diagram, renderOpts) + if err != nil { + return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500} + } + + return out, nil } func GetBoardAtPosition(args []js.Value) (interface{}, error) { @@ -144,7 +235,13 @@ func Encode(args []js.Value) (interface{}, error) { } script := args[0].String() - return map[string]string{"result": script}, nil + encoded, err := urlenc.Encode(script) + // should never happen + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return map[string]string{"result": encoded}, nil } func Decode(args []js.Value) (interface{}, error) { @@ -153,6 +250,10 @@ func Decode(args []js.Value) (interface{}, error) { } script := args[0].String() + script, err := urlenc.Decode(script) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } return map[string]string{"result": script}, nil } @@ -189,7 +290,3 @@ func GetCompletions(args []js.Value) (interface{}, error) { Items: items, }, nil } - -type CompletionResponse struct { - Items []map[string]interface{} `json:"items"` -} diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go index 911c6b5eb..a13b82bae 100644 --- a/d2js/d2wasm/types.go +++ b/d2js/d2wasm/types.go @@ -2,7 +2,11 @@ package d2wasm -import "oss.terrastruct.com/d2/d2ast" +import ( + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2target" +) type WASMResponse struct { Data interface{} `json:"data,omitempty"` @@ -26,3 +30,29 @@ type RefRangesResponse struct { type BoardPositionResponse struct { BoardPath []string `json:"boardPath"` } + +type CompileRequest struct { + FS map[string]string `json:"fs"` + Opts *RenderOptions `json:"options"` +} + +type RenderOptions struct { + Layout *string `json:"layout"` + Sketch *bool `json:"sketch"` + ThemeID *int64 `json:"themeID"` +} + +type CompileResponse struct { + FS map[string]string `json:"fs"` + Diagram d2target.Diagram `json:"diagram"` + Graph d2graph.Graph `json:"graph"` +} + +type CompletionResponse struct { + Items []map[string]interface{} `json:"items"` +} + +type RenderRequest struct { + Diagram *d2target.Diagram `json:"diagram"` + Opts *RenderOptions `json:"options"` +} diff --git a/d2js/js.go b/d2js/js.go index 50280194c..514fd5179 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -16,6 +16,7 @@ func main() { api.Register("getObjOrder", d2wasm.GetObjOrder) api.Register("getRefRanges", d2wasm.GetRefRanges) api.Register("compile", d2wasm.Compile) + api.Register("render", d2wasm.Render) api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition) api.Register("encode", d2wasm.Encode) api.Register("decode", d2wasm.Decode) diff --git a/d2renderers/d2latex/latex.go b/d2renderers/d2latex/latex.go index d2830e632..a822b2c7b 100644 --- a/d2renderers/d2latex/latex.go +++ b/d2renderers/d2latex/latex.go @@ -1,5 +1,3 @@ -//go:build !wasm - package d2latex import ( diff --git a/d2renderers/d2latex/latex_stub.go b/d2renderers/d2latex/latex_stub.go deleted file mode 100644 index 195edf0a5..000000000 --- a/d2renderers/d2latex/latex_stub.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build wasm - -package d2latex - -func Render(s string) (_ string, err error) { - return "", nil -} - -func Measure(s string) (width, height int, err error) { - return -} diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index cb14ff041..12ea2e7c0 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -1867,6 +1867,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } darkThemeID = opts.DarkThemeID scale = opts.Scale + } else { + opts = &RenderOpts{} } buf := &bytes.Buffer{} diff --git a/lib/textmeasure/markdown.go b/lib/textmeasure/markdown.go index e2d73eedc..f85423988 100644 --- a/lib/textmeasure/markdown.go +++ b/lib/textmeasure/markdown.go @@ -1,5 +1,3 @@ -//go:build !wasm - package textmeasure import ( diff --git a/lib/textmeasure/markdown_js.go b/lib/textmeasure/markdown_js.go deleted file mode 100644 index b23b2c101..000000000 --- a/lib/textmeasure/markdown_js.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build wasm - -package textmeasure - -import "oss.terrastruct.com/d2/d2renderers/d2fonts" - -func MeasureMarkdown(mdText string, ruler *Ruler, fontFamily *d2fonts.FontFamily, fontSize int) (width, height int, err error) { - return 0, 0, nil -} - -func RenderMarkdown(m string) (string, error) { - return "", nil -} - -func ReplaceSubstitutionsMarkdown(mdText string, variables map[string]string) string { - return mdText -} diff --git a/lib/textmeasure/substitutions.go b/lib/textmeasure/substitutions.go index a21475494..a141e7385 100644 --- a/lib/textmeasure/substitutions.go +++ b/lib/textmeasure/substitutions.go @@ -1,5 +1,3 @@ -//go:build !wasm - package textmeasure import (