|
| 1 | +//go:build js && wasm |
| 2 | + |
| 3 | +package d2wasm |
| 4 | + |
| 5 | +import ( |
| 6 | + "context" |
| 7 | + "encoding/json" |
| 8 | + "fmt" |
| 9 | + "strings" |
| 10 | + "syscall/js" |
| 11 | + |
| 12 | + "oss.terrastruct.com/d2/d2ast" |
| 13 | + "oss.terrastruct.com/d2/d2compiler" |
| 14 | + "oss.terrastruct.com/d2/d2format" |
| 15 | + "oss.terrastruct.com/d2/d2graph" |
| 16 | + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" |
| 17 | + "oss.terrastruct.com/d2/d2layouts/d2elklayout" |
| 18 | + "oss.terrastruct.com/d2/d2lib" |
| 19 | + "oss.terrastruct.com/d2/d2lsp" |
| 20 | + "oss.terrastruct.com/d2/d2oracle" |
| 21 | + "oss.terrastruct.com/d2/d2parser" |
| 22 | + "oss.terrastruct.com/d2/d2renderers/d2fonts" |
| 23 | + "oss.terrastruct.com/d2/d2renderers/d2svg" |
| 24 | + "oss.terrastruct.com/d2/lib/log" |
| 25 | + "oss.terrastruct.com/d2/lib/memfs" |
| 26 | + "oss.terrastruct.com/d2/lib/textmeasure" |
| 27 | + "oss.terrastruct.com/d2/lib/urlenc" |
| 28 | + "oss.terrastruct.com/d2/lib/version" |
| 29 | + "oss.terrastruct.com/util-go/go2" |
| 30 | +) |
| 31 | + |
| 32 | +func GetParentID(args []js.Value) (interface{}, error) { |
| 33 | + if len(args) < 1 { |
| 34 | + return nil, &WASMError{Message: "missing id argument", Code: 400} |
| 35 | + } |
| 36 | + |
| 37 | + id := args[0].String() |
| 38 | + mk, err := d2parser.ParseMapKey(id) |
| 39 | + if err != nil { |
| 40 | + return nil, &WASMError{Message: err.Error(), Code: 400} |
| 41 | + } |
| 42 | + |
| 43 | + if len(mk.Edges) > 0 { |
| 44 | + return "", nil |
| 45 | + } |
| 46 | + |
| 47 | + if mk.Key != nil { |
| 48 | + if len(mk.Key.Path) == 1 { |
| 49 | + return "root", nil |
| 50 | + } |
| 51 | + mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] |
| 52 | + return strings.Join(mk.Key.StringIDA(), "."), nil |
| 53 | + } |
| 54 | + |
| 55 | + return "", nil |
| 56 | +} |
| 57 | + |
| 58 | +func GetObjOrder(args []js.Value) (interface{}, error) { |
| 59 | + if len(args) < 1 { |
| 60 | + return nil, &WASMError{Message: "missing dsl argument", Code: 400} |
| 61 | + } |
| 62 | + |
| 63 | + dsl := args[0].String() |
| 64 | + g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ |
| 65 | + UTF16Pos: true, |
| 66 | + }) |
| 67 | + if err != nil { |
| 68 | + return nil, &WASMError{Message: err.Error(), Code: 400} |
| 69 | + } |
| 70 | + |
| 71 | + objOrder, err := d2oracle.GetObjOrder(g, nil) |
| 72 | + if err != nil { |
| 73 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 74 | + } |
| 75 | + |
| 76 | + return map[string]interface{}{ |
| 77 | + "order": objOrder, |
| 78 | + }, nil |
| 79 | +} |
| 80 | + |
| 81 | +func GetRefRanges(args []js.Value) (interface{}, error) { |
| 82 | + if len(args) < 4 { |
| 83 | + return nil, &WASMError{Message: "missing required arguments", Code: 400} |
| 84 | + } |
| 85 | + |
| 86 | + var fs map[string]string |
| 87 | + if err := json.Unmarshal([]byte(args[0].String()), &fs); err != nil { |
| 88 | + return nil, &WASMError{Message: "invalid fs argument", Code: 400} |
| 89 | + } |
| 90 | + |
| 91 | + file := args[1].String() |
| 92 | + key := args[2].String() |
| 93 | + |
| 94 | + var boardPath []string |
| 95 | + if err := json.Unmarshal([]byte(args[3].String()), &boardPath); err != nil { |
| 96 | + return nil, &WASMError{Message: "invalid boardPath argument", Code: 400} |
| 97 | + } |
| 98 | + |
| 99 | + ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key) |
| 100 | + if err != nil { |
| 101 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 102 | + } |
| 103 | + |
| 104 | + return RefRangesResponse{ |
| 105 | + Ranges: ranges, |
| 106 | + ImportRanges: importRanges, |
| 107 | + }, nil |
| 108 | +} |
| 109 | + |
| 110 | +func Compile(args []js.Value) (interface{}, error) { |
| 111 | + if len(args) < 1 { |
| 112 | + return nil, &WASMError{Message: "missing JSON argument", Code: 400} |
| 113 | + } |
| 114 | + var input CompileRequest |
| 115 | + if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil { |
| 116 | + return nil, &WASMError{Message: "invalid JSON input", Code: 400} |
| 117 | + } |
| 118 | + |
| 119 | + if input.FS == nil { |
| 120 | + return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400} |
| 121 | + } |
| 122 | + |
| 123 | + if _, ok := input.FS["index"]; !ok { |
| 124 | + return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400} |
| 125 | + } |
| 126 | + |
| 127 | + fs, err := memfs.New(input.FS) |
| 128 | + if err != nil { |
| 129 | + return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400} |
| 130 | + } |
| 131 | + |
| 132 | + ruler, err := textmeasure.NewRuler() |
| 133 | + if err != nil { |
| 134 | + return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500} |
| 135 | + } |
| 136 | + ctx := log.WithDefault(context.Background()) |
| 137 | + layoutFunc := d2dagrelayout.DefaultLayout |
| 138 | + if input.Opts != nil && input.Opts.Layout != nil { |
| 139 | + switch *input.Opts.Layout { |
| 140 | + case "dagre": |
| 141 | + layoutFunc = d2dagrelayout.DefaultLayout |
| 142 | + case "elk": |
| 143 | + layoutFunc = d2elklayout.DefaultLayout |
| 144 | + default: |
| 145 | + return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", *input.Opts.Layout), Code: 400} |
| 146 | + } |
| 147 | + } |
| 148 | + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { |
| 149 | + return layoutFunc, nil |
| 150 | + } |
| 151 | + |
| 152 | + renderOpts := &d2svg.RenderOpts{} |
| 153 | + var fontFamily *d2fonts.FontFamily |
| 154 | + if input.Opts != nil && input.Opts.Sketch != nil { |
| 155 | + fontFamily = go2.Pointer(d2fonts.HandDrawn) |
| 156 | + renderOpts.Sketch = input.Opts.Sketch |
| 157 | + } |
| 158 | + if input.Opts != nil && input.Opts.ThemeID != nil { |
| 159 | + renderOpts.ThemeID = input.Opts.ThemeID |
| 160 | + } |
| 161 | + diagram, g, err := d2lib.Compile(ctx, input.FS["index"], &d2lib.CompileOptions{ |
| 162 | + UTF16Pos: true, |
| 163 | + FS: fs, |
| 164 | + Ruler: ruler, |
| 165 | + LayoutResolver: layoutResolver, |
| 166 | + FontFamily: fontFamily, |
| 167 | + }, renderOpts) |
| 168 | + if err != nil { |
| 169 | + if pe, ok := err.(*d2parser.ParseError); ok { |
| 170 | + return nil, &WASMError{Message: pe.Error(), Code: 400} |
| 171 | + } |
| 172 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 173 | + } |
| 174 | + |
| 175 | + input.FS["index"] = d2format.Format(g.AST) |
| 176 | + |
| 177 | + return CompileResponse{ |
| 178 | + FS: input.FS, |
| 179 | + Diagram: *diagram, |
| 180 | + Graph: *g, |
| 181 | + }, nil |
| 182 | +} |
| 183 | + |
| 184 | +func Render(args []js.Value) (interface{}, error) { |
| 185 | + if len(args) < 1 { |
| 186 | + return nil, &WASMError{Message: "missing JSON argument", Code: 400} |
| 187 | + } |
| 188 | + var input RenderRequest |
| 189 | + if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil { |
| 190 | + return nil, &WASMError{Message: "invalid JSON input", Code: 400} |
| 191 | + } |
| 192 | + |
| 193 | + if input.Diagram == nil { |
| 194 | + return nil, &WASMError{Message: "missing 'diagram' field in input JSON", Code: 400} |
| 195 | + } |
| 196 | + |
| 197 | + renderOpts := &d2svg.RenderOpts{} |
| 198 | + if input.Opts != nil && input.Opts.Sketch != nil { |
| 199 | + renderOpts.Sketch = input.Opts.Sketch |
| 200 | + } |
| 201 | + if input.Opts != nil && input.Opts.ThemeID != nil { |
| 202 | + renderOpts.ThemeID = input.Opts.ThemeID |
| 203 | + } |
| 204 | + out, err := d2svg.Render(input.Diagram, renderOpts) |
| 205 | + if err != nil { |
| 206 | + return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500} |
| 207 | + } |
| 208 | + |
| 209 | + return out, nil |
| 210 | +} |
| 211 | + |
| 212 | +func GetBoardAtPosition(args []js.Value) (interface{}, error) { |
| 213 | + if len(args) < 3 { |
| 214 | + return nil, &WASMError{Message: "missing required arguments", Code: 400} |
| 215 | + } |
| 216 | + |
| 217 | + dsl := args[0].String() |
| 218 | + line := args[1].Int() |
| 219 | + column := args[2].Int() |
| 220 | + |
| 221 | + boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{ |
| 222 | + Line: line, |
| 223 | + Column: column, |
| 224 | + }) |
| 225 | + if err != nil { |
| 226 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 227 | + } |
| 228 | + |
| 229 | + return BoardPositionResponse{BoardPath: boardPath}, nil |
| 230 | +} |
| 231 | + |
| 232 | +func Encode(args []js.Value) (interface{}, error) { |
| 233 | + if len(args) < 1 { |
| 234 | + return nil, &WASMError{Message: "missing script argument", Code: 400} |
| 235 | + } |
| 236 | + |
| 237 | + script := args[0].String() |
| 238 | + encoded, err := urlenc.Encode(script) |
| 239 | + // should never happen |
| 240 | + if err != nil { |
| 241 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 242 | + } |
| 243 | + |
| 244 | + return map[string]string{"result": encoded}, nil |
| 245 | +} |
| 246 | + |
| 247 | +func Decode(args []js.Value) (interface{}, error) { |
| 248 | + if len(args) < 1 { |
| 249 | + return nil, &WASMError{Message: "missing script argument", Code: 400} |
| 250 | + } |
| 251 | + |
| 252 | + script := args[0].String() |
| 253 | + script, err := urlenc.Decode(script) |
| 254 | + if err != nil { |
| 255 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 256 | + } |
| 257 | + return map[string]string{"result": script}, nil |
| 258 | +} |
| 259 | + |
| 260 | +func GetVersion(args []js.Value) (interface{}, error) { |
| 261 | + return version.Version, nil |
| 262 | +} |
| 263 | + |
| 264 | +func GetCompletions(args []js.Value) (interface{}, error) { |
| 265 | + if len(args) < 3 { |
| 266 | + return nil, &WASMError{Message: "missing required arguments", Code: 400} |
| 267 | + } |
| 268 | + |
| 269 | + text := args[0].String() |
| 270 | + line := args[1].Int() |
| 271 | + column := args[2].Int() |
| 272 | + |
| 273 | + completions, err := d2lsp.GetCompletionItems(text, line, column) |
| 274 | + if err != nil { |
| 275 | + return nil, &WASMError{Message: err.Error(), Code: 500} |
| 276 | + } |
| 277 | + |
| 278 | + // Convert to map for JSON serialization |
| 279 | + items := make([]map[string]interface{}, len(completions)) |
| 280 | + for i, completion := range completions { |
| 281 | + items[i] = map[string]interface{}{ |
| 282 | + "label": completion.Label, |
| 283 | + "kind": int(completion.Kind), |
| 284 | + "detail": completion.Detail, |
| 285 | + "insertText": completion.InsertText, |
| 286 | + } |
| 287 | + } |
| 288 | + |
| 289 | + return CompletionResponse{ |
| 290 | + Items: items, |
| 291 | + }, nil |
| 292 | +} |
0 commit comments