From b15161f231a1e66f5e6751fb162c17fdf38109d4 Mon Sep 17 00:00:00 2001 From: Maricaya <915270549@qq.com> Date: Sat, 21 Dec 2024 00:09:36 -0600 Subject: [PATCH 01/36] feat(cli): add format flag to support PNG output to stdout --- d2cli/export.go | 19 +++++++++++++++++++ d2cli/export_test.go | 13 ++++++++++++- d2cli/main.go | 41 ++++++++++++++++++++++++++--------------- d2cli/watch.go | 4 ++-- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/d2cli/export.go b/d2cli/export.go index 6da34bdb89..95eee59abb 100644 --- a/d2cli/export.go +++ b/d2cli/export.go @@ -1,6 +1,7 @@ package d2cli import ( + "fmt" "path/filepath" ) @@ -14,6 +15,24 @@ const SVG exportExtension = ".svg" var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF} +func getOutputFormat(formatFlag *string, outputPath string) (exportExtension, error) { + var formatMap = map[string]exportExtension{ + "png": PNG, + "svg": SVG, + "pdf": PDF, + "pptx": PPTX, + "gif": GIF, + } + + if *formatFlag != "" { + if format, ok := formatMap[*formatFlag]; ok { + return format, nil + } + return "", fmt.Errorf("unsupported format: %s", *formatFlag) + } + return getExportExtension(outputPath), nil +} + func getExportExtension(outputPath string) exportExtension { ext := filepath.Ext(outputPath) for _, kext := range SUPPORTED_EXTENSIONS { diff --git a/d2cli/export_test.go b/d2cli/export_test.go index eb7ac44ee5..cd8df49a08 100644 --- a/d2cli/export_test.go +++ b/d2cli/export_test.go @@ -8,6 +8,7 @@ import ( func TestOutputFormat(t *testing.T) { type testCase struct { + formatFlag string outputPath string extension exportExtension supportsDarkTheme bool @@ -41,6 +42,15 @@ func TestOutputFormat(t *testing.T) { requiresAnimationInterval: false, requiresPngRender: false, }, + { + formatFlag: "png", + outputPath: "-", + extension: PNG, + supportsDarkTheme: false, + supportsAnimation: false, + requiresAnimationInterval: false, + requiresPngRender: true, + }, { outputPath: "/out.png", extension: PNG, @@ -78,7 +88,8 @@ func TestOutputFormat(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.outputPath, func(t *testing.T) { - extension := getExportExtension(tc.outputPath) + extension, err := getOutputFormat(&tc.formatFlag, tc.outputPath) + assert.NoError(t, err) assert.Equal(t, tc.extension, extension) assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation()) assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme()) diff --git a/d2cli/main.go b/d2cli/main.go index eeefd6ae91..e292efa7a1 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -103,6 +103,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if err != nil { return err } + formatFlag := ms.Opts.String("", "format", "f", "", "stdout output format (svg, png)") + if err != nil { + return err + } + browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.") centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen") if err != nil { @@ -213,7 +218,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if filepath.Ext(outputPath) == ".ppt" { return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?") } - outputFormat := getExportExtension(outputPath) + + outputFormat, err := getOutputFormat(formatFlag, outputPath) + if err != nil { + return xmain.UsageErrorf("%v", err) + } + if outputPath != "-" { outputPath = ms.AbsPath(outputPath) if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() { @@ -325,6 +335,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { forceAppendix: *forceAppendixFlag, pw: pw, fontFamily: fontFamily, + outputFormat: outputFormat, }) if err != nil { return err @@ -355,7 +366,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2) defer cancel() - _, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page) + _, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page, outputFormat) if err != nil { if written { return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err) @@ -430,7 +441,7 @@ func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu } } -func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) { +func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page, ext exportExtension) (_ []byte, written bool, _ error) { start := time.Now() input, err := ms.ReadPath(inputPath) if err != nil { @@ -522,7 +533,6 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs return nil, false, err } - ext := getExportExtension(outputPath) switch ext { case GIF: svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram) @@ -598,9 +608,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs var boards [][]byte var err error if noChildren { - boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram) + boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext) } else { - boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram) + boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext) } if err != nil { return nil, false, err @@ -739,7 +749,7 @@ func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string return nil } -func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) { +func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, ext exportExtension) ([][]byte, error) { if diagram.Name != "" { ext := filepath.Ext(outputPath) outputPath = strings.TrimSuffix(outputPath, ext) @@ -785,21 +795,21 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug var boards [][]byte for _, dl := range diagram.Layers { - childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl) + childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl, ext) if err != nil { return nil, err } boards = append(boards, childrenBoards...) } for _, dl := range diagram.Scenarios { - childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl) + childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl, ext) if err != nil { return nil, err } boards = append(boards, childrenBoards...) } for _, dl := range diagram.Steps { - childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl) + childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl, ext) if err != nil { return nil, err } @@ -808,7 +818,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug if !diagram.IsFolderOnly { start := time.Now() - out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram) + out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram, ext) if err != nil { return boards, err } @@ -822,9 +832,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug return boards, nil } -func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) { +func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([][]byte, error) { start := time.Now() - out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram) + out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, outputFormat) if err != nil { return [][]byte{}, err } @@ -835,8 +845,9 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration return [][]byte{out}, nil } -func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) { - toPNG := getExportExtension(outputPath) == PNG +func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([]byte, error) { + toPNG := outputFormat == PNG + var scale *float64 if opts.Scale != nil { scale = opts.Scale diff --git a/d2cli/watch.go b/d2cli/watch.go index 61b236348c..35188970e0 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -3,7 +3,6 @@ package d2cli import ( "context" "embed" - _ "embed" "errors" "fmt" "io/fs" @@ -57,6 +56,7 @@ type watcherOpts struct { forceAppendix bool pw png.Playwright fontFamily *d2fonts.FontFamily + outputFormat exportExtension } type watcher struct { @@ -430,7 +430,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { if w.boardPath != "" { boardPath = strings.Split(w.boardPath, string(os.PathSeparator)) } - svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page) + svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page, w.outputFormat) w.boardpathMu.Unlock() errs := "" if err != nil { From 9bd151bea90018ff0660104244d17e796036d08c Mon Sep 17 00:00:00 2001 From: Maricaya <915270549@qq.com> Date: Mon, 30 Dec 2024 16:49:35 -0600 Subject: [PATCH 02/36] refactor: rename format flag to stdout-format --- ci/release/changelogs/next.md | 1 + d2cli/export.go | 25 +++++++++++++------------ d2cli/export_test.go | 6 +++--- d2cli/main.go | 4 ++-- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 302685f371..4942a9ab6c 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -4,6 +4,7 @@ - Connections now support `link` [#1955](https://github.com/terrastruct/d2/pull/1955) - Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218) - Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221) +- CLI: PNG output to stdout is supported using `--stdout-format png -` [#2260](https://github.com/terrastruct/d2/pull/2260) #### Improvements 🧹 diff --git a/d2cli/export.go b/d2cli/export.go index 95eee59abb..dd3e5c54df 100644 --- a/d2cli/export.go +++ b/d2cli/export.go @@ -3,6 +3,7 @@ package d2cli import ( "fmt" "path/filepath" + "strings" ) type exportExtension string @@ -15,20 +16,20 @@ const SVG exportExtension = ".svg" var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF} -func getOutputFormat(formatFlag *string, outputPath string) (exportExtension, error) { - var formatMap = map[string]exportExtension{ - "png": PNG, - "svg": SVG, - "pdf": PDF, - "pptx": PPTX, - "gif": GIF, - } +var STDOUT_FORMAT_MAP = map[string]exportExtension{ + "png": PNG, + "svg": SVG, +} + +var SUPPORTED_STDOUT_FORMATS = []string{"png", "svg"} - if *formatFlag != "" { - if format, ok := formatMap[*formatFlag]; ok { - return format, nil +func getOutputFormat(stdoutFormatFlag *string, outputPath string) (exportExtension, error) { + if *stdoutFormatFlag != "" { + format := strings.ToLower(*stdoutFormatFlag) + if ext, ok := STDOUT_FORMAT_MAP[format]; ok { + return ext, nil } - return "", fmt.Errorf("unsupported format: %s", *formatFlag) + return "", fmt.Errorf("%s is not a supported format. Supported formats are: %s", *stdoutFormatFlag, SUPPORTED_STDOUT_FORMATS) } return getExportExtension(outputPath), nil } diff --git a/d2cli/export_test.go b/d2cli/export_test.go index cd8df49a08..6022c65d07 100644 --- a/d2cli/export_test.go +++ b/d2cli/export_test.go @@ -8,7 +8,7 @@ import ( func TestOutputFormat(t *testing.T) { type testCase struct { - formatFlag string + stdoutFormatFlag string outputPath string extension exportExtension supportsDarkTheme bool @@ -43,7 +43,7 @@ func TestOutputFormat(t *testing.T) { requiresPngRender: false, }, { - formatFlag: "png", + stdoutFormatFlag: "png", outputPath: "-", extension: PNG, supportsDarkTheme: false, @@ -88,7 +88,7 @@ func TestOutputFormat(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.outputPath, func(t *testing.T) { - extension, err := getOutputFormat(&tc.formatFlag, tc.outputPath) + extension, err := getOutputFormat(&tc.stdoutFormatFlag, tc.outputPath) assert.NoError(t, err) assert.Equal(t, tc.extension, extension) assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation()) diff --git a/d2cli/main.go b/d2cli/main.go index e292efa7a1..6d83ac35f5 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -103,7 +103,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if err != nil { return err } - formatFlag := ms.Opts.String("", "format", "f", "", "stdout output format (svg, png)") + stdoutFormatFlag := ms.Opts.String("", "stdout-format", "", "", "output format when writing to stdout (svg, png). Usage: d2 input.d2 --stdout-format png - > output.png") if err != nil { return err } @@ -219,7 +219,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?") } - outputFormat, err := getOutputFormat(formatFlag, outputPath) + outputFormat, err := getOutputFormat(stdoutFormatFlag, outputPath) if err != nil { return xmain.UsageErrorf("%v", err) } From 5c43324f5596d5742a09cbbf3364da32dd5075b0 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 27 Dec 2024 16:47:41 -0700 Subject: [PATCH 03/36] docs: update lib examples with proper context logging --- docs/examples/lib/1-d2lib/d2lib.go | 8 +++++--- docs/examples/lib/2-d2oracle/d2oracle.go | 4 +++- docs/examples/lib/3-lowlevel/lowlevel.go | 10 ++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/examples/lib/1-d2lib/d2lib.go b/docs/examples/lib/1-d2lib/d2lib.go index 58f2ddc404..d75eedc32e 100644 --- a/docs/examples/lib/1-d2lib/d2lib.go +++ b/docs/examples/lib/1-d2lib/d2lib.go @@ -2,7 +2,7 @@ package main import ( "context" - "io/ioutil" + "os" "path/filepath" "oss.terrastruct.com/d2/d2graph" @@ -10,6 +10,7 @@ import ( "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2themes/d2themescatalog" + "oss.terrastruct.com/d2/lib/log" "oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/util-go/go2" ) @@ -28,7 +29,8 @@ func main() { LayoutResolver: layoutResolver, Ruler: ruler, } - diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", compileOpts, renderOpts) + ctx := log.WithDefault(context.Background()) + diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts) out, _ := d2svg.Render(diagram, renderOpts) - _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) + _ = os.WriteFile(filepath.Join("out.svg"), out, 0600) } diff --git a/docs/examples/lib/2-d2oracle/d2oracle.go b/docs/examples/lib/2-d2oracle/d2oracle.go index b59dca1fe6..067b5b4b53 100644 --- a/docs/examples/lib/2-d2oracle/d2oracle.go +++ b/docs/examples/lib/2-d2oracle/d2oracle.go @@ -9,6 +9,7 @@ import ( "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2oracle" + "oss.terrastruct.com/d2/lib/log" "oss.terrastruct.com/d2/lib/textmeasure" ) @@ -23,7 +24,8 @@ func main() { LayoutResolver: layoutResolver, Ruler: ruler, } - _, graph, _ := d2lib.Compile(context.Background(), "x -> y", compileOpts, nil) + ctx := log.WithDefault(context.Background()) + _, graph, _ := d2lib.Compile(ctx, "x -> y", compileOpts, nil) // Create a shape with the ID, "meow" graph, _, _ = d2oracle.Create(graph, nil, "meow") diff --git a/docs/examples/lib/3-lowlevel/lowlevel.go b/docs/examples/lib/3-lowlevel/lowlevel.go index 53f0468e77..89b10d13d4 100644 --- a/docs/examples/lib/3-lowlevel/lowlevel.go +++ b/docs/examples/lib/3-lowlevel/lowlevel.go @@ -2,7 +2,7 @@ package main import ( "context" - "io/ioutil" + "os" "path/filepath" "strings" @@ -11,6 +11,7 @@ import ( "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2themes/d2themescatalog" + "oss.terrastruct.com/d2/lib/log" "oss.terrastruct.com/d2/lib/textmeasure" ) @@ -20,11 +21,12 @@ func main() { graph.ApplyTheme(d2themescatalog.NeutralDefault.ID) ruler, _ := textmeasure.NewRuler() _ = graph.SetDimensions(nil, ruler, nil) - _ = d2dagrelayout.Layout(context.Background(), graph, nil) - diagram, _ := d2exporter.Export(context.Background(), graph, nil) + ctx := log.WithDefault(context.Background()) + _ = d2dagrelayout.Layout(ctx, graph, nil) + diagram, _ := d2exporter.Export(ctx, graph, nil) diagram.Config = config out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ ThemeID: &d2themescatalog.NeutralDefault.ID, }) - _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) + _ = os.WriteFile(filepath.Join("out.svg"), out, 0600) } From 674257a1c9adde0a50cabe776907fabad2385d32 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 28 Dec 2024 20:35:26 -0700 Subject: [PATCH 04/36] d2sequence: fix notes with self messages --- d2layouts/d2sequence/sequence_diagram.go | 7 +- .../txtar/note-overlap/dagre/board.exp.json | 481 ++++++++++++++++++ .../txtar/note-overlap/dagre/sketch.exp.svg | 108 ++++ .../txtar/note-overlap/elk/board.exp.json | 481 ++++++++++++++++++ .../txtar/note-overlap/elk/sketch.exp.svg | 108 ++++ e2etests/txtar.txt | 9 + 6 files changed, 1193 insertions(+), 1 deletion(-) create mode 100644 e2etests/testdata/txtar/note-overlap/dagre/board.exp.json create mode 100644 e2etests/testdata/txtar/note-overlap/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/txtar/note-overlap/elk/board.exp.json create mode 100644 e2etests/testdata/txtar/note-overlap/elk/sketch.exp.svg diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go index 69eb199854..8943bfce6d 100644 --- a/d2layouts/d2sequence/sequence_diagram.go +++ b/d2layouts/d2sequence/sequence_diagram.go @@ -466,7 +466,12 @@ func (sd *sequenceDiagram) placeNotes() { for _, msg := range sd.messages { if sd.verticalIndices[msg.AbsID()] < verticalIndex { - y += sd.yStep + float64(msg.LabelDimensions.Height) + if msg.Src == msg.Dst { + // For self-messages, account for the full vertical space they occupy + y += sd.yStep + math.Max(float64(msg.LabelDimensions.Height), MIN_MESSAGE_DISTANCE)*1.5 + } else { + y += sd.yStep + float64(msg.LabelDimensions.Height) + } } } for _, otherNote := range sd.notes { diff --git a/e2etests/testdata/txtar/note-overlap/dagre/board.exp.json b/e2etests/testdata/txtar/note-overlap/dagre/board.exp.json new file mode 100644 index 0000000000..62cfc035ac --- /dev/null +++ b/e2etests/testdata/txtar/note-overlap/dagre/board.exp.json @@ -0,0 +1,481 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "alice", + "type": "rectangle", + "pos": { + "x": 12, + "y": 52 + }, + "width": 100, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "alice", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 32, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "bob", + "type": "rectangle", + "pos": { + "x": 162, + "y": 52 + }, + "width": 100, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "bob", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 26, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "bob.\"In the eyes of my dog, I'm a man.\"", + "type": "page", + "pos": { + "x": 81, + "y": 718 + }, + "width": 261, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "In the eyes of my dog, I'm a man.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 216, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 5, + "level": 2 + } + ], + "connections": [ + { + "id": "(alice -> bob)[0]", + "src": "alice", + "srcArrow": "none", + "dst": "bob", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 188 + }, + { + "x": 212, + "y": 188 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[0]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 258 + }, + { + "x": 142, + "y": 258 + }, + { + "x": 142, + "y": 303 + }, + { + "x": 62, + "y": 303 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[1]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 373 + }, + { + "x": 142, + "y": 373 + }, + { + "x": 142, + "y": 418 + }, + { + "x": 62, + "y": 418 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[2]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 488 + }, + { + "x": 142, + "y": 488 + }, + { + "x": 142, + "y": 533 + }, + { + "x": 62, + "y": 533 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[3]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 603 + }, + { + "x": 142, + "y": 603 + }, + { + "x": 142, + "y": 648 + }, + { + "x": 62, + "y": 648 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -- )[0]", + "src": "alice", + "srcArrow": "none", + "dst": "alice-lifeline-end-3851299086", + "dstArrow": "none", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "B2", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 118 + }, + { + "x": 62, + "y": 854 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(bob -- )[0]", + "src": "bob", + "srcArrow": "none", + "dst": "bob-lifeline-end-3036726343", + "dstArrow": "none", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "B2", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 212, + "y": 118 + }, + { + "x": 212, + "y": 854 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/note-overlap/dagre/sketch.exp.svg b/e2etests/testdata/txtar/note-overlap/dagre/sketch.exp.svg new file mode 100644 index 0000000000..306fa47306 --- /dev/null +++ b/e2etests/testdata/txtar/note-overlap/dagre/sketch.exp.svg @@ -0,0 +1,108 @@ +alicebob Self-messagesSelf-messagesSelf-messagesSelf-messagesIn the eyes of my dog, I'm a man. + + + + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/txtar/note-overlap/elk/board.exp.json b/e2etests/testdata/txtar/note-overlap/elk/board.exp.json new file mode 100644 index 0000000000..62cfc035ac --- /dev/null +++ b/e2etests/testdata/txtar/note-overlap/elk/board.exp.json @@ -0,0 +1,481 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "alice", + "type": "rectangle", + "pos": { + "x": 12, + "y": 52 + }, + "width": 100, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "alice", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 32, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "bob", + "type": "rectangle", + "pos": { + "x": 162, + "y": 52 + }, + "width": 100, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "bob", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 26, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "bob.\"In the eyes of my dog, I'm a man.\"", + "type": "page", + "pos": { + "x": 81, + "y": 718 + }, + "width": 261, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "In the eyes of my dog, I'm a man.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 216, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 5, + "level": 2 + } + ], + "connections": [ + { + "id": "(alice -> bob)[0]", + "src": "alice", + "srcArrow": "none", + "dst": "bob", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 188 + }, + { + "x": 212, + "y": 188 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[0]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 258 + }, + { + "x": 142, + "y": 258 + }, + { + "x": 142, + "y": 303 + }, + { + "x": 62, + "y": 303 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[1]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 373 + }, + { + "x": 142, + "y": 373 + }, + { + "x": 142, + "y": 418 + }, + { + "x": 62, + "y": 418 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[2]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 488 + }, + { + "x": 142, + "y": 488 + }, + { + "x": 142, + "y": 533 + }, + { + "x": 62, + "y": 533 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -> alice)[3]", + "src": "alice", + "srcArrow": "none", + "dst": "alice", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "Self-messages", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 95, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 603 + }, + { + "x": 142, + "y": 603 + }, + { + "x": 142, + "y": 648 + }, + { + "x": 62, + "y": 648 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(alice -- )[0]", + "src": "alice", + "srcArrow": "none", + "dst": "alice-lifeline-end-3851299086", + "dstArrow": "none", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "B2", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 62, + "y": 118 + }, + { + "x": 62, + "y": 854 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(bob -- )[0]", + "src": "bob", + "srcArrow": "none", + "dst": "bob-lifeline-end-3036726343", + "dstArrow": "none", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "B2", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 212, + "y": 118 + }, + { + "x": 212, + "y": 854 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/note-overlap/elk/sketch.exp.svg b/e2etests/testdata/txtar/note-overlap/elk/sketch.exp.svg new file mode 100644 index 0000000000..306fa47306 --- /dev/null +++ b/e2etests/testdata/txtar/note-overlap/elk/sketch.exp.svg @@ -0,0 +1,108 @@ +alicebob Self-messagesSelf-messagesSelf-messagesSelf-messagesIn the eyes of my dog, I'm a man. + + + + + + + + + \ No newline at end of file diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt index 004b0eae7e..a93f6518d1 100644 --- a/e2etests/txtar.txt +++ b/e2etests/txtar.txt @@ -738,3 +738,12 @@ logs: {shape: page; style.multiple: true} network.data processor -> api server +-- note-overlap -- +shape: sequence_diagram +alice -> bob +alice -> alice: "Self-messages" +alice -> alice: "Self-messages" +alice -> alice: "Self-messages" +alice -> alice: "Self-messages" + +bob."In the eyes of my dog, I'm a man." From 7a3d54ba310d33c17b0a9c250317c7a0b9dc8556 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 28 Dec 2024 20:38:12 -0700 Subject: [PATCH 05/36] ta --- .../diagram_wider_than_tooltip/sketch.exp.svg | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg b/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg index 5f20e45e55..607ec2fbbb 100644 --- a/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg +++ b/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg @@ -1,19 +1,19 @@ -customerissuerstoreLike starbucks or somethingacquirerI'm not sure what this isnetworkcustomer bankstore bankinitial transactionpayment processor behind the scenessimplified 1 banana please$10 dollarsthinking: wow, inflationchecks bank accountSavings: $11I can do that, here's my cardRun this cardProcess to card issuerProcess this payment$10 debit$10 creditAn error in judgement is about to occurLike starbucks or something1I'm not sure what this is2 + .d2-4183732618 .fill-N1{fill:#0A0F25;} + .d2-4183732618 .fill-N2{fill:#676C7E;} + .d2-4183732618 .fill-N3{fill:#9499AB;} + .d2-4183732618 .fill-N4{fill:#CFD2DD;} + .d2-4183732618 .fill-N5{fill:#DEE1EB;} + .d2-4183732618 .fill-N6{fill:#EEF1F8;} + .d2-4183732618 .fill-N7{fill:#FFFFFF;} + .d2-4183732618 .fill-B1{fill:#0D32B2;} + .d2-4183732618 .fill-B2{fill:#0D32B2;} + .d2-4183732618 .fill-B3{fill:#E3E9FD;} + .d2-4183732618 .fill-B4{fill:#E3E9FD;} + .d2-4183732618 .fill-B5{fill:#EDF0FD;} + .d2-4183732618 .fill-B6{fill:#F7F8FE;} + .d2-4183732618 .fill-AA2{fill:#4A6FF3;} + .d2-4183732618 .fill-AA4{fill:#EDF0FD;} + .d2-4183732618 .fill-AA5{fill:#F7F8FE;} + .d2-4183732618 .fill-AB4{fill:#EDF0FD;} + .d2-4183732618 .fill-AB5{fill:#F7F8FE;} + .d2-4183732618 .stroke-N1{stroke:#0A0F25;} + .d2-4183732618 .stroke-N2{stroke:#676C7E;} + .d2-4183732618 .stroke-N3{stroke:#9499AB;} + .d2-4183732618 .stroke-N4{stroke:#CFD2DD;} + .d2-4183732618 .stroke-N5{stroke:#DEE1EB;} + .d2-4183732618 .stroke-N6{stroke:#EEF1F8;} + .d2-4183732618 .stroke-N7{stroke:#FFFFFF;} + .d2-4183732618 .stroke-B1{stroke:#0D32B2;} + .d2-4183732618 .stroke-B2{stroke:#0D32B2;} + .d2-4183732618 .stroke-B3{stroke:#E3E9FD;} + .d2-4183732618 .stroke-B4{stroke:#E3E9FD;} + .d2-4183732618 .stroke-B5{stroke:#EDF0FD;} + .d2-4183732618 .stroke-B6{stroke:#F7F8FE;} + .d2-4183732618 .stroke-AA2{stroke:#4A6FF3;} + .d2-4183732618 .stroke-AA4{stroke:#EDF0FD;} + .d2-4183732618 .stroke-AA5{stroke:#F7F8FE;} + .d2-4183732618 .stroke-AB4{stroke:#EDF0FD;} + .d2-4183732618 .stroke-AB5{stroke:#F7F8FE;} + .d2-4183732618 .background-color-N1{background-color:#0A0F25;} + .d2-4183732618 .background-color-N2{background-color:#676C7E;} + .d2-4183732618 .background-color-N3{background-color:#9499AB;} + .d2-4183732618 .background-color-N4{background-color:#CFD2DD;} + .d2-4183732618 .background-color-N5{background-color:#DEE1EB;} + .d2-4183732618 .background-color-N6{background-color:#EEF1F8;} + .d2-4183732618 .background-color-N7{background-color:#FFFFFF;} + .d2-4183732618 .background-color-B1{background-color:#0D32B2;} + .d2-4183732618 .background-color-B2{background-color:#0D32B2;} + .d2-4183732618 .background-color-B3{background-color:#E3E9FD;} + .d2-4183732618 .background-color-B4{background-color:#E3E9FD;} + .d2-4183732618 .background-color-B5{background-color:#EDF0FD;} + .d2-4183732618 .background-color-B6{background-color:#F7F8FE;} + .d2-4183732618 .background-color-AA2{background-color:#4A6FF3;} + .d2-4183732618 .background-color-AA4{background-color:#EDF0FD;} + .d2-4183732618 .background-color-AA5{background-color:#F7F8FE;} + .d2-4183732618 .background-color-AB4{background-color:#EDF0FD;} + .d2-4183732618 .background-color-AB5{background-color:#F7F8FE;} + .d2-4183732618 .color-N1{color:#0A0F25;} + .d2-4183732618 .color-N2{color:#676C7E;} + .d2-4183732618 .color-N3{color:#9499AB;} + .d2-4183732618 .color-N4{color:#CFD2DD;} + .d2-4183732618 .color-N5{color:#DEE1EB;} + .d2-4183732618 .color-N6{color:#EEF1F8;} + .d2-4183732618 .color-N7{color:#FFFFFF;} + .d2-4183732618 .color-B1{color:#0D32B2;} + .d2-4183732618 .color-B2{color:#0D32B2;} + .d2-4183732618 .color-B3{color:#E3E9FD;} + .d2-4183732618 .color-B4{color:#E3E9FD;} + .d2-4183732618 .color-B5{color:#EDF0FD;} + .d2-4183732618 .color-B6{color:#F7F8FE;} + .d2-4183732618 .color-AA2{color:#4A6FF3;} + .d2-4183732618 .color-AA4{color:#EDF0FD;} + .d2-4183732618 .color-AA5{color:#F7F8FE;} + .d2-4183732618 .color-AB4{color:#EDF0FD;} + .d2-4183732618 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>customerissuerstoreLike starbucks or somethingacquirerI'm not sure what this isnetworkcustomer bankstore bankinitial transactionpayment processor behind the scenessimplified 1 banana please$10 dollarsthinking: wow, inflationchecks bank accountSavings: $11I can do that, here's my cardRun this cardProcess to card issuerProcess this payment$10 debit$10 creditAn error in judgement is about to occurLike starbucks or something1I'm not sure what this is2 @@ -122,7 +122,7 @@ - + 1Like starbucks or something 2I'm not sure what this is windowroofgarage - - - - -blindsglass + 90.000000%, 100% { + opacity: 0; + } +}@keyframes d2Transition-d2-2913974970-9 { + 0%, 89.990000% { + opacity: 0; + } + 90.000000%, 100.000000% { + opacity: 1; + } +}]]>Multi-layer diagram of a home.windowroofgarage + + + + + +blindsglass -shinglesstarlinkutility hookup +shinglesstarlinkutility hookup -toolsvehicles +toolsvehicles -find contractorscraigslistfacebook - - - - -find contractorssolicit quotescraigslistfacebook - - - - - -find contractorssolicit quotesobtain quotesnegotiatecraigslistfacebook - - - - - - - -find contractorssolicit quotesobtain quotesnegotiatebook the best bidcraigslistfacebook - - - - - - - - -windowroofgaragewaterrainthunder - - - - - - - +How to repair a home. + + +How to repair a home.find contractorscraigslistfacebook + + + + + +How to repair a home.find contractorssolicit quotescraigslistfacebook + + + + + + +How to repair a home.find contractorssolicit quotesobtain quotesnegotiatecraigslistfacebook + + + + + + + + +How to repair a home.find contractorssolicit quotesobtain quotesnegotiatebook the best bidcraigslistfacebook + + + + + + + + + +Multi-layer diagram of a home.windowroofgaragewaterrainthunder + + + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/complex-layers/elk/board.exp.json b/e2etests/testdata/stable/complex-layers/elk/board.exp.json index d8da04f901..59583bc63a 100644 --- a/e2etests/testdata/stable/complex-layers/elk/board.exp.json +++ b/e2etests/testdata/stable/complex-layers/elk/board.exp.json @@ -4,12 +4,54 @@ "fontFamily": "SourceSansPro", "shapes": [ { - "id": "window", + "id": "desc", "type": "rectangle", "pos": { "x": 12, "y": 12 }, + "width": 261, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Multi-layer diagram of a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 216, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "window", + "type": "rectangle", + "pos": { + "x": 293, + "y": 12 + }, "width": 103, "height": 66, "opacity": 1, @@ -49,7 +91,7 @@ "id": "roof", "type": "rectangle", "pos": { - "x": 135, + "x": 416, "y": 12 }, "width": 75, @@ -91,7 +133,7 @@ "id": "garage", "type": "rectangle", "pos": { - "x": 230, + "x": 511, "y": 12 }, "width": 94, @@ -616,9 +658,52 @@ }, { "name": "repair", - "isFolderOnly": true, + "isFolderOnly": false, "fontFamily": "SourceSansPro", - "shapes": [], + "shapes": [ + { + "id": "desc", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 200, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "How to repair a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 155, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], "connections": [], "root": { "id": "", @@ -668,10 +753,52 @@ "fontFamily": "SourceSansPro", "shapes": [ { - "id": "find contractors", + "id": "desc", "type": "rectangle", "pos": { "x": 12, + "y": 62 + }, + "width": 200, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "How to repair a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 155, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "find contractors", + "type": "rectangle", + "pos": { + "x": 232, "y": 12 }, "width": 341, @@ -713,7 +840,7 @@ "id": "find contractors.craigslist", "type": "rectangle", "pos": { - "x": 62, + "x": 282, "y": 62 }, "width": 110, @@ -755,7 +882,7 @@ "id": "find contractors.facebook", "type": "rectangle", "pos": { - "x": 192, + "x": 412, "y": 62 }, "width": 111, @@ -843,10 +970,52 @@ "fontFamily": "SourceSansPro", "shapes": [ { - "id": "find contractors", + "id": "desc", "type": "rectangle", "pos": { "x": 12, + "y": 62 + }, + "width": 200, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "How to repair a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 155, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "find contractors", + "type": "rectangle", + "pos": { + "x": 232, "y": 12 }, "width": 341, @@ -888,7 +1057,7 @@ "id": "find contractors.craigslist", "type": "rectangle", "pos": { - "x": 62, + "x": 282, "y": 62 }, "width": 110, @@ -930,7 +1099,7 @@ "id": "find contractors.facebook", "type": "rectangle", "pos": { - "x": 192, + "x": 412, "y": 62 }, "width": 111, @@ -972,7 +1141,7 @@ "id": "solicit quotes", "type": "rectangle", "pos": { - "x": 112, + "x": 332, "y": 248 }, "width": 140, @@ -1038,11 +1207,11 @@ "link": "", "route": [ { - "x": 182.5, + "x": 402.5, "y": 178 }, { - "x": 182.5, + "x": 402.5, "y": 248 } ], @@ -1100,10 +1269,52 @@ "fontFamily": "SourceSansPro", "shapes": [ { - "id": "find contractors", + "id": "desc", "type": "rectangle", "pos": { "x": 12, + "y": 62 + }, + "width": 200, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "How to repair a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 155, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "find contractors", + "type": "rectangle", + "pos": { + "x": 232, "y": 12 }, "width": 341, @@ -1145,7 +1356,7 @@ "id": "find contractors.craigslist", "type": "rectangle", "pos": { - "x": 62, + "x": 282, "y": 62 }, "width": 110, @@ -1187,7 +1398,7 @@ "id": "find contractors.facebook", "type": "rectangle", "pos": { - "x": 192, + "x": 412, "y": 62 }, "width": 111, @@ -1229,7 +1440,7 @@ "id": "solicit quotes", "type": "rectangle", "pos": { - "x": 112, + "x": 332, "y": 248 }, "width": 140, @@ -1271,7 +1482,7 @@ "id": "obtain quotes", "type": "rectangle", "pos": { - "x": 373, + "x": 593, "y": 112 }, "width": 143, @@ -1313,7 +1524,7 @@ "id": "negotiate", "type": "rectangle", "pos": { - "x": 388, + "x": 608, "y": 248 }, "width": 112, @@ -1379,11 +1590,11 @@ "link": "", "route": [ { - "x": 182.5, + "x": 402.5, "y": 178 }, { - "x": 182.5, + "x": 402.5, "y": 248 } ], @@ -1418,11 +1629,11 @@ "link": "", "route": [ { - "x": 444.5, + "x": 664.5, "y": 178 }, { - "x": 444.5, + "x": 664.5, "y": 248 } ], @@ -1480,10 +1691,52 @@ "fontFamily": "SourceSansPro", "shapes": [ { - "id": "find contractors", + "id": "desc", "type": "rectangle", "pos": { "x": 12, + "y": 62 + }, + "width": 200, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "How to repair a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 155, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "find contractors", + "type": "rectangle", + "pos": { + "x": 232, "y": 12 }, "width": 341, @@ -1525,7 +1778,7 @@ "id": "find contractors.craigslist", "type": "rectangle", "pos": { - "x": 62, + "x": 282, "y": 62 }, "width": 110, @@ -1567,7 +1820,7 @@ "id": "find contractors.facebook", "type": "rectangle", "pos": { - "x": 192, + "x": 412, "y": 62 }, "width": 111, @@ -1609,7 +1862,7 @@ "id": "solicit quotes", "type": "rectangle", "pos": { - "x": 112, + "x": 332, "y": 248 }, "width": 140, @@ -1651,7 +1904,7 @@ "id": "obtain quotes", "type": "rectangle", "pos": { - "x": 373, + "x": 593, "y": 112 }, "width": 143, @@ -1693,7 +1946,7 @@ "id": "negotiate", "type": "rectangle", "pos": { - "x": 388, + "x": 608, "y": 248 }, "width": 112, @@ -1735,7 +1988,7 @@ "id": "book the best bid", "type": "rectangle", "pos": { - "x": 361, + "x": 581, "y": 384 }, "width": 167, @@ -1801,11 +2054,11 @@ "link": "", "route": [ { - "x": 182.5, + "x": 402.5, "y": 178 }, { - "x": 182.5, + "x": 402.5, "y": 248 } ], @@ -1840,11 +2093,11 @@ "link": "", "route": [ { - "x": 444.5, + "x": 664.5, "y": 178 }, { - "x": 444.5, + "x": 664.5, "y": 248 } ], @@ -1879,11 +2132,11 @@ "link": "", "route": [ { - "x": 444.5, + "x": 664.5, "y": 314 }, { - "x": 444.5, + "x": 664.5, "y": 384 } ], @@ -1945,12 +2198,54 @@ "fontFamily": "SourceSansPro", "shapes": [ { - "id": "window", + "id": "desc", "type": "rectangle", "pos": { "x": 12, "y": 12 }, + "width": 261, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Multi-layer diagram of a home.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 216, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "window", + "type": "rectangle", + "pos": { + "x": 293, + "y": 12 + }, "width": 103, "height": 66, "opacity": 1, @@ -1990,7 +2285,7 @@ "id": "roof", "type": "rectangle", "pos": { - "x": 135, + "x": 416, "y": 12 }, "width": 75, @@ -2032,7 +2327,7 @@ "id": "garage", "type": "rectangle", "pos": { - "x": 230, + "x": 511, "y": 12 }, "width": 94, @@ -2074,7 +2369,7 @@ "id": "water", "type": "rectangle", "pos": { - "x": 344, + "x": 625, "y": 12 }, "width": 88, @@ -2116,7 +2411,7 @@ "id": "rain", "type": "rectangle", "pos": { - "x": 452, + "x": 733, "y": 12 }, "width": 73, @@ -2158,7 +2453,7 @@ "id": "thunder", "type": "rectangle", "pos": { - "x": 545, + "x": 826, "y": 12 }, "width": 103, diff --git a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg index 0fb3f8fadd..4f3a0e4956 100644 --- a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg +++ b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg @@ -1,17 +1,17 @@ -windowroofgarage - - - - -blindsglass + 90.000000%, 100% { + opacity: 0; + } +}@keyframes d2Transition-d2-469555219-9 { + 0%, 89.990000% { + opacity: 0; + } + 90.000000%, 100.000000% { + opacity: 1; + } +}]]>Multi-layer diagram of a home.windowroofgarage + + + + + +blindsglass -shinglesstarlinkutility hookup +shinglesstarlinkutility hookup -toolsvehicles +toolsvehicles -find contractorscraigslistfacebook - - - - -find contractorssolicit quotescraigslistfacebook - - - - - -find contractorssolicit quotesobtain quotesnegotiatecraigslistfacebook - - - - - - - -find contractorssolicit quotesobtain quotesnegotiatebook the best bidcraigslistfacebook - - - - - - - - -windowroofgaragewaterrainthunder - - - - - - - +How to repair a home. + + +How to repair a home.find contractorscraigslistfacebook + + + + + +How to repair a home.find contractorssolicit quotescraigslistfacebook + + + + + + +How to repair a home.find contractorssolicit quotesobtain quotesnegotiatecraigslistfacebook + + + + + + + + +How to repair a home.find contractorssolicit quotesobtain quotesnegotiatebook the best bidcraigslistfacebook + + + + + + + + + +Multi-layer diagram of a home.windowroofgaragewaterrainthunder + + + + + + + + \ No newline at end of file From 081dd1b8dab88ea7e769d0b1dbb5d87501154f8e Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 3 Apr 2023 13:40:40 -0700 Subject: [PATCH 09/36] 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 cd56cbea106b86629d3076ccbd6ed3941440a4f1 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 11 Jun 2023 16:39:19 -0700 Subject: [PATCH 10/36] 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 7840eab037baf9dce8fed16b21d0bc0771dbcd81 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Wed, 14 Jun 2023 14:57:43 -0700 Subject: [PATCH 11/36] 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 b43fecf5ad6d94f5128181ee1b3022af80aecb57 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 15 Jun 2023 00:15:43 -0700 Subject: [PATCH 12/36] 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 cdf771a6c9c712245992ae7cbf5ebdd77886204e Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 22 Jul 2023 10:03:26 -0700 Subject: [PATCH 13/36] 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 6277d6db6332f9e851c5d54892b03223fe00762b Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 4 Aug 2023 10:54:33 -0700 Subject: [PATCH 14/36] 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 084a62ce85387056d37a319cedcef85c9a495362 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 25 Sep 2023 11:29:33 -0700 Subject: [PATCH 15/36] 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 21afb4861d46f9b8741d226bfd0dcaf10b923537 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 2 Oct 2023 14:57:37 -0700 Subject: [PATCH 16/36] 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 7dea1db40bbd168ae51e496163b1699b38bd072d Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 20 Jan 2024 11:13:34 -0800 Subject: [PATCH 17/36] 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 b3e79f7aa2fa377641925869f36a05566e40d150 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Wed, 4 Sep 2024 12:20:16 -0600 Subject: [PATCH 18/36] 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 e75b2e9b92893512cdf059624a338f106c83e1ef Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 10 Oct 2024 19:29:39 -0700 Subject: [PATCH 19/36] 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 2e6234aa0f850565bac97f44af6efe5f03487211 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 11 Oct 2024 13:18:22 -0700 Subject: [PATCH 20/36] 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 63ddaebd7a0c4b936b56a41b69de363534a93e58 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 11 Oct 2024 17:49:16 -0700 Subject: [PATCH 21/36] 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 84edf7e27df8ce1a8017500299eabfd07ece2da4 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 12 Oct 2024 12:42:41 -0700 Subject: [PATCH 22/36] 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 6656679335b423c76a378c5dd3d25a8b83c34868 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 17 Oct 2024 09:04:11 -0600 Subject: [PATCH 23/36] 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 5c40b01bedbbcc626c8ba2030230316cb4153284 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sat, 19 Oct 2024 00:09:04 -0600 Subject: [PATCH 24/36] 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 5cb8a622681d8e0019a70463c9bec27dfbac2d61 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Wed, 13 Nov 2024 12:17:49 -0700 Subject: [PATCH 25/36] 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 ecdf977c9977ebc4a47b79e31790d5b1df6d0fad Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 12 Dec 2024 15:27:28 -0700 Subject: [PATCH 26/36] 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 3db5d791be14549a25faba4082919ef93c85f377 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 20 Dec 2024 17:08:19 -0800 Subject: [PATCH 27/36] 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 80f1354737e7dfac1b8d03ba44faeed80be39221 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 29 Dec 2024 09:50:10 -0700 Subject: [PATCH 28/36] 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 3a6617af6158ebc2f76708ac5b98c1298b644546 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 29 Dec 2024 14:19:32 -0700 Subject: [PATCH 29/36] 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 ( From 688060df5deb1fd9788659c9d5ee28eefb8ebd56 Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Mon, 16 Dec 2024 15:09:31 -0800 Subject: [PATCH 30/36] Implement --check flag for fmt --- d2cli/fmt.go | 24 +++++++++++++++++++++--- d2cli/main.go | 7 ++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/d2cli/fmt.go b/d2cli/fmt.go index a2b8371589..61daf15ea0 100644 --- a/d2cli/fmt.go +++ b/d2cli/fmt.go @@ -12,9 +12,10 @@ import ( "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/lib/log" ) -func fmtCmd(ctx context.Context, ms *xmain.State) (err error) { +func fmtCmd(ctx context.Context, ms *xmain.State, check bool) (err error) { defer xdefer.Errorf(&err, "failed to fmt") ms.Opts = xmain.NewOpts(ms.Env, ms.Opts.Flags.Args()[1:]) @@ -22,6 +23,8 @@ func fmtCmd(ctx context.Context, ms *xmain.State) (err error) { return xmain.UsageErrorf("fmt must be passed at least one file to be formatted") } + unformattedCount := 0 + for _, inputPath := range ms.Opts.Args { if inputPath != "-" { inputPath = ms.AbsPath(inputPath) @@ -43,10 +46,25 @@ func fmtCmd(ctx context.Context, ms *xmain.State) (err error) { output := []byte(d2format.Format(m)) if !bytes.Equal(output, input) { - if err := ms.WritePath(inputPath, output); err != nil { - return err + if check { + unformattedCount += 1 + log.Warn(ctx, inputPath) + } else { + if err := ms.WritePath(inputPath, output); err != nil { + return err + } } } } + + if unformattedCount > 0 { + pluralFiles := "file" + if unformattedCount > 1 { + pluralFiles = "files" + } + + return xmain.ExitErrorf(1, "found %d unformatted %s. Run d2 fmt to fix.", unformattedCount, pluralFiles) + } + return nil } diff --git a/d2cli/main.go b/d2cli/main.go index 6d83ac35f5..4f6ec54c2e 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -124,6 +124,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.") fontSemiboldFlag := ms.Opts.String("D2_FONT_SEMIBOLD", "font-semibold", "", "", "path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.") + checkFlag, err := ms.Opts.Bool("D2_CHECK", "check", "", false, "check that the specified files are formatted correctly.") + if err != nil { + return err + } + plugins, err := d2plugin.ListPlugins(ctx) if err != nil { return err @@ -158,7 +163,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { themesCmd(ctx, ms) return nil case "fmt": - return fmtCmd(ctx, ms) + return fmtCmd(ctx, ms, *checkFlag) case "version": if len(ms.Opts.Flags.Args()) > 1 { return xmain.UsageErrorf("version subcommand accepts no arguments") From 0642d5c8431d5fa961e93cec87009e61af244ac2 Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Mon, 16 Dec 2024 15:29:21 -0800 Subject: [PATCH 31/36] Add E2E test --- e2etests-cli/main_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index 432d59aff5..ff764ac5e1 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -1005,6 +1005,19 @@ layers: { assert.Equal(t, "x -> y\n", string(gotBar)) }, }, + { + name: "fmt-check", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "foo.d2", `a ---> b`) + writeFile(t, dir, "bar.d2", `x ---> y`) + err := runTestMainPersist(t, ctx, dir, env, "fmt", "--check", "foo.d2", "bar.d2") + assert.ErrorString(t, err, "failed to wait xmain test: e2etests-cli/d2: failed to fmt: exiting with code 1: found 2 unformatted files. Run d2 fmt to fix.") + gotFoo := readFile(t, dir, "foo.d2") + gotBar := readFile(t, dir, "bar.d2") + assert.Equal(t, "a ---> b", string(gotFoo)) + assert.Equal(t, "x ---> y", string(gotBar)) + }, + }, { name: "watch-regular", serial: true, From 5aab6123b7fae2d7c929066c774260d3980d2eae Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Mon, 16 Dec 2024 15:37:22 -0800 Subject: [PATCH 32/36] Update changelog --- ci/release/changelogs/next.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 7512f66481..9dff242f5e 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -5,6 +5,7 @@ - Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218) - Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221) - CLI: PNG output to stdout is supported using `--stdout-format png -` [#2260](https://github.com/terrastruct/d2/pull/2260) +- `d2 fm` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253) #### Improvements 🧹 From 5339983ac1592df4b7a370ba84e81f1c73fe0569 Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Mon, 16 Dec 2024 15:40:12 -0800 Subject: [PATCH 33/36] Fix typo --- ci/release/changelogs/next.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 9dff242f5e..fe803cce08 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -5,7 +5,7 @@ - Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218) - Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221) - CLI: PNG output to stdout is supported using `--stdout-format png -` [#2260](https://github.com/terrastruct/d2/pull/2260) -- `d2 fm` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253) +- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253) #### Improvements 🧹 From 63ebe965f8b961068fddfb6775a80e8ee73034cb Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Mon, 16 Dec 2024 15:45:31 -0800 Subject: [PATCH 34/36] Update the manpage --- ci/release/template/man/d2.1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1 index 1a5ebee02e..42cead1790 100644 --- a/ci/release/template/man/d2.1 +++ b/ci/release/template/man/d2.1 @@ -125,6 +125,9 @@ In watch mode, images used in icons are cached for subsequent compilations. This .It Fl -timeout Ar 120 The maximum number of seconds that D2 runs for before timing out and exiting. When rendering a large diagram, it is recommended to increase this value .Ns . +.It Fl -check Ar false +Check that the specified files are formatted correctly +.Ns . .It Fl h , -help Print usage information and exit .Ns . @@ -180,6 +183,8 @@ See --font-semibold flag. See --animate-interval flag. .It Ev Sy D2_TIMEOUT See --timeout flag. +.It Ev Sy D2_CHECK +See --check flag. .El .Bl -tag -width Ds .It Ev Sy DEBUG From 564cd3a9cc5f899c9bd7b6d9f9a8ca84e97811c8 Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Tue, 17 Dec 2024 09:45:50 -0800 Subject: [PATCH 35/36] Improve tests --- e2etests-cli/main_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index ff764ac5e1..3c300b3374 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -1006,11 +1006,12 @@ layers: { }, }, { - name: "fmt-check", + name: "fmt-check-unformatted", run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { writeFile(t, dir, "foo.d2", `a ---> b`) writeFile(t, dir, "bar.d2", `x ---> y`) - err := runTestMainPersist(t, ctx, dir, env, "fmt", "--check", "foo.d2", "bar.d2") + writeFile(t, dir, "baz.d2", "a -> z\n") + err := runTestMainPersist(t, ctx, dir, env, "fmt", "--check", "foo.d2", "bar.d2", "baz.d2") assert.ErrorString(t, err, "failed to wait xmain test: e2etests-cli/d2: failed to fmt: exiting with code 1: found 2 unformatted files. Run d2 fmt to fix.") gotFoo := readFile(t, dir, "foo.d2") gotBar := readFile(t, dir, "bar.d2") @@ -1018,6 +1019,15 @@ layers: { assert.Equal(t, "x ---> y", string(gotBar)) }, }, + { + name: "fmt-check-formatted", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "foo.d2", "a -> b\n") + writeFile(t, dir, "bar.d2", "x -> y\n") + err := runTestMainPersist(t, ctx, dir, env, "fmt", "--check", "foo.d2", "bar.d2") + assert.Success(t, err) + }, + }, { name: "watch-regular", serial: true, From 714c20e9dee68d4690e6bf5987818f83c28b941a Mon Sep 17 00:00:00 2001 From: Maricaya <915270549@qq.com> Date: Mon, 30 Dec 2024 17:01:39 -0600 Subject: [PATCH 36/36] feat: Update the manpage --- ci/release/template/man/d2.1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1 index 42cead1790..a3bf928c03 100644 --- a/ci/release/template/man/d2.1 +++ b/ci/release/template/man/d2.1 @@ -134,6 +134,9 @@ Print usage information and exit .It Fl v , -version Print version information and exit .Ns . +.It Fl -stdout-format Ar string +Set the output format when writing to stdout. Supported formats are: png, svg. Only used when output is set to stdout (-) +.Ns . .El .Sh SUBCOMMANDS .Bl -tag -width Fl @@ -197,6 +200,8 @@ See -h[ost] flag. See -p[ort] flag. .It Ev Sy BROWSER See --browser flag. +.It Ev Sy D2_STDOUT_FORMAT +See --stdout-format flag. .El .Sh SEE ALSO .Xr d2plugin-tala 1