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 763c539b9..99f8e0982 100644 Binary files a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf and b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf differ 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" + } + ] + } +}