Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: wasm build #436

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
30 changes: 30 additions & 0 deletions d2js/README.md
Original file line number Diff line number Diff line change
@@ -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
304 changes: 304 additions & 0 deletions d2js/js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
//go:build 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"
)

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("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.IDA(), ".")
}

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()

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)
}

func jsVersion(this js.Value, args []js.Value) interface{} {
return version.Version
}
2 changes: 2 additions & 0 deletions d2renderers/d2latex/latex.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !wasm

package d2latex

import (
Expand Down
11 changes: 11 additions & 0 deletions d2renderers/d2latex/latex_stub.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions lib/textmeasure/markdown.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !wasm

package textmeasure

import (
Expand Down
13 changes: 13 additions & 0 deletions lib/textmeasure/markdown_js.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading