From a541c14e0e2f8c35a008a4469dcc289c0442e102 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 3 Apr 2023 13:40:40 -0700 Subject: [PATCH 01/21] init --- d2js/README.md | 30 ++++ d2js/js.go | 259 ++++++++++++++++++++++++++++++ d2renderers/d2latex/latex.go | 2 + d2renderers/d2latex/latex_stub.go | 11 ++ 4 files changed, 302 insertions(+) create mode 100644 d2js/README.md create mode 100644 d2js/js.go create mode 100644 d2renderers/d2latex/latex_stub.go diff --git a/d2js/README.md b/d2js/README.md new file mode 100644 index 0000000000..6728d72af6 --- /dev/null +++ b/d2js/README.md @@ -0,0 +1,30 @@ +# D2 as a Javascript library + +D2 is runnable as a Javascript library, on both the client and server side. This means you +can run D2 entirely on the browser. + +This is achieved by a JS wrapper around a WASM file. + +## Install + +### NPM + +```sh +npm install @terrastruct/d2 +``` + +### Yarn + +```sh +yarn add @terrastruct/d2 +``` + +## Build + +```sh +GOOS=js GOARCH=wasm go build -ldflags='-s -w' -trimpath -o main.wasm ./d2js +``` + +## API + +todo diff --git a/d2js/js.go b/d2js/js.go new file mode 100644 index 0000000000..0fcca7da25 --- /dev/null +++ b/d2js/js.go @@ -0,0 +1,259 @@ +//go:build wasm + +package main + +import ( + "encoding/json" + "errors" + "io/fs" + "strings" + "syscall/js" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2compiler" + "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2oracle" + "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/lib/urlenc" +) + +func main() { + js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) + js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) + js.Global().Set("d2Compile", js.FuncOf(jsCompile)) + js.Global().Set("d2Parse", js.FuncOf(jsParse)) + js.Global().Set("d2Encode", js.FuncOf(jsEncode)) + js.Global().Set("d2Decode", js.FuncOf(jsDecode)) + select {} +} + +type jsObjOrder struct { + Order []string `json:"order"` + Error string `json:"error"` +} + +func jsGetObjOrder(this js.Value, args []js.Value) interface{} { + dsl := args[0].String() + + g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + UTF16: true, + }) + if err != nil { + ret := jsObjOrder{Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + resp := jsObjOrder{ + Order: d2oracle.GetObjOrder(g), + } + + str, _ := json.Marshal(resp) + return string(str) +} + +type jsRefRanges struct { + Ranges []d2ast.Range `json:"ranges"` + ParseError string `json:"parseError"` + UserError string `json:"userError"` + D2Error string `json:"d2Error"` +} + +func jsGetRefRanges(this js.Value, args []js.Value) interface{} { + dsl := args[0].String() + key := args[1].String() + + mk, err := d2parser.ParseMapKey(key) + if err != nil { + ret := jsRefRanges{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + UTF16: true, + }) + var pe *d2parser.ParseError + if err != nil { + if errors.As(err, &pe) { + serialized, _ := json.Marshal(err) + // TODO + ret := jsRefRanges{ParseError: string(serialized)} + str, _ := json.Marshal(ret) + return string(str) + } + ret := jsRefRanges{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + var ranges []d2ast.Range + if len(mk.Edges) == 1 { + edge := d2oracle.GetEdge(g, key) + if edge == nil { + ret := jsRefRanges{D2Error: "edge not found"} + str, _ := json.Marshal(ret) + return string(str) + } + + for _, ref := range edge.References { + ranges = append(ranges, ref.MapKey.Range) + } + } else { + obj := d2oracle.GetObj(g, key) + if obj == nil { + ret := jsRefRanges{D2Error: "obj not found"} + str, _ := json.Marshal(ret) + return string(str) + } + + for _, ref := range obj.References { + ranges = append(ranges, ref.Key.Range) + } + } + + resp := jsRefRanges{ + Ranges: ranges, + } + + str, _ := json.Marshal(resp) + return string(str) +} + +type jsObject struct { + Result string `json:"result"` + UserError string `json:"userError"` + D2Error string `json:"d2Error"` +} + +type jsParseResponse struct { + DSL string `json:"dsl"` + Texts []*d2target.MText `json:"texts"` + ParseError string `json:"parseError"` + UserError string `json:"userError"` + D2Error string `json:"d2Error"` +} + +type blockFS struct{} + +func (blockFS blockFS) Open(name string) (fs.File, error) { + return nil, errors.New("import statements not currently implemented") +} + +func jsParse(this js.Value, args []js.Value) interface{} { + dsl := args[0].String() + themeID := args[1].Int() + + g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + UTF16: true, + FS: blockFS{}, + }) + var pe *d2parser.ParseError + if err != nil { + if errors.As(err, &pe) { + serialized, _ := json.Marshal(err) + ret := jsParseResponse{ParseError: string(serialized)} + str, _ := json.Marshal(ret) + return string(str) + } + ret := jsParseResponse{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + if len(g.Layers) > 0 || len(g.Scenarios) > 0 || len(g.Steps) > 0 { + ret := jsParseResponse{UserError: "layers, scenarios, and steps are not yet supported. Coming soon."} + str, _ := json.Marshal(ret) + return string(str) + } + + for _, o := range g.Objects { + if (o.Attributes.Top == nil) != (o.Attributes.Left == nil) { + ret := jsParseResponse{UserError: `keywords "top" and "left" currently must be used together`} + str, _ := json.Marshal(ret) + return string(str) + } + } + + err = g.ApplyTheme(int64(themeID)) + if err != nil { + ret := jsParseResponse{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + resp := jsParseResponse{ + Texts: g.Texts(), + } + + newDSL := d2format.Format(g.AST) + if dsl != newDSL { + resp.DSL = newDSL + } + + str, _ := json.Marshal(resp) + return string(str) +} + +// TODO error passing +// TODO recover panics +func jsCompile(this js.Value, args []js.Value) interface{} { + script := args[0].String() + + g, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ + UTF16: true, + }) + var pe *d2parser.ParseError + if err != nil { + if errors.As(err, &pe) { + serialized, _ := json.Marshal(err) + ret := jsObject{UserError: string(serialized)} + str, _ := json.Marshal(ret) + return string(str) + } + ret := jsObject{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + newScript := d2format.Format(g.AST) + if script != newScript { + ret := jsObject{Result: newScript} + str, _ := json.Marshal(ret) + return string(str) + } + + return nil +} + +func jsEncode(this js.Value, args []js.Value) interface{} { + script := args[0].String() + + encoded, err := urlenc.Encode(script) + // should never happen + if err != nil { + ret := jsObject{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + ret := jsObject{Result: encoded} + str, _ := json.Marshal(ret) + return string(str) +} + +func jsDecode(this js.Value, args []js.Value) interface{} { + script := args[0].String() + + script, err := urlenc.Decode(script) + if err != nil { + ret := jsObject{UserError: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + ret := jsObject{Result: script} + str, _ := json.Marshal(ret) + return string(str) +} diff --git a/d2renderers/d2latex/latex.go b/d2renderers/d2latex/latex.go index a822b2c7bd..d2830e632b 100644 --- a/d2renderers/d2latex/latex.go +++ b/d2renderers/d2latex/latex.go @@ -1,3 +1,5 @@ +//go:build !wasm + package d2latex import ( diff --git a/d2renderers/d2latex/latex_stub.go b/d2renderers/d2latex/latex_stub.go new file mode 100644 index 0000000000..195edf0a58 --- /dev/null +++ b/d2renderers/d2latex/latex_stub.go @@ -0,0 +1,11 @@ +//go:build wasm + +package d2latex + +func Render(s string) (_ string, err error) { + return "", nil +} + +func Measure(s string) (width, height int, err error) { + return +} From 3fd9964129baa1246a3902154b430cdbafc8fbf9 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 11 Jun 2023 16:39:19 -0700 Subject: [PATCH 02/21] remove layers restriction --- d2js/js.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index 0fcca7da25..144c575046 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -162,12 +162,6 @@ func jsParse(this js.Value, args []js.Value) interface{} { return string(str) } - if len(g.Layers) > 0 || len(g.Scenarios) > 0 || len(g.Steps) > 0 { - ret := jsParseResponse{UserError: "layers, scenarios, and steps are not yet supported. Coming soon."} - str, _ := json.Marshal(ret) - return string(str) - } - for _, o := range g.Objects { if (o.Attributes.Top == nil) != (o.Attributes.Left == nil) { ret := jsParseResponse{UserError: `keywords "top" and "left" currently must be used together`} From 5445e5e0def70cad1f51f4c6618cd25f9a3042c1 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Wed, 14 Jun 2023 14:57:43 -0700 Subject: [PATCH 03/21] update format --- d2js/js.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index 144c575046..676e9fb718 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -14,7 +14,6 @@ import ( "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2parser" - "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/urlenc" ) @@ -128,11 +127,10 @@ type jsObject struct { } type jsParseResponse struct { - DSL string `json:"dsl"` - Texts []*d2target.MText `json:"texts"` - ParseError string `json:"parseError"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` + DSL string `json:"dsl"` + ParseError string `json:"parseError"` + UserError string `json:"userError"` + D2Error string `json:"d2Error"` } type blockFS struct{} @@ -177,11 +175,14 @@ func jsParse(this js.Value, args []js.Value) interface{} { return string(str) } - resp := jsParseResponse{ - Texts: g.Texts(), + m, err := d2parser.Parse("", strings.NewReader(dsl), nil) + if err != nil { + return err } - newDSL := d2format.Format(g.AST) + resp := jsParseResponse{} + + newDSL := d2format.Format(m) if dsl != newDSL { resp.DSL = newDSL } From 72790df1eb7076e669050b7cb4d9b49744c5c60c Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 15 Jun 2023 00:15:43 -0700 Subject: [PATCH 04/21] defer imports --- d2js/js.go | 71 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index 676e9fb718..f5ba8c6207 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -5,7 +5,9 @@ package main import ( "encoding/json" "errors" + "io" "io/fs" + "os" "strings" "syscall/js" @@ -133,48 +135,71 @@ type jsParseResponse struct { D2Error string `json:"d2Error"` } -type blockFS struct{} +type emptyFile struct{} -func (blockFS blockFS) Open(name string) (fs.File, error) { - return nil, errors.New("import statements not currently implemented") +func (f *emptyFile) Stat() (os.FileInfo, error) { + return nil, nil +} + +func (f *emptyFile) Read(p []byte) (int, error) { + return 0, io.EOF +} + +func (f *emptyFile) Close() error { + return nil +} + +type detectFS struct { + importUsed bool +} + +func (detectFS detectFS) Open(name string) (fs.File, error) { + detectFS.importUsed = true + return &emptyFile{}, nil } func jsParse(this js.Value, args []js.Value) interface{} { dsl := args[0].String() themeID := args[1].Int() + detectFS := detectFS{} + g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ UTF16: true, - FS: blockFS{}, + FS: detectFS, }) - var pe *d2parser.ParseError - if err != nil { - if errors.As(err, &pe) { - serialized, _ := json.Marshal(err) - ret := jsParseResponse{ParseError: string(serialized)} + // If an import was used, client side D2 cannot reliably compile + // Defer to backend compilation + if !detectFS.importUsed { + var pe *d2parser.ParseError + if err != nil { + if errors.As(err, &pe) { + serialized, _ := json.Marshal(err) + ret := jsParseResponse{ParseError: string(serialized)} + str, _ := json.Marshal(ret) + return string(str) + } + ret := jsParseResponse{D2Error: err.Error()} str, _ := json.Marshal(ret) return string(str) } - ret := jsParseResponse{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - for _, o := range g.Objects { - if (o.Attributes.Top == nil) != (o.Attributes.Left == nil) { - ret := jsParseResponse{UserError: `keywords "top" and "left" currently must be used together`} + for _, o := range g.Objects { + if (o.Attributes.Top == nil) != (o.Attributes.Left == nil) { + ret := jsParseResponse{UserError: `keywords "top" and "left" currently must be used together`} + str, _ := json.Marshal(ret) + return string(str) + } + } + + err = g.ApplyTheme(int64(themeID)) + if err != nil { + ret := jsParseResponse{D2Error: err.Error()} str, _ := json.Marshal(ret) return string(str) } } - err = g.ApplyTheme(int64(themeID)) - if err != nil { - ret := jsParseResponse{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - m, err := d2parser.Parse("", strings.NewReader(dsl), nil) if err != nil { return err From 087bb5639dd86dc38857c5cd200b5392a9050439 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 22 Jul 2023 10:03:26 -0700 Subject: [PATCH 05/21] compile --- d2js/js.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index f5ba8c6207..4f8bd024c0 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -37,7 +37,7 @@ type jsObjOrder struct { func jsGetObjOrder(this js.Value, args []js.Value) interface{} { dsl := args[0].String() - g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ UTF16: true, }) if err != nil { @@ -46,8 +46,14 @@ func jsGetObjOrder(this js.Value, args []js.Value) interface{} { return string(str) } + objOrder, err := d2oracle.GetObjOrder(g, nil) + if err != nil { + ret := jsObjOrder{Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } resp := jsObjOrder{ - Order: d2oracle.GetObjOrder(g), + Order: objOrder, } str, _ := json.Marshal(resp) @@ -72,7 +78,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } - g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ UTF16: true, }) var pe *d2parser.ParseError @@ -91,7 +97,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { var ranges []d2ast.Range if len(mk.Edges) == 1 { - edge := d2oracle.GetEdge(g, key) + edge := d2oracle.GetEdge(g, nil, key) if edge == nil { ret := jsRefRanges{D2Error: "edge not found"} str, _ := json.Marshal(ret) @@ -102,7 +108,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { ranges = append(ranges, ref.MapKey.Range) } } else { - obj := d2oracle.GetObj(g, key) + obj := d2oracle.GetObj(g, nil, key) if obj == nil { ret := jsRefRanges{D2Error: "obj not found"} str, _ := json.Marshal(ret) @@ -164,7 +170,7 @@ func jsParse(this js.Value, args []js.Value) interface{} { detectFS := detectFS{} - g, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ UTF16: true, FS: detectFS, }) @@ -221,7 +227,7 @@ func jsParse(this js.Value, args []js.Value) interface{} { func jsCompile(this js.Value, args []js.Value) interface{} { script := args[0].String() - g, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ + g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ UTF16: true, }) var pe *d2parser.ParseError From b5179d989fa5fae28d9cc6ec0c3316f77fb53e6f Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 4 Aug 2023 10:54:33 -0700 Subject: [PATCH 06/21] fix importUsed --- d2js/js.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index 4f8bd024c0..0f64cc9509 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -38,7 +38,7 @@ func jsGetObjOrder(this js.Value, args []js.Value) interface{} { dsl := args[0].String() g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16: true, + UTF16Pos: true, }) if err != nil { ret := jsObjOrder{Error: err.Error()} @@ -79,7 +79,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { } g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16: true, + UTF16Pos: true, }) var pe *d2parser.ParseError if err != nil { @@ -159,7 +159,7 @@ type detectFS struct { importUsed bool } -func (detectFS detectFS) Open(name string) (fs.File, error) { +func (detectFS *detectFS) Open(name string) (fs.File, error) { detectFS.importUsed = true return &emptyFile{}, nil } @@ -171,8 +171,8 @@ func jsParse(this js.Value, args []js.Value) interface{} { detectFS := detectFS{} g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16: true, - FS: detectFS, + UTF16Pos: true, + FS: &detectFS, }) // If an import was used, client side D2 cannot reliably compile // Defer to backend compilation @@ -228,7 +228,7 @@ func jsCompile(this js.Value, args []js.Value) interface{} { script := args[0].String() g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ - UTF16: true, + UTF16Pos: true, }) var pe *d2parser.ParseError if err != nil { From 50f01f3fc3ec98618bad119602fc61fa7832dc01 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 25 Sep 2023 11:29:33 -0700 Subject: [PATCH 07/21] invoke callback if defined --- d2js/js.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/d2js/js.go b/d2js/js.go index 0f64cc9509..a846c6ab8c 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -26,6 +26,10 @@ func main() { js.Global().Set("d2Parse", js.FuncOf(jsParse)) js.Global().Set("d2Encode", js.FuncOf(jsEncode)) js.Global().Set("d2Decode", js.FuncOf(jsDecode)) + initCallback := js.Global().Get("onWasmInitialized") + if !initCallback.IsUndefined() { + initCallback.Invoke() + } select {} } From 9b2f68c89ff0fc6e1b651b7721deec50c000d1f7 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 2 Oct 2023 14:57:37 -0700 Subject: [PATCH 08/21] no goldmark in compile --- d2js/js.go | 4 +++- lib/textmeasure/markdown.go | 2 ++ lib/textmeasure/markdown_js.go | 13 +++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 lib/textmeasure/markdown_js.go diff --git a/d2js/js.go b/d2js/js.go index a846c6ab8c..7c6a0c8809 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -210,7 +210,9 @@ func jsParse(this js.Value, args []js.Value) interface{} { } } - m, err := d2parser.Parse("", strings.NewReader(dsl), nil) + m, err := d2parser.Parse("", strings.NewReader(dsl), &d2parser.ParseOptions{ + UTF16Pos: true, + }) if err != nil { return err } diff --git a/lib/textmeasure/markdown.go b/lib/textmeasure/markdown.go index f854239889..e2d73eedca 100644 --- a/lib/textmeasure/markdown.go +++ b/lib/textmeasure/markdown.go @@ -1,3 +1,5 @@ +//go:build !wasm + package textmeasure import ( diff --git a/lib/textmeasure/markdown_js.go b/lib/textmeasure/markdown_js.go new file mode 100644 index 0000000000..06f20a10a4 --- /dev/null +++ b/lib/textmeasure/markdown_js.go @@ -0,0 +1,13 @@ +//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 +} From 6016c7e6d94cff037dc5c689c188ca8c781fe263 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 20 Jan 2024 11:13:34 -0800 Subject: [PATCH 09/21] getparentid --- d2js/js.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/d2js/js.go b/d2js/js.go index 7c6a0c8809..a642ed6259 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -20,6 +20,7 @@ import ( ) func main() { + js.Global().Set("d2GetParentID", js.FuncOf(jsGetParentID)) js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) js.Global().Set("d2Compile", js.FuncOf(jsCompile)) @@ -64,6 +65,26 @@ func jsGetObjOrder(this js.Value, args []js.Value) interface{} { return string(str) } +func jsGetParentID(this js.Value, args []js.Value) interface{} { + id := args[0].String() + + mk, _ := d2parser.ParseMapKey(id) + + if len(mk.Edges) > 0 { + return "" + } + + if mk.Key != nil { + if len(mk.Key.Path) == 1 { + return "root" + } + mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] + return strings.Join(mk.Key.IDA(), ".") + } + + return "" +} + type jsRefRanges struct { Ranges []d2ast.Range `json:"ranges"` ParseError string `json:"parseError"` From 4550117025c2ef2a797bc67e7b36c02263312943 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Wed, 4 Sep 2024 12:20:16 -0600 Subject: [PATCH 10/21] add version call --- d2js/js.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/d2js/js.go b/d2js/js.go index a642ed6259..a7708328b9 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -17,6 +17,7 @@ import ( "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/lib/urlenc" + "oss.terrastruct.com/d2/lib/version" ) func main() { @@ -27,6 +28,7 @@ func main() { js.Global().Set("d2Parse", js.FuncOf(jsParse)) js.Global().Set("d2Encode", js.FuncOf(jsEncode)) js.Global().Set("d2Decode", js.FuncOf(jsDecode)) + js.Global().Set("d2Version", js.FuncOf(jsVersion)) initCallback := js.Global().Get("onWasmInitialized") if !initCallback.IsUndefined() { initCallback.Invoke() @@ -310,3 +312,7 @@ func jsDecode(this js.Value, args []js.Value) interface{} { str, _ := json.Marshal(ret) return string(str) } + +func jsVersion(this js.Value, args []js.Value) interface{} { + return version.Version +} From 46b499af9f74b4779cdbb9f12f22db718524b70d Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 10 Oct 2024 19:29:39 -0700 Subject: [PATCH 11/21] update getRefRanges --- d2js/js.go | 48 ++++++++++++++---------------------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index a7708328b9..3e98cf769e 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -14,6 +14,7 @@ import ( "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2lsp" "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/lib/urlenc" @@ -95,56 +96,35 @@ type jsRefRanges struct { } func jsGetRefRanges(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() + fsRaw := args[0].String() key := args[1].String() - mk, err := d2parser.ParseMapKey(key) + var fs map[string]string + err := json.Unmarshal([]byte(fsRaw), &fs) if err != nil { ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) return string(str) } - g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16Pos: true, - }) - var pe *d2parser.ParseError + _, err = d2parser.ParseMapKey(key) if err != nil { - if errors.As(err, &pe) { - serialized, _ := json.Marshal(err) - // TODO - ret := jsRefRanges{ParseError: string(serialized)} - str, _ := json.Marshal(ret) - return string(str) - } ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) return string(str) } - var ranges []d2ast.Range - if len(mk.Edges) == 1 { - edge := d2oracle.GetEdge(g, nil, key) - if edge == nil { - ret := jsRefRanges{D2Error: "edge not found"} - str, _ := json.Marshal(ret) - return string(str) - } + refs, err := d2lsp.GetFieldRefs("", fs, key) + if err != nil { + ret := jsRefRanges{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } - for _, ref := range edge.References { - ranges = append(ranges, ref.MapKey.Range) - } - } else { - obj := d2oracle.GetObj(g, nil, key) - if obj == nil { - ret := jsRefRanges{D2Error: "obj not found"} - str, _ := json.Marshal(ret) - return string(str) - } + var ranges []d2ast.Range - for _, ref := range obj.References { - ranges = append(ranges, ref.Key.Range) - } + for _, ref := range refs { + ranges = append(ranges, ref.AST().GetRange()) } resp := jsRefRanges{ From 24b8e05e35181475ae4ce5edb9dc242e42a102a1 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 11 Oct 2024 13:18:22 -0700 Subject: [PATCH 12/21] update getrefs call --- d2js/js.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index 3e98cf769e..d71f4305ec 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -97,7 +97,8 @@ type jsRefRanges struct { func jsGetRefRanges(this js.Value, args []js.Value) interface{} { fsRaw := args[0].String() - key := args[1].String() + file := args[1].String() + key := args[2].String() var fs map[string]string err := json.Unmarshal([]byte(fsRaw), &fs) @@ -114,7 +115,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } - refs, err := d2lsp.GetFieldRefs("", fs, key) + refs, err := d2lsp.GetFieldRefs("", file, fs, key) if err != nil { ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) From 1aaa55e4f4db2315677146bb3de2d70a57a21cc6 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 11 Oct 2024 17:49:16 -0700 Subject: [PATCH 13/21] support board path --- d2js/js.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/d2js/js.go b/d2js/js.go index d71f4305ec..ba432f681d 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -99,6 +99,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { fsRaw := args[0].String() file := args[1].String() key := args[2].String() + boardPathRaw := args[3].String() var fs map[string]string err := json.Unmarshal([]byte(fsRaw), &fs) @@ -115,7 +116,15 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } - refs, err := d2lsp.GetFieldRefs("", file, fs, key) + var boardPath []string + err = json.Unmarshal([]byte(boardPathRaw), &boardPath) + if err != nil { + ret := jsRefRanges{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + refs, err := d2lsp.GetFieldRefs(file, fs, key, boardPath) if err != nil { ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) From 010f3082b6220d9b6a4c6b7198e3ea4ffa4b5ccf Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 12 Oct 2024 12:42:41 -0700 Subject: [PATCH 14/21] rebase --- d2js/js.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/d2js/js.go b/d2js/js.go index ba432f681d..3b0e8a6390 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -124,7 +124,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } - refs, err := d2lsp.GetFieldRefs(file, fs, key, boardPath) + refs, err := d2lsp.GetRefs(file, fs, key, boardPath) if err != nil { ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) From 47867fb8584b4031d01ebb89aee8eb1ad1092ec3 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 17 Oct 2024 09:04:11 -0600 Subject: [PATCH 15/21] update lsp --- d2js/js.go | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/d2js/js.go b/d2js/js.go index 3b0e8a6390..1f3dfeb261 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -25,6 +25,7 @@ func main() { js.Global().Set("d2GetParentID", js.FuncOf(jsGetParentID)) js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) + js.Global().Set("d2GetImportRanges", js.FuncOf(jsGetImportRanges)) js.Global().Set("d2Compile", js.FuncOf(jsCompile)) js.Global().Set("d2Parse", js.FuncOf(jsParse)) js.Global().Set("d2Encode", js.FuncOf(jsEncode)) @@ -124,7 +125,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } - refs, err := d2lsp.GetRefs(file, fs, key, boardPath) + refs, err := d2lsp.GetRefs(file, fs, boardPath, key) if err != nil { ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) @@ -145,6 +146,34 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } +func jsGetImportRanges(this js.Value, args []js.Value) interface{} { + fsRaw := args[0].String() + path := args[1].String() + importPath := args[2].String() + + var fs map[string]string + err := json.Unmarshal([]byte(fsRaw), &fs) + if err != nil { + ret := jsRefRanges{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + ranges, err := d2lsp.GetImportRanges(path, fs, importPath) + if err != nil { + ret := jsRefRanges{D2Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + resp := jsRefRanges{ + Ranges: ranges, + } + + str, _ := json.Marshal(resp) + return string(str) +} + type jsObject struct { Result string `json:"result"` UserError string `json:"userError"` From 2b8c047b5b65f46c10b3b039d42d10e3605a7983 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 19 Oct 2024 00:09:04 -0600 Subject: [PATCH 16/21] update to latest --- d2js/js.go | 49 ++++++++----------------------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/d2js/js.go b/d2js/js.go index 1f3dfeb261..6c03d4998d 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -25,7 +25,6 @@ func main() { js.Global().Set("d2GetParentID", js.FuncOf(jsGetParentID)) js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) - js.Global().Set("d2GetImportRanges", js.FuncOf(jsGetImportRanges)) js.Global().Set("d2Compile", js.FuncOf(jsCompile)) js.Global().Set("d2Parse", js.FuncOf(jsParse)) js.Global().Set("d2Encode", js.FuncOf(jsEncode)) @@ -90,10 +89,11 @@ func jsGetParentID(this js.Value, args []js.Value) interface{} { } type jsRefRanges struct { - Ranges []d2ast.Range `json:"ranges"` - ParseError string `json:"parseError"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` + Ranges []d2ast.Range `json:"ranges"` + ImportRanges []d2ast.Range `json:"importRanges"` + ParseError string `json:"parseError"` + UserError string `json:"userError"` + D2Error string `json:"d2Error"` } func jsGetRefRanges(this js.Value, args []js.Value) interface{} { @@ -125,41 +125,7 @@ func jsGetRefRanges(this js.Value, args []js.Value) interface{} { return string(str) } - refs, err := d2lsp.GetRefs(file, fs, boardPath, key) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - var ranges []d2ast.Range - - for _, ref := range refs { - ranges = append(ranges, ref.AST().GetRange()) - } - - resp := jsRefRanges{ - Ranges: ranges, - } - - str, _ := json.Marshal(resp) - return string(str) -} - -func jsGetImportRanges(this js.Value, args []js.Value) interface{} { - fsRaw := args[0].String() - path := args[1].String() - importPath := args[2].String() - - var fs map[string]string - err := json.Unmarshal([]byte(fsRaw), &fs) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ranges, err := d2lsp.GetImportRanges(path, fs, importPath) + ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key) if err != nil { ret := jsRefRanges{D2Error: err.Error()} str, _ := json.Marshal(ret) @@ -167,7 +133,8 @@ func jsGetImportRanges(this js.Value, args []js.Value) interface{} { } resp := jsRefRanges{ - Ranges: ranges, + Ranges: ranges, + ImportRanges: importRanges, } str, _ := json.Marshal(resp) From 784a961885723b485b3d8dac6f7a8785f51365ab Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Wed, 13 Nov 2024 12:17:49 -0700 Subject: [PATCH 17/21] get board at position --- d2js/js.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/d2js/js.go b/d2js/js.go index 6c03d4998d..e21c03f5bc 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -26,6 +26,7 @@ func main() { js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) js.Global().Set("d2Compile", js.FuncOf(jsCompile)) + js.Global().Set("d2GetBoardAtPosition", js.FuncOf(jsGetBoardAtPosition)) js.Global().Set("d2Parse", js.FuncOf(jsParse)) js.Global().Set("d2Encode", js.FuncOf(jsEncode)) js.Global().Set("d2Decode", js.FuncOf(jsDecode)) @@ -302,3 +303,31 @@ func jsDecode(this js.Value, args []js.Value) interface{} { func jsVersion(this js.Value, args []js.Value) interface{} { return version.Version } + +type jsBoardAtPosition struct { + BoardPath []string `json:"boardPath"` + Error string `json:"error"` +} + +func jsGetBoardAtPosition(this js.Value, args []js.Value) interface{} { + dsl := args[0].String() + line := args[1].Int() + column := args[2].Int() + + boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{ + Line: line, + Column: column, + }) + + if err != nil { + ret := jsBoardAtPosition{Error: err.Error()} + str, _ := json.Marshal(ret) + return string(str) + } + + resp := jsBoardAtPosition{ + BoardPath: boardPath, + } + str, _ := json.Marshal(resp) + return string(str) +} From 2a561265c19486fc7d00ae247e46c35749920988 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 12 Dec 2024 15:27:28 -0700 Subject: [PATCH 18/21] add wasm tag --- d2js/js.go | 2 +- lib/textmeasure/markdown_js.go | 4 ++++ lib/textmeasure/substitutions.go | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/d2js/js.go b/d2js/js.go index e21c03f5bc..53aeb13f3d 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -83,7 +83,7 @@ func jsGetParentID(this js.Value, args []js.Value) interface{} { return "root" } mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] - return strings.Join(mk.Key.IDA(), ".") + return strings.Join(mk.Key.StringIDA(), ".") } return "" diff --git a/lib/textmeasure/markdown_js.go b/lib/textmeasure/markdown_js.go index 06f20a10a4..b23b2c101e 100644 --- a/lib/textmeasure/markdown_js.go +++ b/lib/textmeasure/markdown_js.go @@ -11,3 +11,7 @@ func MeasureMarkdown(mdText string, ruler *Ruler, fontFamily *d2fonts.FontFamily 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 a141e7385a..a21475494d 100644 --- a/lib/textmeasure/substitutions.go +++ b/lib/textmeasure/substitutions.go @@ -1,3 +1,5 @@ +//go:build !wasm + package textmeasure import ( From 8d9bf98e5912a21191bf8cdba919b3f77919036b Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 20 Dec 2024 17:08:19 -0800 Subject: [PATCH 19/21] refactor --- d2js/d2wasm/api.go | 70 ++++++++ d2js/d2wasm/functions.go | 161 +++++++++++++++++++ d2js/d2wasm/types.go | 28 ++++ d2js/js.go | 334 ++------------------------------------- 4 files changed, 274 insertions(+), 319 deletions(-) create mode 100644 d2js/d2wasm/api.go create mode 100644 d2js/d2wasm/functions.go create mode 100644 d2js/d2wasm/types.go diff --git a/d2js/d2wasm/api.go b/d2js/d2wasm/api.go new file mode 100644 index 0000000000..b3c9f7274c --- /dev/null +++ b/d2js/d2wasm/api.go @@ -0,0 +1,70 @@ +//go:build js && wasm + +package d2wasm + +import ( + "encoding/json" + "fmt" + "syscall/js" +) + +type D2API struct { + exports map[string]js.Func +} + +func NewD2API() *D2API { + return &D2API{ + exports: make(map[string]js.Func), + } +} + +func (api *D2API) Register(name string, fn func(args []js.Value) (interface{}, error)) { + api.exports[name] = wrapWASMCall(fn) +} + +func (api *D2API) ExportTo(target js.Value) { + d2Namespace := make(map[string]interface{}) + for name, fn := range api.exports { + d2Namespace[name] = fn + } + target.Set("d2", js.ValueOf(d2Namespace)) +} + +func wrapWASMCall(fn func(args []js.Value) (interface{}, error)) js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) (result any) { + defer func() { + if r := recover(); r != nil { + resp := WASMResponse{ + Error: &WASMError{ + Message: fmt.Sprintf("panic recovered: %v", r), + Code: 500, + }, + } + jsonResp, _ := json.Marshal(resp) + result = string(jsonResp) + } + }() + + data, err := fn(args) + if err != nil { + wasmErr, ok := err.(*WASMError) + if !ok { + wasmErr = &WASMError{ + Message: err.Error(), + Code: 500, + } + } + resp := WASMResponse{ + Error: wasmErr, + } + jsonResp, _ := json.Marshal(resp) + return string(jsonResp) + } + + resp := WASMResponse{ + Data: data, + } + jsonResp, _ := json.Marshal(resp) + return string(jsonResp) + }) +} diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go new file mode 100644 index 0000000000..10706fb750 --- /dev/null +++ b/d2js/d2wasm/functions.go @@ -0,0 +1,161 @@ +//go:build js && wasm + +package d2wasm + +import ( + "encoding/json" + "strings" + "syscall/js" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2compiler" + "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2lsp" + "oss.terrastruct.com/d2/d2oracle" + "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/lib/version" +) + +func GetParentID(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing id argument", Code: 400} + } + + id := args[0].String() + mk, err := d2parser.ParseMapKey(id) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 400} + } + + if len(mk.Edges) > 0 { + return "", nil + } + + if mk.Key != nil { + if len(mk.Key.Path) == 1 { + return "root", nil + } + mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] + return strings.Join(mk.Key.StringIDA(), "."), nil + } + + return "", nil +} + +func GetObjOrder(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing dsl argument", Code: 400} + } + + dsl := args[0].String() + g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + UTF16Pos: true, + }) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 400} + } + + objOrder, err := d2oracle.GetObjOrder(g, nil) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return map[string]interface{}{ + "order": objOrder, + }, nil +} + +func GetRefRanges(args []js.Value) (interface{}, error) { + if len(args) < 4 { + return nil, &WASMError{Message: "missing required arguments", Code: 400} + } + + var fs map[string]string + if err := json.Unmarshal([]byte(args[0].String()), &fs); err != nil { + return nil, &WASMError{Message: "invalid fs argument", Code: 400} + } + + file := args[1].String() + key := args[2].String() + + var boardPath []string + if err := json.Unmarshal([]byte(args[3].String()), &boardPath); err != nil { + return nil, &WASMError{Message: "invalid boardPath argument", Code: 400} + } + + ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return RefRangesResponse{ + Ranges: ranges, + ImportRanges: importRanges, + }, nil +} + +func Compile(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing script argument", Code: 400} + } + + script := args[0].String() + g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ + UTF16Pos: true, + }) + if err != nil { + if pe, ok := err.(*d2parser.ParseError); ok { + return nil, &WASMError{Message: pe.Error(), Code: 400} + } + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + newScript := d2format.Format(g.AST) + if script != newScript { + return map[string]string{"result": newScript}, nil + } + + return nil, nil +} + +func GetBoardAtPosition(args []js.Value) (interface{}, error) { + if len(args) < 3 { + return nil, &WASMError{Message: "missing required arguments", Code: 400} + } + + dsl := args[0].String() + line := args[1].Int() + column := args[2].Int() + + boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{ + Line: line, + Column: column, + }) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return BoardPositionResponse{BoardPath: boardPath}, nil +} + +func Encode(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing script argument", Code: 400} + } + + script := args[0].String() + return map[string]string{"result": script}, nil +} + +func Decode(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing script argument", Code: 400} + } + + script := args[0].String() + return map[string]string{"result": script}, nil +} + +func GetVersion(args []js.Value) (interface{}, error) { + return version.Version, nil +} diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go new file mode 100644 index 0000000000..911c6b5ebe --- /dev/null +++ b/d2js/d2wasm/types.go @@ -0,0 +1,28 @@ +//go:build js && wasm + +package d2wasm + +import "oss.terrastruct.com/d2/d2ast" + +type WASMResponse struct { + Data interface{} `json:"data,omitempty"` + Error *WASMError `json:"error,omitempty"` +} + +type WASMError struct { + Message string `json:"message"` + Code int `json:"code"` +} + +func (e *WASMError) Error() string { + return e.Message +} + +type RefRangesResponse struct { + Ranges []d2ast.Range `json:"ranges"` + ImportRanges []d2ast.Range `json:"importRanges"` +} + +type BoardPositionResponse struct { + BoardPath []string `json:"boardPath"` +} diff --git a/d2js/js.go b/d2js/js.go index 53aeb13f3d..c1461b5df8 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -1,333 +1,29 @@ -//go:build wasm +//go:build js && wasm package main import ( - "encoding/json" - "errors" - "io" - "io/fs" - "os" - "strings" "syscall/js" - "oss.terrastruct.com/d2/d2ast" - "oss.terrastruct.com/d2/d2compiler" - "oss.terrastruct.com/d2/d2format" - "oss.terrastruct.com/d2/d2lsp" - "oss.terrastruct.com/d2/d2oracle" - "oss.terrastruct.com/d2/d2parser" - "oss.terrastruct.com/d2/lib/urlenc" - "oss.terrastruct.com/d2/lib/version" + "oss.terrastruct.com/d2/d2js/d2wasm" ) func main() { - js.Global().Set("d2GetParentID", js.FuncOf(jsGetParentID)) - js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) - js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) - js.Global().Set("d2Compile", js.FuncOf(jsCompile)) - js.Global().Set("d2GetBoardAtPosition", js.FuncOf(jsGetBoardAtPosition)) - js.Global().Set("d2Parse", js.FuncOf(jsParse)) - js.Global().Set("d2Encode", js.FuncOf(jsEncode)) - js.Global().Set("d2Decode", js.FuncOf(jsDecode)) - js.Global().Set("d2Version", js.FuncOf(jsVersion)) - initCallback := js.Global().Get("onWasmInitialized") - if !initCallback.IsUndefined() { - initCallback.Invoke() - } - select {} -} - -type jsObjOrder struct { - Order []string `json:"order"` - Error string `json:"error"` -} - -func jsGetObjOrder(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() - - g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16Pos: true, - }) - if err != nil { - ret := jsObjOrder{Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - objOrder, err := d2oracle.GetObjOrder(g, nil) - if err != nil { - ret := jsObjOrder{Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - resp := jsObjOrder{ - Order: objOrder, - } - - str, _ := json.Marshal(resp) - return string(str) -} - -func jsGetParentID(this js.Value, args []js.Value) interface{} { - id := args[0].String() - - mk, _ := d2parser.ParseMapKey(id) - - if len(mk.Edges) > 0 { - return "" - } - - if mk.Key != nil { - if len(mk.Key.Path) == 1 { - return "root" - } - mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] - return strings.Join(mk.Key.StringIDA(), ".") - } - - return "" -} - -type jsRefRanges struct { - Ranges []d2ast.Range `json:"ranges"` - ImportRanges []d2ast.Range `json:"importRanges"` - ParseError string `json:"parseError"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` -} - -func jsGetRefRanges(this js.Value, args []js.Value) interface{} { - fsRaw := args[0].String() - file := args[1].String() - key := args[2].String() - boardPathRaw := args[3].String() - - var fs map[string]string - err := json.Unmarshal([]byte(fsRaw), &fs) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - _, err = d2parser.ParseMapKey(key) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - var boardPath []string - err = json.Unmarshal([]byte(boardPathRaw), &boardPath) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - resp := jsRefRanges{ - Ranges: ranges, - ImportRanges: importRanges, - } - - str, _ := json.Marshal(resp) - return string(str) -} - -type jsObject struct { - Result string `json:"result"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` -} - -type jsParseResponse struct { - DSL string `json:"dsl"` - ParseError string `json:"parseError"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` -} - -type emptyFile struct{} - -func (f *emptyFile) Stat() (os.FileInfo, error) { - return nil, nil -} - -func (f *emptyFile) Read(p []byte) (int, error) { - return 0, io.EOF -} - -func (f *emptyFile) Close() error { - return nil -} - -type detectFS struct { - importUsed bool -} - -func (detectFS *detectFS) Open(name string) (fs.File, error) { - detectFS.importUsed = true - return &emptyFile{}, nil -} - -func jsParse(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() - themeID := args[1].Int() - - detectFS := detectFS{} - - g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16Pos: true, - FS: &detectFS, - }) - // If an import was used, client side D2 cannot reliably compile - // Defer to backend compilation - if !detectFS.importUsed { - var pe *d2parser.ParseError - if err != nil { - if errors.As(err, &pe) { - serialized, _ := json.Marshal(err) - ret := jsParseResponse{ParseError: string(serialized)} - str, _ := json.Marshal(ret) - return string(str) - } - ret := jsParseResponse{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - for _, o := range g.Objects { - if (o.Attributes.Top == nil) != (o.Attributes.Left == nil) { - ret := jsParseResponse{UserError: `keywords "top" and "left" currently must be used together`} - str, _ := json.Marshal(ret) - return string(str) - } - } - - err = g.ApplyTheme(int64(themeID)) - if err != nil { - ret := jsParseResponse{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - } - - m, err := d2parser.Parse("", strings.NewReader(dsl), &d2parser.ParseOptions{ - UTF16Pos: true, - }) - if err != nil { - return err - } - - resp := jsParseResponse{} - - newDSL := d2format.Format(m) - if dsl != newDSL { - resp.DSL = newDSL - } - - str, _ := json.Marshal(resp) - return string(str) -} - -// TODO error passing -// TODO recover panics -func jsCompile(this js.Value, args []js.Value) interface{} { - script := args[0].String() - - g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ - UTF16Pos: true, - }) - var pe *d2parser.ParseError - if err != nil { - if errors.As(err, &pe) { - serialized, _ := json.Marshal(err) - ret := jsObject{UserError: string(serialized)} - str, _ := json.Marshal(ret) - return string(str) - } - ret := jsObject{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - newScript := d2format.Format(g.AST) - if script != newScript { - ret := jsObject{Result: newScript} - str, _ := json.Marshal(ret) - return string(str) - } - - return nil -} - -func jsEncode(this js.Value, args []js.Value) interface{} { - script := args[0].String() - - encoded, err := urlenc.Encode(script) - // should never happen - if err != nil { - ret := jsObject{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ret := jsObject{Result: encoded} - str, _ := json.Marshal(ret) - return string(str) -} - -func jsDecode(this js.Value, args []js.Value) interface{} { - script := args[0].String() + api := d2wasm.NewD2API() - script, err := urlenc.Decode(script) - if err != nil { - ret := jsObject{UserError: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ret := jsObject{Result: script} - str, _ := json.Marshal(ret) - return string(str) -} + api.Register("getParentID", d2wasm.GetParentID) + api.Register("getObjOrder", d2wasm.GetObjOrder) + api.Register("getRefRanges", d2wasm.GetRefRanges) + api.Register("compile", d2wasm.Compile) + api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition) + api.Register("encode", d2wasm.Encode) + api.Register("decode", d2wasm.Decode) + api.Register("version", d2wasm.GetVersion) -func jsVersion(this js.Value, args []js.Value) interface{} { - return version.Version -} - -type jsBoardAtPosition struct { - BoardPath []string `json:"boardPath"` - Error string `json:"error"` -} + api.ExportTo(js.Global()) -func jsGetBoardAtPosition(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() - line := args[1].Int() - column := args[2].Int() - - boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{ - Line: line, - Column: column, - }) - - if err != nil { - ret := jsBoardAtPosition{Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) + if cb := js.Global().Get("onWasmInitialized"); !cb.IsUndefined() { + cb.Invoke() } - - resp := jsBoardAtPosition{ - BoardPath: boardPath, - } - str, _ := json.Marshal(resp) - return string(str) + select {} } From 9e20ed816d7f20b5915342cf0692da17257c7dd7 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 29 Dec 2024 09:50:10 -0700 Subject: [PATCH 20/21] integrate with d2lsp completions --- d2js/d2wasm/functions.go | 34 ++++++++++++++++++++++++++++++++++ d2js/js.go | 1 + 2 files changed, 35 insertions(+) diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go index 10706fb750..25250880f0 100644 --- a/d2js/d2wasm/functions.go +++ b/d2js/d2wasm/functions.go @@ -159,3 +159,37 @@ func Decode(args []js.Value) (interface{}, error) { func GetVersion(args []js.Value) (interface{}, error) { return version.Version, nil } + +func GetCompletions(args []js.Value) (interface{}, error) { + if len(args) < 3 { + return nil, &WASMError{Message: "missing required arguments", Code: 400} + } + + text := args[0].String() + line := args[1].Int() + column := args[2].Int() + + completions, err := d2lsp.GetCompletionItems(text, line, column) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + // Convert to map for JSON serialization + items := make([]map[string]interface{}, len(completions)) + for i, completion := range completions { + items[i] = map[string]interface{}{ + "label": completion.Label, + "kind": int(completion.Kind), + "detail": completion.Detail, + "insertText": completion.InsertText, + } + } + + return CompletionResponse{ + Items: items, + }, nil +} + +type CompletionResponse struct { + Items []map[string]interface{} `json:"items"` +} diff --git a/d2js/js.go b/d2js/js.go index c1461b5df8..50280194c6 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -11,6 +11,7 @@ import ( func main() { api := d2wasm.NewD2API() + api.Register("getCompletions", d2wasm.GetCompletions) api.Register("getParentID", d2wasm.GetParentID) api.Register("getObjOrder", d2wasm.GetObjOrder) api.Register("getRefRanges", d2wasm.GetRefRanges) From 3dee7bbdaf0bc09fefbaf553655d711aed1d48d2 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 29 Dec 2024 14:19:32 -0700 Subject: [PATCH 21/21] compile and render functions --- d2js/d2wasm/api.go | 3 +- d2js/d2wasm/functions.go | 125 ++++++++++++++++++++++++++---- d2js/d2wasm/types.go | 32 +++++++- d2js/js.go | 1 + d2renderers/d2latex/latex.go | 2 - d2renderers/d2latex/latex_stub.go | 11 --- d2renderers/d2svg/d2svg.go | 2 + lib/textmeasure/markdown.go | 2 - lib/textmeasure/markdown_js.go | 17 ---- lib/textmeasure/substitutions.go | 2 - 10 files changed, 147 insertions(+), 50 deletions(-) delete mode 100644 d2renderers/d2latex/latex_stub.go delete mode 100644 lib/textmeasure/markdown_js.go diff --git a/d2js/d2wasm/api.go b/d2js/d2wasm/api.go index b3c9f7274c..e87386cd64 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 25250880f0..d82965a69e 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} + } + + 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 nil, nil + 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 911c6b5ebe..a13b82baec 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 50280194c6..514fd5179d 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 d2830e632b..a822b2c7bd 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 195edf0a58..0000000000 --- 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 cb14ff0413..12ea2e7c0a 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 e2d73eedca..f854239889 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 b23b2c101e..0000000000 --- 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 a21475494d..a141e7385a 100644 --- a/lib/textmeasure/substitutions.go +++ b/lib/textmeasure/substitutions.go @@ -1,5 +1,3 @@ -//go:build !wasm - package textmeasure import (