From 45b396c894be46ac45eb85b08745b5959f00f34e Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 14 Jul 2023 13:08:26 -0700 Subject: [PATCH] config vars --- d2chaos/d2chaos_test.go | 2 +- d2cli/main.go | 132 ++- d2cli/watch.go | 5 +- d2compiler/compile.go | 52 +- d2compiler/compile_test.go | 167 +++- d2exporter/export_test.go | 2 +- d2graph/serde_test.go | 4 +- d2ir/compile.go | 60 ++ d2layouts/d2sequence/layout_test.go | 6 +- d2lib/d2.go | 109 ++- d2oracle/edit.go | 2 +- d2oracle/edit_test.go | 10 +- d2renderers/d2animate/d2animate.go | 10 +- d2renderers/d2sketch/sketch_test.go | 29 +- d2renderers/d2svg/appendix/appendix_test.go | 20 +- d2renderers/d2svg/d2svg.go | 33 +- .../d2svg/dark_theme/dark_theme_test.go | 22 +- d2target/d2target.go | 18 +- docs/examples/lib/1-d2lib/d2lib.go | 23 +- docs/examples/lib/2-d2oracle/d2oracle.go | 13 +- docs/examples/lib/3-lowlevel/lowlevel.go | 5 +- e2etests-cli/main_test.go | 63 ++ .../testdata/TestCLI_E2E/import_vars.exp.svg | 96 ++ .../TestCLI_E2E/internal_linked_pdf.exp.pdf | Bin 80096 -> 80081 bytes .../TestCLI_E2E/vars-animation.exp.svg | 883 ++++++++++++++++++ .../testdata/TestCLI_E2E/vars-config.exp.svg | 117 +++ e2etests/e2e_test.go | 40 +- e2etests/themes_test.go | 8 +- .../TestCompile2/vars/config/basic.exp.json | 291 ++++++ .../TestCompile2/vars/config/invalid.exp.json | 11 + .../vars/config/not-root.exp.json | 11 + 31 files changed, 2015 insertions(+), 229 deletions(-) create mode 100644 e2etests-cli/testdata/TestCLI_E2E/import_vars.exp.svg create mode 100644 e2etests-cli/testdata/TestCLI_E2E/vars-animation.exp.svg create mode 100644 e2etests-cli/testdata/TestCLI_E2E/vars-config.exp.svg create mode 100644 testdata/d2compiler/TestCompile2/vars/config/basic.exp.json create mode 100644 testdata/d2compiler/TestCompile2/vars/config/invalid.exp.json create mode 100644 testdata/d2compiler/TestCompile2/vars/config/not-root.exp.json diff --git a/d2chaos/d2chaos_test.go b/d2chaos/d2chaos_test.go index 0033cec90..e101d434d 100644 --- a/d2chaos/d2chaos_test.go +++ b/d2chaos/d2chaos_test.go @@ -102,7 +102,7 @@ func test(t *testing.T, textPath, text string) { t.Fatal(err) } - g, err := d2compiler.Compile("", strings.NewReader(text), nil) + g, _, err := d2compiler.Compile("", strings.NewReader(text), nil) if err != nil { t.Fatal(err) } diff --git a/d2cli/main.go b/d2cli/main.go index 3d4fa6054..0f0e8167b 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -20,6 +20,7 @@ import ( "oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/xmain" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2plugin" @@ -117,11 +118,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.") - ps, err := d2plugin.ListPlugins(ctx) + plugins, err := d2plugin.ListPlugins(ctx) if err != nil { return err } - err = populateLayoutOpts(ctx, ms, ps) + err = populateLayoutOpts(ctx, ms, plugins) if err != nil { return err } @@ -146,7 +147,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { case "init-playwright": return initPlaywright() case "layout": - return layoutCmd(ctx, ms, ps) + return layoutCmd(ctx, ms, plugins) case "themes": themesCmd(ctx, ms) return nil @@ -226,6 +227,38 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { } ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag) + // If flag is not explicitly set by user, set to nil. + // Later, configs from D2 code will only overwrite if they weren't explicitly set by user + flagSet := make(map[string]struct{}) + ms.Opts.Flags.Visit(func(f *pflag.Flag) { + flagSet[f.Name] = struct{}{} + }) + if ms.Env.Getenv("D2_LAYOUT") == "" { + if _, ok := flagSet["layout"]; !ok { + layoutFlag = nil + } + } + if ms.Env.Getenv("D2_THEME") == "" { + if _, ok := flagSet["theme"]; !ok { + themeFlag = nil + } + } + if ms.Env.Getenv("D2_SKETCH") == "" { + if _, ok := flagSet["sketch"]; !ok { + sketchFlag = nil + } + } + if ms.Env.Getenv("D2_PAD") == "" { + if _, ok := flagSet["pad"]; !ok { + padFlag = nil + } + } + if ms.Env.Getenv("D2_CENTER") == "" { + if _, ok := flagSet["center"]; !ok { + centerFlag = nil + } + } + if *darkThemeFlag == -1 { darkThemeFlag = nil // TODO this is a temporary solution: https://github.com/terrastruct/util-go/issues/7 } @@ -241,29 +274,6 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { scale = scaleFlag } - plugin, err := d2plugin.FindPlugin(ctx, ps, *layoutFlag) - if err != nil { - if errors.Is(err, exec.ErrNotFound) { - return layoutNotFound(ctx, ps, *layoutFlag) - } - return err - } - - err = d2plugin.HydratePluginOpts(ctx, ms, plugin) - if err != nil { - return err - } - - pinfo, err := plugin.Info(ctx) - if err != nil { - return err - } - plocation := pinfo.Type - if pinfo.Type == "binary" { - plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path)) - } - ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation) - if !outputFormat.supportsDarkTheme() { if darkThemeFlag != nil { ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg") @@ -285,10 +295,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { } renderOpts := d2svg.RenderOpts{ - Pad: int(*padFlag), - Sketch: *sketchFlag, - Center: *centerFlag, - ThemeID: *themeFlag, + Pad: padFlag, + Sketch: sketchFlag, + Center: centerFlag, + ThemeID: themeFlag, DarkThemeID: darkThemeFlag, Scale: scale, } @@ -298,7 +308,8 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin") } w, err := newWatcher(ctx, ms, watcherOpts{ - layoutPlugin: plugin, + plugins: plugins, + layout: layoutFlag, renderOpts: renderOpts, animateInterval: *animateIntervalFlag, host: *hostFlag, @@ -319,7 +330,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, plugin, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page) + _, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page) if err != nil { if written { return fmt.Errorf("failed to fully compile (partial render written): %w", err) @@ -329,7 +340,32 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { return nil } -func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) { +func LayoutResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin) func(engine string) (d2graph.LayoutGraph, error) { + cached := make(map[string]d2graph.LayoutGraph) + return func(engine string) (d2graph.LayoutGraph, error) { + if c, ok := cached[engine]; ok { + return c, nil + } + + plugin, err := d2plugin.FindPlugin(ctx, plugins, engine) + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return nil, layoutNotFound(ctx, plugins, engine) + } + return nil, err + } + + err = d2plugin.HydratePluginOpts(ctx, ms, plugin) + if err != nil { + return nil, err + } + + cached[engine] = plugin.Layout + return plugin.Layout, nil + } +} + +func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) { start := time.Now() input, err := ms.ReadPath(inputPath) if err != nil { @@ -341,16 +377,12 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende return nil, false, err } - layout := plugin.Layout opts := &d2lib.CompileOptions{ - Layout: layout, - Ruler: ruler, - ThemeID: renderOpts.ThemeID, - FontFamily: fontFamily, - InputPath: inputPath, - } - if renderOpts.Sketch { - opts.FontFamily = go2.Pointer(d2fonts.HandDrawn) + Ruler: ruler, + FontFamily: fontFamily, + InputPath: inputPath, + LayoutResolver: LayoutResolver(ctx, ms, plugins), + Layout: layout, } cancel := background.Repeat(func() { @@ -358,12 +390,14 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende }, time.Second*5) defer cancel() - diagram, g, err := d2lib.Compile(ctx, string(input), opts) + diagram, g, err := d2lib.Compile(ctx, string(input), opts, &renderOpts) if err != nil { return nil, false, err } cancel() + plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout) + if animateInterval > 0 { masterID, err := diagram.HashID() if err != nil { @@ -372,6 +406,16 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende renderOpts.MasterID = masterID } + pinfo, err := plugin.Info(ctx) + if err != nil { + return nil, false, err + } + plocation := pinfo.Type + if pinfo.Type == "binary" { + plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path)) + } + ms.Log.Debug.Printf("using layout plugin %s (%s)", *opts.Layout, plocation) + pluginInfo, err := plugin.Info(ctx) if err != nil { return nil, false, err @@ -805,7 +849,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt if err != nil { return svg, err } - err = doc.AddPDFPage(pngImg, boardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap) + err = doc.AddPDFPage(pngImg, boardPath, *opts.ThemeID, rootFill, diagram.Shapes, *opts.Pad, viewboxX, viewboxY, pageMap) if err != nil { return svg, err } diff --git a/d2cli/watch.go b/d2cli/watch.go index bb006eb17..0fefabd61 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -41,7 +41,8 @@ var devMode = false var staticFS embed.FS type watcherOpts struct { - layoutPlugin d2plugin.Plugin + layout *string + plugins []d2plugin.Plugin renderOpts d2svg.RenderOpts animateInterval int64 host string @@ -361,7 +362,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { w.pw = newPW } - svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page) + svg, _, err := compile(ctx, w.ms, w.plugins, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page) errs := "" if err != nil { if len(svg) > 0 { diff --git a/d2compiler/compile.go b/d2compiler/compile.go index 600179d40..486e886b1 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -27,7 +27,7 @@ type CompileOptions struct { FS fs.FS } -func Compile(p string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph, error) { +func Compile(p string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph, *d2target.Config, error) { if opts == nil { opts = &CompileOptions{} } @@ -36,7 +36,7 @@ func Compile(p string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph, e UTF16: opts.UTF16, }) if err != nil { - return nil, err + return nil, nil, err } ir, err := d2ir.Compile(ast, &d2ir.CompileOptions{ @@ -44,16 +44,16 @@ func Compile(p string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph, e FS: opts.FS, }) if err != nil { - return nil, err + return nil, nil, err } g, err := compileIR(ast, ir) if err != nil { - return nil, err + return nil, nil, err } g.SortObjectsByAST() g.SortEdgesByAST() - return g, nil + return g, compileConfig(ir), nil } func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) { @@ -1285,3 +1285,45 @@ func parentSeqDiagram(n d2ir.Node) *d2ir.Map { n = m } } + +func compileConfig(ir *d2ir.Map) *d2target.Config { + f := ir.GetField("vars", "d2-config") + if f == nil || f.Map() == nil { + return nil + } + + configMap := f.Map() + + config := &d2target.Config{} + + f = configMap.GetField("sketch") + if f != nil { + val, _ := strconv.ParseBool(f.Primary().Value.ScalarString()) + config.Sketch = &val + } + + f = configMap.GetField("theme-id") + if f != nil { + val, _ := strconv.Atoi(f.Primary().Value.ScalarString()) + config.ThemeID = go2.Pointer(int64(val)) + } + + f = configMap.GetField("dark-theme-id") + if f != nil { + val, _ := strconv.Atoi(f.Primary().Value.ScalarString()) + config.DarkThemeID = go2.Pointer(int64(val)) + } + + f = configMap.GetField("pad") + if f != nil { + val, _ := strconv.Atoi(f.Primary().Value.ScalarString()) + config.Pad = go2.Pointer(int64(val)) + } + + f = configMap.GetField("layout-engine") + if f != nil { + config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString()) + } + + return config +} diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index aee82e145..959defbf2 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -2702,7 +2702,7 @@ object: { t.Parallel() d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) if tc.expErr != "" { if err == nil { t.Fatalf("expected error with: %q", tc.expErr) @@ -2757,7 +2757,7 @@ func testBoards(t *testing.T) { { name: "root", run: func(t *testing.T) { - g := assertCompile(t, `base + g, _ := assertCompile(t, `base layers: { one: { @@ -2776,7 +2776,7 @@ layers: { { name: "recursive", run: func(t *testing.T) { - g := assertCompile(t, `base + g, _ := assertCompile(t, `base layers: { one: { @@ -2804,7 +2804,7 @@ layers: { { name: "isFolderOnly", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` layers: { one: { santa @@ -2939,7 +2939,7 @@ func testNulls(t *testing.T) { { name: "shape", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a a: null `, "") @@ -2949,7 +2949,7 @@ a: null { name: "edge", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a -> b (a -> b)[0]: null `, "") @@ -2960,7 +2960,7 @@ a -> b { name: "attribute", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a.style.opacity: 0.2 a.style.opacity: null `, "") @@ -2992,7 +2992,7 @@ a.style.opacity: null { name: "shape", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a a: null a @@ -3003,7 +3003,7 @@ a { name: "edge", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a -> b (a -> b)[0]: null a -> b @@ -3015,7 +3015,7 @@ a -> b { name: "attribute-reset", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a.style.opacity: 0.2 a: null a @@ -3027,7 +3027,7 @@ a { name: "edge-reset", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a -> b a: null a @@ -3039,7 +3039,7 @@ a { name: "children-reset", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` a.b.c a.b: null a.b @@ -3072,7 +3072,7 @@ a.b { name: "delete-connection", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` x -> y y: null `, "") @@ -3083,7 +3083,7 @@ y: null { name: "delete-multiple-connections", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` x -> y z -> y y -> a @@ -3096,7 +3096,7 @@ y: null { name: "no-delete-connection", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` y: null x -> y `, "") @@ -3107,7 +3107,7 @@ x -> y { name: "delete-children", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` x.y.z a.b.c @@ -3142,7 +3142,7 @@ a.b: null { name: "scenario", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` x scenarios: { @@ -3183,7 +3183,7 @@ func testVars(t *testing.T) { { name: "shape-label", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3196,7 +3196,7 @@ hi: ${x} { name: "style", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { primary-color: red } @@ -3211,7 +3211,7 @@ hi: { { name: "number", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { columns: 2 } @@ -3226,7 +3226,7 @@ hi: { { name: "nested", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { colors: { primary: { @@ -3244,7 +3244,7 @@ hi: { { name: "combined", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3256,7 +3256,7 @@ hi: 1 ${x} 2 { name: "double-quoted", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3268,7 +3268,7 @@ hi: "1 ${x} 2" { name: "single-quoted", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3280,7 +3280,7 @@ hi: '1 ${x} 2' { name: "edge-label", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3293,7 +3293,7 @@ a -> b: ${x} { name: "edge-map", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3308,7 +3308,7 @@ a -> b: { { name: "quoted-var", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { primaryColors: { button: { @@ -3330,7 +3330,7 @@ button: { { name: "quoted-var-quoted-sub", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: "hi" } @@ -3343,7 +3343,7 @@ y: "hey ${x}" { name: "parent-scope", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im root var } @@ -3360,7 +3360,7 @@ a: { { name: "map", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { cool-style: { fill: red @@ -3379,7 +3379,7 @@ a -> b: ${arrows} { name: "primary-and-composite", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: all { a: b @@ -3395,7 +3395,7 @@ z: ${x} { name: "spread", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: all { a: b @@ -3415,7 +3415,7 @@ z: { { name: "array", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { base-constraints: [UNQ; NOT NULL] } @@ -3431,7 +3431,7 @@ a: { { name: "spread-array", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { base-constraints: [UNQ; NOT NULL] } @@ -3447,7 +3447,7 @@ a: { { name: "sub-array", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: all } @@ -3460,7 +3460,7 @@ z.class: [a; ${x}] { name: "multi-part-array", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: all } @@ -3473,7 +3473,7 @@ z.class: [a; ${x}together] { name: "double-quote-primary", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: always { a: b @@ -3488,7 +3488,7 @@ z: "${x} be my maybe" { name: "spread-nested", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { disclaimer: { I am not a lawyer @@ -3504,7 +3504,7 @@ custom-disclaimer: DRAFT DISCLAIMER { { name: "spread-edge", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { connections: { x -> a @@ -3543,7 +3543,7 @@ hi: { { name: "label", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3557,7 +3557,7 @@ hi: not a var { name: "map", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im root var } @@ -3574,7 +3574,7 @@ a: { { name: "var-in-var", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { surname: Smith } @@ -3592,7 +3592,7 @@ a: { { name: "recursive-var", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: a } @@ -3667,7 +3667,7 @@ a: { { name: "layer", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3685,7 +3685,7 @@ layers: { { name: "layer-2", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: root var x y: root var y @@ -3710,7 +3710,7 @@ layers: { { name: "scenario", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im a var } @@ -3728,7 +3728,7 @@ scenarios: { { name: "overlay", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im x var } @@ -3763,7 +3763,7 @@ layers: { { name: "replace", run: func(t *testing.T) { - g := assertCompile(t, ` + g, _ := assertCompile(t, ` vars: { x: im x var } @@ -3795,6 +3795,71 @@ scenarios: { } }) + t.Run("config", func(t *testing.T) { + t.Parallel() + + tca := []struct { + name string + skip bool + run func(t *testing.T) + }{ + { + name: "basic", + run: func(t *testing.T) { + _, config := assertCompile(t, ` +vars: { + d2-config: { + sketch: true + } +} + +x -> y +`, "") + assert.Equal(t, true, *config.Sketch) + }, + }, + { + name: "invalid", + run: func(t *testing.T) { + assertCompile(t, ` +vars: { + d2-config: { + sketch: lol + } +} + +x -> y +`, `d2/testdata/d2compiler/TestCompile2/vars/config/invalid.d2:4:5: expected a boolean for "sketch", got "lol"`) + }, + }, + { + name: "not-root", + run: func(t *testing.T) { + assertCompile(t, ` +x: { + vars: { + d2-config: { + sketch: false + } + } +} +`, `d2/testdata/d2compiler/TestCompile2/vars/config/not-root.d2:4:4: "d2-config" can only appear at root vars`) + }, + }, + } + + for _, tc := range tca { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.skip { + t.SkipNow() + } + tc.run(t) + }) + } + }) + t.Run("errors", func(t *testing.T) { t.Parallel() @@ -3952,9 +4017,9 @@ z: { }) } -func assertCompile(t *testing.T, text string, expErr string) *d2graph.Graph { +func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) { d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil) + g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil) if expErr != "" { assert.Error(t, err) assert.ErrorString(t, err, expErr) @@ -3972,5 +4037,5 @@ func assertCompile(t *testing.T, text string, expErr string) *d2graph.Graph { err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got) assert.Success(t, err) - return g + return g, config } diff --git a/d2exporter/export_test.go b/d2exporter/export_test.go index bc6f47010..d98b214ac 100644 --- a/d2exporter/export_test.go +++ b/d2exporter/export_test.go @@ -219,7 +219,7 @@ func run(t *testing.T, tc testCase) { ctx = log.WithTB(ctx, t, nil) ctx = log.Leveled(ctx, slog.LevelDebug) - g, err := d2compiler.Compile("", strings.NewReader(tc.dsl), &d2compiler.CompileOptions{ + g, _, err := d2compiler.Compile("", strings.NewReader(tc.dsl), &d2compiler.CompileOptions{ UTF16: true, }) if err != nil { diff --git a/d2graph/serde_test.go b/d2graph/serde_test.go index 9d52b91f7..d1435a82d 100644 --- a/d2graph/serde_test.go +++ b/d2graph/serde_test.go @@ -13,7 +13,7 @@ import ( func TestSerialization(t *testing.T) { t.Parallel() - g, err := d2compiler.Compile("", strings.NewReader("a.a.b -> a.a.c"), nil) + g, _, err := d2compiler.Compile("", strings.NewReader("a.a.b -> a.a.c"), nil) assert.Nil(t, err) asserts := func(g *d2graph.Graph) { @@ -53,7 +53,7 @@ func TestCasingRegression(t *testing.T) { script := `UserCreatedTypeField` - g, err := d2compiler.Compile("", strings.NewReader(script), nil) + g, _, err := d2compiler.Compile("", strings.NewReader(script), nil) assert.Nil(t, err) _, ok := g.Root.HasChild([]string{"UserCreatedTypeField"}) diff --git a/d2ir/compile.go b/d2ir/compile.go index 31c5512eb..88dd960f0 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -2,11 +2,14 @@ package d2ir import ( "io/fs" + "strconv" "strings" "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/d2themes" + "oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/util-go/go2" ) @@ -116,6 +119,7 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) { // don't resolve substitutions in vars with the current scope of vars if f.Name == "vars" { c.compileSubstitutions(f.Map(), varsStack[1:]) + c.validateConfigs(f.Map().GetField("d2-config")) } else { c.compileSubstitutions(f.Map(), varsStack) } @@ -131,6 +135,62 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) { } } +func (c *compiler) validateConfigs(configs *Field) { + if configs == nil || configs.Map() == nil { + return + } + + if NodeBoardKind(ParentMap(ParentMap(configs))) == "" { + c.errorf(configs.LastRef().AST(), `"%s" can only appear at root vars`, configs.Name) + return + } + + for _, f := range configs.Map().Fields { + var val string + if f.Primary() == nil { + if f.Name != "theme-colors" { + c.errorf(f.LastRef().AST(), `"%s" needs a value`, f.Name) + continue + } + } else { + val = f.Primary().Value.ScalarString() + } + + switch f.Name { + case "sketch", "center": + _, err := strconv.ParseBool(val) + if err != nil { + c.errorf(f.LastRef().AST(), `expected a boolean for "%s", got "%s"`, f.Name, val) + continue + } + case "theme-colors": + if f.Map() == nil { + c.errorf(f.LastRef().AST(), `"%s" needs a map`, f.Name) + continue + } + case "theme-id", "dark-theme-id": + valInt, err := strconv.Atoi(val) + if err != nil { + c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name, val) + continue + } + if d2themescatalog.Find(int64(valInt)) == (d2themes.Theme{}) { + c.errorf(f.LastRef().AST(), `%d is not a valid theme ID`, valInt) + continue + } + case "pad": + _, err := strconv.Atoi(val) + if err != nil { + c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name, val) + continue + } + case "layout-engine": + default: + c.errorf(f.LastRef().AST(), `"%s" is not a valid config`, f.Name) + } + } +} + func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) { var subbed bool var resolvedField *Field diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index 6fe39ce9e..f538e7cb6 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -37,7 +37,7 @@ n2 -> n1: right to left n1 -> n2 n2 -> n1 ` - g, err := d2compiler.Compile("", strings.NewReader(input), nil) + g, _, err := d2compiler.Compile("", strings.NewReader(input), nil) assert.Nil(t, err) n1, has := g.Root.HasChild([]string{"n1"}) @@ -177,7 +177,7 @@ a.t2 -> b b -> a.t2` ctx := log.WithTB(context.Background(), t, nil) - g, err := d2compiler.Compile("", strings.NewReader(input), nil) + g, _, err := d2compiler.Compile("", strings.NewReader(input), nil) assert.Nil(t, err) g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} @@ -298,7 +298,7 @@ c container -> c: edge 1 ` ctx := log.WithTB(context.Background(), t, nil) - g, err := d2compiler.Compile("", strings.NewReader(input), nil) + g, _, err := d2compiler.Compile("", strings.NewReader(input), nil) assert.Nil(t, err) container, has := g.Root.HasChild([]string{"container"}) diff --git a/d2lib/d2.go b/d2lib/d2.go index d12423332..bf4902d9f 100644 --- a/d2lib/d2.go +++ b/d2lib/d2.go @@ -15,17 +15,21 @@ import ( "oss.terrastruct.com/d2/d2layouts/d2near" "oss.terrastruct.com/d2/d2layouts/d2sequence" "oss.terrastruct.com/d2/d2renderers/d2fonts" + "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/lib/textmeasure" + "oss.terrastruct.com/util-go/go2" ) type CompileOptions struct { - UTF16 bool - FS fs.FS - MeasuredTexts []*d2target.MText - Ruler *textmeasure.Ruler - Layout func(context.Context, *d2graph.Graph) error - ThemeID int64 + UTF16 bool + FS fs.FS + MeasuredTexts []*d2target.MText + Ruler *textmeasure.Ruler + LayoutResolver func(engine string) (d2graph.LayoutGraph, error) + + Layout *string // FontFamily controls the font family used for all texts that are not the following: // - code @@ -37,39 +41,42 @@ type CompileOptions struct { InputPath string } -func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) { - if opts == nil { - opts = &CompileOptions{} +func Compile(ctx context.Context, input string, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, *d2graph.Graph, error) { + if compileOpts == nil { + compileOpts = &CompileOptions{} + } + if renderOpts == nil { + renderOpts = &d2svg.RenderOpts{} } - g, err := d2compiler.Compile(opts.InputPath, strings.NewReader(input), &d2compiler.CompileOptions{ - UTF16: opts.UTF16, - FS: opts.FS, + g, config, err := d2compiler.Compile(compileOpts.InputPath, strings.NewReader(input), &d2compiler.CompileOptions{ + UTF16: compileOpts.UTF16, + FS: compileOpts.FS, }) if err != nil { return nil, nil, err } - d, err := compile(ctx, g, opts) - if err != nil { - return nil, nil, err - } - return d, g, nil + applyConfigs(config, compileOpts, renderOpts) + applyDefaults(compileOpts, renderOpts) + + d, err := compile(ctx, g, compileOpts, renderOpts) + return d, g, err } -func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) { - err := g.ApplyTheme(opts.ThemeID) +func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, error) { + err := g.ApplyTheme(*renderOpts.ThemeID) if err != nil { return nil, err } if len(g.Objects) > 0 { - err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily) + err := g.SetDimensions(compileOpts.MeasuredTexts, compileOpts.Ruler, compileOpts.FontFamily) if err != nil { return nil, err } - coreLayout, err := getLayout(opts) + coreLayout, err := getLayout(compileOpts) if err != nil { return nil, err } @@ -96,27 +103,27 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta } } - d, err := d2exporter.Export(ctx, g, opts.FontFamily) + d, err := d2exporter.Export(ctx, g, compileOpts.FontFamily) if err != nil { return nil, err } for _, l := range g.Layers { - ld, err := compile(ctx, l, opts) + ld, err := compile(ctx, l, compileOpts, renderOpts) if err != nil { return nil, err } d.Layers = append(d.Layers, ld) } for _, l := range g.Scenarios { - ld, err := compile(ctx, l, opts) + ld, err := compile(ctx, l, compileOpts, renderOpts) if err != nil { return nil, err } d.Scenarios = append(d.Scenarios, ld) } for _, l := range g.Steps { - ld, err := compile(ctx, l, opts) + ld, err := compile(ctx, l, compileOpts, renderOpts) if err != nil { return nil, err } @@ -127,7 +134,7 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) { if opts.Layout != nil { - return opts.Layout, nil + return opts.LayoutResolver(*opts.Layout) } else if os.Getenv("D2_LAYOUT") == "dagre" { defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { return d2dagrelayout.Layout(ctx, g, nil) @@ -137,3 +144,53 @@ func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) { return nil, errors.New("no available layout") } } + +// applyConfigs applies the configs read from D2 and applies it to passed in opts +// It will only write to opt fields that are nil, as passed-in opts have precedence +func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) { + if config == nil { + return + } + + if compileOpts.Layout == nil { + compileOpts.Layout = config.LayoutEngine + } + + if renderOpts.ThemeID == nil { + renderOpts.ThemeID = config.ThemeID + } + if renderOpts.DarkThemeID == nil { + renderOpts.DarkThemeID = config.DarkThemeID + } + if renderOpts.Sketch == nil { + renderOpts.Sketch = config.Sketch + } + if renderOpts.Pad == nil { + renderOpts.Pad = config.Pad + } + if renderOpts.Center == nil { + renderOpts.Center = config.Center + } +} + +func applyDefaults(compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) { + if compileOpts.Layout == nil { + compileOpts.Layout = go2.Pointer("dagre") + } + + if renderOpts.ThemeID == nil { + renderOpts.ThemeID = &d2themescatalog.NeutralDefault.ID + } + if renderOpts.Sketch == nil { + renderOpts.Sketch = go2.Pointer(false) + } + if *renderOpts.Sketch { + compileOpts.FontFamily = go2.Pointer(d2fonts.HandDrawn) + } + if renderOpts.Pad == nil { + renderOpts.Pad = go2.Pointer(int64(d2svg.DEFAULT_PADDING)) + } + if renderOpts.Center == nil { + renderOpts.Center = go2.Pointer(false) + } +} diff --git a/d2oracle/edit.go b/d2oracle/edit.go index d3231aba3..01f54d391 100644 --- a/d2oracle/edit.go +++ b/d2oracle/edit.go @@ -305,7 +305,7 @@ func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Objec func recompile(ast *d2ast.Map) (*d2graph.Graph, error) { s := d2format.Format(ast) - g, err := d2compiler.Compile(ast.Range.Path, strings.NewReader(s), nil) + g, _, err := d2compiler.Compile(ast.Range.Path, strings.NewReader(s), nil) if err != nil { return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err) } diff --git a/d2oracle/edit_test.go b/d2oracle/edit_test.go index f942fc3f9..424deec72 100644 --- a/d2oracle/edit_test.go +++ b/d2oracle/edit_test.go @@ -7002,7 +7002,7 @@ type editTest struct { func (tc editTest) run(t *testing.T) { d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) if err != nil { t.Fatal(err) } @@ -7265,7 +7265,7 @@ scenarios: { t.Parallel() d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) if err != nil { t.Fatal(err) } @@ -7725,7 +7725,7 @@ z t.Parallel() d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) if err != nil { t.Fatal(err) } @@ -8095,7 +8095,7 @@ layers: { t.Parallel() d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) if err != nil { t.Fatal(err) } @@ -8325,7 +8325,7 @@ scenarios: { t.Parallel() d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) - g, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) + g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) if err != nil { t.Fatal(err) } diff --git a/d2renderers/d2animate/d2animate.go b/d2renderers/d2animate/d2animate.go index 7fca92dd7..9eed2bf10 100644 --- a/d2renderers/d2animate/d2animate.go +++ b/d2renderers/d2animate/d2animate.go @@ -49,10 +49,10 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO // TODO account for stroke width of root border tl, br := rootDiagram.NestedBoundingBox() - left := tl.X - renderOpts.Pad - top := tl.Y - renderOpts.Pad - width := br.X - tl.X + renderOpts.Pad*2 - height := br.Y - tl.Y + renderOpts.Pad*2 + left := tl.X - int(*renderOpts.Pad) + top := tl.Y - int(*renderOpts.Pad) + width := br.X - tl.X + int(*renderOpts.Pad)*2 + height := br.Y - tl.Y + int(*renderOpts.Pad)*2 fitToScreenWrapperOpening := fmt.Sprintf(``, version.Version, @@ -93,7 +93,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO fmt.Fprintf(buf, ``, css) } - if renderOpts.Sketch { + if renderOpts.Sketch != nil && *renderOpts.Sketch { d2sketch.DefineFillPatterns(buf) } diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go index 7b494c416..e7d147223 100644 --- a/d2renderers/d2sketch/sketch_test.go +++ b/d2renderers/d2sketch/sketch_test.go @@ -17,6 +17,7 @@ import ( "oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/go2" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2elklayout" "oss.terrastruct.com/d2/d2lib" @@ -1339,16 +1340,22 @@ func run(t *testing.T, tc testCase) { return } - layout := d2dagrelayout.DefaultLayout - if strings.EqualFold(tc.engine, "elk") { - layout = d2elklayout.DefaultLayout + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { + if strings.EqualFold(engine, "elk") { + return d2elklayout.DefaultLayout, nil + } + return d2dagrelayout.DefaultLayout, nil + } + renderOpts := &d2svg.RenderOpts{ + Sketch: go2.Pointer(true), + ThemeID: go2.Pointer(tc.themeID), } diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ - Ruler: ruler, - Layout: layout, - FontFamily: go2.Pointer(d2fonts.HandDrawn), - ThemeID: tc.themeID, - }) + Ruler: ruler, + Layout: &tc.engine, + LayoutResolver: layoutResolver, + FontFamily: go2.Pointer(d2fonts.HandDrawn), + }, renderOpts) if !tassert.Nil(t, err) { return } @@ -1356,11 +1363,7 @@ func run(t *testing.T, tc testCase) { dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/")) pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") - svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: d2svg.DEFAULT_PADDING, - Sketch: true, - ThemeID: tc.themeID, - }) + svgBytes, err := d2svg.Render(diagram, renderOpts) assert.Success(t, err) err = os.MkdirAll(dataPath, 0755) assert.Success(t, err) diff --git a/d2renderers/d2svg/appendix/appendix_test.go b/d2renderers/d2svg/appendix/appendix_test.go index d9bec84a9..3a152d92b 100644 --- a/d2renderers/d2svg/appendix/appendix_test.go +++ b/d2renderers/d2svg/appendix/appendix_test.go @@ -16,6 +16,7 @@ import ( "oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/diff" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2renderers/d2svg" @@ -152,11 +153,17 @@ func run(t *testing.T, tc testCase) { return } + renderOpts := &d2svg.RenderOpts{ + ThemeID: &tc.themeID, + } + + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { + return d2dagrelayout.DefaultLayout, nil + } diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ - Ruler: ruler, - Layout: d2dagrelayout.DefaultLayout, - ThemeID: tc.themeID, - }) + Ruler: ruler, + LayoutResolver: layoutResolver, + }, renderOpts) if !tassert.Nil(t, err) { return } @@ -164,10 +171,7 @@ func run(t *testing.T, tc testCase) { dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestAppendix/")) pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") - svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: d2svg.DEFAULT_PADDING, - ThemeID: tc.themeID, - }) + svgBytes, err := d2svg.Render(diagram, renderOpts) assert.Success(t, err) svgBytes = appendix.Append(diagram, ruler, svgBytes) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 57b9f61dd..7b940fcf5 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -69,10 +69,10 @@ var grain string var paper string type RenderOpts struct { - Pad int - Sketch bool - Center bool - ThemeID int64 + Pad *int64 + Sketch *bool + Center *bool + ThemeID *int64 DarkThemeID *int64 Font string // the svg will be scaled by this factor, if unset the svg will fit to screen @@ -1689,26 +1689,28 @@ func appendOnTrigger(buf *bytes.Buffer, source string, triggers []string, newCon } } -const DEFAULT_THEME int64 = 0 - var DEFAULT_DARK_THEME *int64 = nil // no theme selected func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { var sketchRunner *d2sketch.Runner pad := DEFAULT_PADDING - themeID := DEFAULT_THEME + themeID := d2themescatalog.NeutralDefault.ID darkThemeID := DEFAULT_DARK_THEME var scale *float64 if opts != nil { - pad = opts.Pad - if opts.Sketch { + if opts.Pad != nil { + pad = int(*opts.Pad) + } + if opts.Sketch != nil && *opts.Sketch { var err error sketchRunner, err = d2sketch.InitSketchVM() if err != nil { return nil, err } } - themeID = opts.ThemeID + if opts.ThemeID != nil { + themeID = *opts.ThemeID + } darkThemeID = opts.DarkThemeID scale = opts.Scale } @@ -1792,7 +1794,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { upperBuf := &bytes.Buffer{} if opts.MasterID == "" { EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily, diagram.GetCorpus()) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf` - themeStylesheet, err := ThemeCSS(diagramHash, themeID, darkThemeID) + themeStylesheet, err := ThemeCSS(diagramHash, &themeID, darkThemeID) if err != nil { return nil, err } @@ -1913,7 +1915,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } alignment := "xMinYMin" - if opts.Center { + if opts.Center != nil && *opts.Center { alignment = "xMidYMid" } fitToScreenWrapperOpening := "" @@ -1954,8 +1956,11 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } // TODO include only colors that are being used to reduce size -func ThemeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) { - out, err := singleThemeRulesets(diagramHash, themeID) +func ThemeCSS(diagramHash string, themeID *int64, darkThemeID *int64) (stylesheet string, err error) { + if themeID == nil { + themeID = &d2themescatalog.NeutralDefault.ID + } + out, err := singleThemeRulesets(diagramHash, *themeID) if err != nil { return "", err } diff --git a/d2renderers/d2svg/dark_theme/dark_theme_test.go b/d2renderers/d2svg/dark_theme/dark_theme_test.go index caf81ce00..6439b93c9 100644 --- a/d2renderers/d2svg/dark_theme/dark_theme_test.go +++ b/d2renderers/d2svg/dark_theme/dark_theme_test.go @@ -17,6 +17,7 @@ import ( "oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/go2" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2renderers/d2fonts" @@ -426,12 +427,18 @@ func run(t *testing.T, tc testCase) { return } + renderOpts := &d2svg.RenderOpts{ + ThemeID: go2.Pointer(int64(200)), + } + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { + return d2dagrelayout.DefaultLayout, nil + } + diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ - Ruler: ruler, - Layout: d2dagrelayout.DefaultLayout, - FontFamily: go2.Pointer(d2fonts.HandDrawn), - ThemeID: 200, - }) + Ruler: ruler, + LayoutResolver: layoutResolver, + FontFamily: go2.Pointer(d2fonts.HandDrawn), + }, renderOpts) if !tassert.Nil(t, err) { return } @@ -439,10 +446,7 @@ func run(t *testing.T, tc testCase) { dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestDarkTheme/")) pathGotSVG := filepath.Join(dataPath, "dark_theme.got.svg") - svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: d2svg.DEFAULT_PADDING, - ThemeID: 200, - }) + svgBytes, err := d2svg.Render(diagram, renderOpts) assert.Success(t, err) err = os.MkdirAll(dataPath, 0755) assert.Success(t, err) diff --git a/d2target/d2target.go b/d2target/d2target.go index fb7cd0cf2..a4502ef90 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -38,8 +38,24 @@ const ( var BorderOffset = geo.NewVector(5, 5) +type Config struct { + Sketch *bool `json:"sketch"` + ThemeID *int64 `json:"themeID"` + DarkThemeID *int64 `json:"darkThemeID"` + Pad *int64 `json:"pad"` + Center *bool `json:"center"` + LayoutEngine *string `json:"layoutEngine"` + ThemeOverrides *ThemeOverrides `json:"themeOverrides"` +} + +type ThemeOverrides struct { + N1 *string `json:"n1"` + // TODO +} + type Diagram struct { - Name string `json:"name"` + Name string `json:"name"` + Config *Config `json:"config,omitempty"` // See docs on the same field in d2graph to understand what it means. IsFolderOnly bool `json:"isFolderOnly"` Description string `json:"description,omitempty"` diff --git a/docs/examples/lib/1-d2lib/d2lib.go b/docs/examples/lib/1-d2lib/d2lib.go index 1a0472970..58f2ddc40 100644 --- a/docs/examples/lib/1-d2lib/d2lib.go +++ b/docs/examples/lib/1-d2lib/d2lib.go @@ -11,21 +11,24 @@ import ( "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/lib/textmeasure" + "oss.terrastruct.com/util-go/go2" ) // Remember to add if err != nil checks in production. func main() { ruler, _ := textmeasure.NewRuler() - defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { - return d2dagrelayout.Layout(ctx, g, nil) + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { + return d2dagrelayout.DefaultLayout, nil } - diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ - Layout: defaultLayout, - Ruler: ruler, - }) - out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: d2svg.DEFAULT_PADDING, - ThemeID: d2themescatalog.GrapeSoda.ID, - }) + renderOpts := &d2svg.RenderOpts{ + Pad: go2.Pointer(int64(5)), + ThemeID: &d2themescatalog.GrapeSoda.ID, + } + compileOpts := &d2lib.CompileOptions{ + LayoutResolver: layoutResolver, + Ruler: ruler, + } + diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", compileOpts, renderOpts) + out, _ := d2svg.Render(diagram, renderOpts) _ = ioutil.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 8f37024e2..b59dca1fe 100644 --- a/docs/examples/lib/2-d2oracle/d2oracle.go +++ b/docs/examples/lib/2-d2oracle/d2oracle.go @@ -5,6 +5,7 @@ import ( "fmt" "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2oracle" @@ -15,10 +16,14 @@ import ( func main() { // From one.go ruler, _ := textmeasure.NewRuler() - _, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ - Layout: d2dagrelayout.DefaultLayout, - Ruler: ruler, - }) + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { + return d2dagrelayout.DefaultLayout, nil + } + compileOpts := &d2lib.CompileOptions{ + LayoutResolver: layoutResolver, + Ruler: ruler, + } + _, graph, _ := d2lib.Compile(context.Background(), "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 55bcea233..f120df278 100644 --- a/docs/examples/lib/3-lowlevel/lowlevel.go +++ b/docs/examples/lib/3-lowlevel/lowlevel.go @@ -16,15 +16,14 @@ import ( // Remember to add if err != nil checks in production. func main() { - graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) + graph, _, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) 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) out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: d2svg.DEFAULT_PADDING, - ThemeID: d2themescatalog.NeutralDefault.ID, + ThemeID: &d2themescatalog.NeutralDefault.ID, }) _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) } diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index c968c6f42..32f5ec26f 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -111,6 +111,38 @@ func TestCLI_E2E(t *testing.T) { shape: text } +steps: { + 1: { + Approach road + } + 2: { + Approach road -> Cross road + } + 3: { + Cross road -> Make you wonder why + } +} +`) + err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2") + assert.Success(t, err) + svg := readFile(t, dir, "animation.svg") + assert.Testdata(t, ".svg", svg) + }, + }, + { + name: "vars-animation", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "animation.d2", `vars: { + d2-config: { + theme-id: 300 + } +} +Chicken's plan: { + style.font-size: 35 + near: top-center + shape: text +} + steps: { 1: { Approach road @@ -404,6 +436,17 @@ steps: { assert.Testdata(t, ".svg", svg) }, }, + { + name: "import_vars", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "hello-world.d2", `vars: { d2-config: @config }; x -> y`) + writeFile(t, dir, "config.d2", `theme-id: 200`) + err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2")) + assert.Success(t, err) + svg := readFile(t, dir, "hello-world.svg") + assert.Testdata(t, ".svg", svg) + }, + }, { name: "import_spread_nested", run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { @@ -449,6 +492,26 @@ steps: { }) }, }, + { + name: "vars-config", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "hello-world.d2", `vars: { + d2-config: { + sketch: true + layout-engine: elk + } +} +x -> y -> a.dream +it -> was -> all -> a.dream +i used to read +`) + env.Setenv("D2_THEME", "1") + err := runTestMain(t, ctx, dir, env, "--pad=10", "hello-world.d2") + assert.Success(t, err) + svg := readFile(t, dir, "hello-world.svg") + assert.Testdata(t, ".svg", svg) + }, + }, } ctx := context.Background() diff --git a/e2etests-cli/testdata/TestCLI_E2E/import_vars.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/import_vars.exp.svg new file mode 100644 index 000000000..565762cdf --- /dev/null +++ b/e2etests-cli/testdata/TestCLI_E2E/import_vars.exp.svg @@ -0,0 +1,96 @@ +xy + + + + diff --git a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf index 763c539b9822b93b1cc157155275f42bdc34ae08..99f8e0982eadddb8c6d3f06a184ed5bd2a2f5edb 100644 GIT binary patch delta 21584 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 Ag8%>k delta 21583 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 T5s8S5-DHZuI0pwWnm6tbt~@j` diff --git a/e2etests-cli/testdata/TestCLI_E2E/vars-animation.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/vars-animation.exp.svg new file mode 100644 index 000000000..c48b464ec --- /dev/null +++ b/e2etests-cli/testdata/TestCLI_E2E/vars-animation.exp.svg @@ -0,0 +1,883 @@ +CHICKEN'S PLAN + + +APPROACH ROADCHICKEN'S PLAN + + + +APPROACH ROADCROSS ROADCHICKEN'S PLAN + + + + +APPROACH ROADCROSS ROADMAKE YOU WONDER WHYCHICKEN'S PLAN + + + + + + \ No newline at end of file diff --git a/e2etests-cli/testdata/TestCLI_E2E/vars-config.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/vars-config.exp.svg new file mode 100644 index 000000000..40d76dd75 --- /dev/null +++ b/e2etests-cli/testdata/TestCLI_E2E/vars-config.exp.svg @@ -0,0 +1,117 @@ + + + + + + + + +xyaitwasalli used to readdream + + + + + + + + + + diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go index 74bec7b16..53201f8d7 100644 --- a/e2etests/e2e_test.go +++ b/e2etests/e2e_test.go @@ -15,6 +15,7 @@ import ( "oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/diff" + "oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2graph" @@ -87,7 +88,7 @@ type testCase struct { dagreFeatureError string elkFeatureError string expErr string - themeID int64 + themeID *int64 } func runa(t *testing.T, tcs []testCase) { @@ -109,7 +110,7 @@ func runa(t *testing.T, tcs []testCase) { func serde(t *testing.T, tc testCase, ruler *textmeasure.Ruler) { ctx := context.Background() ctx = log.WithTB(ctx, t, nil) - g, err := d2compiler.Compile("", strings.NewReader(tc.script), &d2compiler.CompileOptions{ + g, _, err := d2compiler.Compile("", strings.NewReader(tc.script), &d2compiler.CompileOptions{ UTF16: false, }) trequire.Nil(t, err) @@ -146,28 +147,39 @@ func run(t *testing.T, tc testCase) { layoutsTested = append(layoutsTested, "elk") } + layoutResolver := func(engine string) (d2graph.LayoutGraph, error) { + if strings.EqualFold(engine, "elk") { + return d2elklayout.DefaultLayout, nil + } + return d2dagrelayout.DefaultLayout, nil + } + for _, layoutName := range layoutsTested { - var layout func(context.Context, *d2graph.Graph) error var plugin d2plugin.Plugin if layoutName == "dagre" { - layout = d2dagrelayout.DefaultLayout plugin = &d2plugin.DagrePlugin } else if layoutName == "elk" { // If measured texts exists, we are specifically exercising text measurements, no need to run on both layouts if tc.mtexts != nil { continue } - layout = d2elklayout.DefaultLayout plugin = &d2plugin.ELKPlugin } - diagram, g, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ - Ruler: ruler, - MeasuredTexts: tc.mtexts, - Layout: layout, - ThemeID: tc.themeID, - }) + compileOpts := &d2lib.CompileOptions{ + Ruler: ruler, + MeasuredTexts: tc.mtexts, + Layout: go2.Pointer(layoutName), + LayoutResolver: layoutResolver, + } + renderOpts := &d2svg.RenderOpts{ + Pad: go2.Pointer(int64(0)), + ThemeID: tc.themeID, + // To compare deltas at a fixed scale + // Scale: go2.Pointer(1.), + } + diagram, g, err := d2lib.Compile(ctx, tc.script, compileOpts, renderOpts) if tc.expErr != "" { assert.Error(t, err) assert.ErrorString(t, err, tc.expErr) @@ -205,12 +217,6 @@ func run(t *testing.T, tc testCase) { dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestE2E/"), layoutName) pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") - renderOpts := &d2svg.RenderOpts{ - Pad: 0, - ThemeID: tc.themeID, - // To compare deltas at a fixed scale - // Scale: go2.Pointer(1.), - } if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 { masterID, err := diagram.HashID() assert.Success(t, err) diff --git a/e2etests/themes_test.go b/e2etests/themes_test.go index 42600ec94..702cfda66 100644 --- a/e2etests/themes_test.go +++ b/e2etests/themes_test.go @@ -11,7 +11,7 @@ func testThemes(t *testing.T) { tcs := []testCase{ { name: "dark terrastruct flagship", - themeID: d2themescatalog.DarkFlagshipTerrastruct.ID, + themeID: &d2themescatalog.DarkFlagshipTerrastruct.ID, script: ` network: { cell tower: { @@ -118,7 +118,7 @@ ex: |tex }, { name: "terminal", - themeID: d2themescatalog.Terminal.ID, + themeID: &d2themescatalog.Terminal.ID, script: ` network: { cell tower: { @@ -225,7 +225,7 @@ ex: |tex }, { name: "terminal_grayscale", - themeID: d2themescatalog.TerminalGrayscale.ID, + themeID: &d2themescatalog.TerminalGrayscale.ID, script: ` network: { cell tower: { @@ -279,7 +279,7 @@ network.data processor -> api server }, { name: "origami", - themeID: d2themescatalog.Origami.ID, + themeID: &d2themescatalog.Origami.ID, script: ` network: 通信網 { cell tower: { diff --git a/testdata/d2compiler/TestCompile2/vars/config/basic.exp.json b/testdata/d2compiler/TestCompile2/vars/config/basic.exp.json new file mode 100644 index 000000000..bff64593a --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/config/basic.exp.json @@ -0,0 +1,291 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,0:0:0-8:0:54", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,1:0:1-5:1:45", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,1:0:1-1:4:5", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,1:0:1-1:4:5", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,1:6:7-5:1:45", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,2:1:10-4:3:43", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,2:1:10-2:10:19", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,2:1:10-2:10:19", + "value": [ + { + "string": "d2-config", + "raw_string": "d2-config" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,2:12:21-4:3:43", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,3:4:27-3:16:39", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,3:4:27-3:10:33", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,3:4:27-3:10:33", + "value": [ + { + "string": "sketch", + "raw_string": "sketch" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "boolean": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,3:12:35-3:16:39", + "value": true + } + } + } + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:0:47-7:6:53", + "edges": [ + { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:0:47-7:6:53", + "src": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:0:47-7:1:48", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:0:47-7:1:48", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "src_arrow": "", + "dst": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:5:52-7:6:53", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:5:52-7:6:53", + "value": [ + { + "string": "y", + "raw_string": "y" + } + ] + } + } + ] + }, + "dst_arrow": ">" + } + ], + "primary": {}, + "value": {} + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": [ + { + "index": 0, + "isCurve": false, + "src_arrow": false, + "dst_arrow": true, + "references": [ + { + "map_key_edge_index": 0 + } + ], + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ], + "objects": [ + { + "id": "x", + "id_val": "x", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:0:47-7:1:48", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:0:47-7:1:48", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": 0 + } + ], + "attributes": { + "label": { + "value": "x" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "y", + "id_val": "y", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:5:52-7:6:53", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/basic.d2,7:5:52-7:6:53", + "value": [ + { + "string": "y", + "raw_string": "y" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": 0 + } + ], + "attributes": { + "label": { + "value": "y" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +} diff --git a/testdata/d2compiler/TestCompile2/vars/config/invalid.exp.json b/testdata/d2compiler/TestCompile2/vars/config/invalid.exp.json new file mode 100644 index 000000000..aebf3ac63 --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/config/invalid.exp.json @@ -0,0 +1,11 @@ +{ + "graph": null, + "err": { + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/invalid.d2,3:4:27-3:10:33", + "errmsg": "d2/testdata/d2compiler/TestCompile2/vars/config/invalid.d2:4:5: expected a boolean for \"sketch\", got \"lol\"" + } + ] + } +} diff --git a/testdata/d2compiler/TestCompile2/vars/config/not-root.exp.json b/testdata/d2compiler/TestCompile2/vars/config/not-root.exp.json new file mode 100644 index 000000000..3bdfc55ce --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/config/not-root.exp.json @@ -0,0 +1,11 @@ +{ + "graph": null, + "err": { + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile2/vars/config/not-root.d2,3:3:19-3:12:28", + "errmsg": "d2/testdata/d2compiler/TestCompile2/vars/config/not-root.d2:4:4: \"d2-config\" can only appear at root vars" + } + ] + } +}