d2/d2js/d2wasm/functions.go

384 lines
10 KiB
Go
Raw Normal View History

2024-12-21 01:08:19 +00:00
//go:build js && wasm
package d2wasm
import (
2024-12-29 21:19:32 +00:00
"context"
2024-12-21 01:08:19 +00:00
"encoding/json"
2024-12-29 21:19:32 +00:00
"fmt"
2024-12-21 01:08:19 +00:00
"strings"
"syscall/js"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format"
2024-12-29 21:19:32 +00:00
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
"oss.terrastruct.com/d2/d2lib"
2024-12-21 01:08:19 +00:00
"oss.terrastruct.com/d2/d2lsp"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2parser"
2024-12-29 21:19:32 +00:00
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
2025-02-13 21:59:29 +00:00
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
2024-12-29 21:19:32 +00:00
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/memfs"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/urlenc"
2024-12-21 01:08:19 +00:00
"oss.terrastruct.com/d2/lib/version"
2024-12-29 21:19:32 +00:00
"oss.terrastruct.com/util-go/go2"
2024-12-21 01:08:19 +00:00
)
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
}
2025-01-14 18:55:48 +00:00
func GetELKGraph(args []js.Value) (interface{}, error) {
if len(args) < 1 {
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}
}
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}
}
g, _, err := d2compiler.Compile("", strings.NewReader(input.FS["index"]), &d2compiler.CompileOptions{
UTF16Pos: true,
FS: fs,
})
if err != nil {
return nil, &WASMError{Message: 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}
}
err = g.SetDimensions(nil, ruler, nil)
if err != nil {
return nil, err
}
elk, err := d2elklayout.ConvertGraph(context.Background(), g, nil)
if err != nil {
return nil, &WASMError{Message: err.Error(), Code: 400}
}
return elk, nil
}
2024-12-21 01:08:19 +00:00
func Compile(args []js.Value) (interface{}, error) {
if len(args) < 1 {
2024-12-29 21:19:32 +00:00
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}
2024-12-21 01:08:19 +00:00
}
2024-12-29 21:19:32 +00:00
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}
}
2025-02-14 22:04:30 +00:00
compileOpts := &d2lib.CompileOptions{
UTF16Pos: true,
}
compileOpts.LayoutResolver = func(engine string) (d2graph.LayoutGraph, error) {
switch engine {
case "dagre":
return d2dagrelayout.DefaultLayout, nil
case "elk":
return d2elklayout.DefaultLayout, nil
default:
return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", engine), Code: 400}
}
2025-02-14 22:04:30 +00:00
}
var err error
compileOpts.FS, err = memfs.New(input.FS)
2024-12-29 21:19:32 +00:00
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
}
2025-02-14 22:04:30 +00:00
compileOpts.Ruler, err = textmeasure.NewRuler()
2024-12-29 21:19:32 +00:00
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
}
2025-02-14 22:04:30 +00:00
2024-12-29 21:19:32 +00:00
if input.Opts != nil && input.Opts.Layout != nil {
2025-02-14 22:04:30 +00:00
compileOpts.Layout = input.Opts.Layout
2024-12-29 21:19:32 +00:00
}
renderOpts := &d2svg.RenderOpts{}
if input.Opts != nil && input.Opts.Sketch != nil {
2024-12-29 21:19:32 +00:00
renderOpts.Sketch = input.Opts.Sketch
if *input.Opts.Sketch {
compileOpts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
}
2024-12-29 21:19:32 +00:00
}
2025-02-11 04:10:27 +00:00
if input.Opts != nil && input.Opts.Pad != nil {
renderOpts.Pad = input.Opts.Pad
}
if input.Opts != nil && input.Opts.Center != nil {
renderOpts.Center = input.Opts.Center
}
2024-12-29 21:19:32 +00:00
if input.Opts != nil && input.Opts.ThemeID != nil {
renderOpts.ThemeID = input.Opts.ThemeID
}
2025-02-11 04:10:27 +00:00
if input.Opts != nil && input.Opts.DarkThemeID != nil {
renderOpts.DarkThemeID = input.Opts.DarkThemeID
}
if input.Opts != nil && input.Opts.Scale != nil {
renderOpts.Scale = input.Opts.Scale
}
2025-02-14 22:04:30 +00:00
ctx := log.WithDefault(context.Background())
diagram, g, err := d2lib.Compile(ctx, input.FS["index"], compileOpts, renderOpts)
2024-12-21 01:08:19 +00:00
if err != nil {
if pe, ok := err.(*d2parser.ParseError); ok {
2025-02-04 17:01:08 +00:00
errs, _ := json.Marshal(pe.Errors)
return nil, &WASMError{Message: string(errs), Code: 400}
2024-12-21 01:08:19 +00:00
}
return nil, &WASMError{Message: err.Error(), Code: 500}
}
2024-12-29 21:19:32 +00:00
input.FS["index"] = d2format.Format(g.AST)
return CompileResponse{
FS: input.FS,
Diagram: *diagram,
Graph: *g,
RenderOptions: RenderOptions{
2025-02-14 22:04:30 +00:00
ThemeID: renderOpts.ThemeID,
DarkThemeID: renderOpts.DarkThemeID,
Sketch: renderOpts.Sketch,
Pad: renderOpts.Pad,
Center: renderOpts.Center,
Scale: renderOpts.Scale,
ForceAppendix: input.Opts.ForceAppendix,
},
}, nil
2024-12-29 21:19:32 +00:00
}
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}
}
2025-02-13 21:59:29 +00:00
ruler, err := textmeasure.NewRuler()
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
}
2024-12-29 21:19:32 +00:00
renderOpts := &d2svg.RenderOpts{}
if input.Opts != nil && input.Opts.Sketch != nil {
renderOpts.Sketch = input.Opts.Sketch
}
2025-02-11 04:10:27 +00:00
if input.Opts != nil && input.Opts.Pad != nil {
renderOpts.Pad = input.Opts.Pad
}
if input.Opts != nil && input.Opts.Center != nil {
renderOpts.Center = input.Opts.Center
}
2024-12-29 21:19:32 +00:00
if input.Opts != nil && input.Opts.ThemeID != nil {
renderOpts.ThemeID = input.Opts.ThemeID
}
2025-02-11 04:10:27 +00:00
if input.Opts != nil && input.Opts.DarkThemeID != nil {
renderOpts.DarkThemeID = input.Opts.DarkThemeID
}
if input.Opts != nil && input.Opts.Scale != nil {
renderOpts.Scale = input.Opts.Scale
}
2024-12-29 21:19:32 +00:00
out, err := d2svg.Render(input.Diagram, renderOpts)
if err != nil {
return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500}
2024-12-21 01:08:19 +00:00
}
2025-02-14 14:15:45 +00:00
if input.Opts != nil && input.Opts.ForceAppendix != nil && *input.Opts.ForceAppendix {
2025-02-13 21:59:29 +00:00
out = appendix.Append(input.Diagram, renderOpts, ruler, out)
}
2024-12-21 01:08:19 +00:00
2024-12-29 21:19:32 +00:00
return out, nil
2024-12-21 01:08:19 +00:00
}
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()
2024-12-29 21:19:32 +00:00
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
2024-12-21 01:08:19 +00:00
}
func Decode(args []js.Value) (interface{}, error) {
if len(args) < 1 {
return nil, &WASMError{Message: "missing script argument", Code: 400}
}
script := args[0].String()
2024-12-29 21:19:32 +00:00
script, err := urlenc.Decode(script)
if err != nil {
return nil, &WASMError{Message: err.Error(), Code: 500}
}
2024-12-21 01:08:19 +00:00
return map[string]string{"result": script}, nil
}
func GetVersion(args []js.Value) (interface{}, error) {
return version.Version, nil
}
2024-12-29 16:50:10 +00:00
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
}