config vars

This commit is contained in:
Alexander Wang 2023-07-14 13:08:26 -07:00
parent 6b65f298b2
commit 45b396c894
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
31 changed files with 2015 additions and 229 deletions

View file

@ -102,7 +102,7 @@ func test(t *testing.T, textPath, text string) {
t.Fatal(err) t.Fatal(err)
} }
g, err := d2compiler.Compile("", strings.NewReader(text), nil) g, _, err := d2compiler.Compile("", strings.NewReader(text), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -20,6 +20,7 @@ import (
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/util-go/xmain" "oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2plugin" "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.") 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.") 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 { if err != nil {
return err return err
} }
err = populateLayoutOpts(ctx, ms, ps) err = populateLayoutOpts(ctx, ms, plugins)
if err != nil { if err != nil {
return err return err
} }
@ -146,7 +147,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
case "init-playwright": case "init-playwright":
return initPlaywright() return initPlaywright()
case "layout": case "layout":
return layoutCmd(ctx, ms, ps) return layoutCmd(ctx, ms, plugins)
case "themes": case "themes":
themesCmd(ctx, ms) themesCmd(ctx, ms)
return nil 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) 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 { if *darkThemeFlag == -1 {
darkThemeFlag = nil // TODO this is a temporary solution: https://github.com/terrastruct/util-go/issues/7 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 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 !outputFormat.supportsDarkTheme() {
if darkThemeFlag != nil { if darkThemeFlag != nil {
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg") 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{ renderOpts := d2svg.RenderOpts{
Pad: int(*padFlag), Pad: padFlag,
Sketch: *sketchFlag, Sketch: sketchFlag,
Center: *centerFlag, Center: centerFlag,
ThemeID: *themeFlag, ThemeID: themeFlag,
DarkThemeID: darkThemeFlag, DarkThemeID: darkThemeFlag,
Scale: scale, 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") return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
} }
w, err := newWatcher(ctx, ms, watcherOpts{ w, err := newWatcher(ctx, ms, watcherOpts{
layoutPlugin: plugin, plugins: plugins,
layout: layoutFlag,
renderOpts: renderOpts, renderOpts: renderOpts,
animateInterval: *animateIntervalFlag, animateInterval: *animateIntervalFlag,
host: *hostFlag, host: *hostFlag,
@ -319,7 +330,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2) ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
defer cancel() 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 err != nil {
if written { if written {
return fmt.Errorf("failed to fully compile (partial render written): %w", err) 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 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() start := time.Now()
input, err := ms.ReadPath(inputPath) input, err := ms.ReadPath(inputPath)
if err != nil { if err != nil {
@ -341,16 +377,12 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
return nil, false, err return nil, false, err
} }
layout := plugin.Layout
opts := &d2lib.CompileOptions{ opts := &d2lib.CompileOptions{
Layout: layout, Ruler: ruler,
Ruler: ruler, FontFamily: fontFamily,
ThemeID: renderOpts.ThemeID, InputPath: inputPath,
FontFamily: fontFamily, LayoutResolver: LayoutResolver(ctx, ms, plugins),
InputPath: inputPath, Layout: layout,
}
if renderOpts.Sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
} }
cancel := background.Repeat(func() { cancel := background.Repeat(func() {
@ -358,12 +390,14 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
}, time.Second*5) }, time.Second*5)
defer cancel() defer cancel()
diagram, g, err := d2lib.Compile(ctx, string(input), opts) diagram, g, err := d2lib.Compile(ctx, string(input), opts, &renderOpts)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
cancel() cancel()
plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
if animateInterval > 0 { if animateInterval > 0 {
masterID, err := diagram.HashID() masterID, err := diagram.HashID()
if err != nil { if err != nil {
@ -372,6 +406,16 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
renderOpts.MasterID = masterID 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) pluginInfo, err := plugin.Info(ctx)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@ -805,7 +849,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
if err != nil { if err != nil {
return svg, err 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 { if err != nil {
return svg, err return svg, err
} }

View file

@ -41,7 +41,8 @@ var devMode = false
var staticFS embed.FS var staticFS embed.FS
type watcherOpts struct { type watcherOpts struct {
layoutPlugin d2plugin.Plugin layout *string
plugins []d2plugin.Plugin
renderOpts d2svg.RenderOpts renderOpts d2svg.RenderOpts
animateInterval int64 animateInterval int64
host string host string
@ -361,7 +362,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
w.pw = newPW 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 := "" errs := ""
if err != nil { if err != nil {
if len(svg) > 0 { if len(svg) > 0 {

View file

@ -27,7 +27,7 @@ type CompileOptions struct {
FS fs.FS 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 { if opts == nil {
opts = &CompileOptions{} opts = &CompileOptions{}
} }
@ -36,7 +36,7 @@ func Compile(p string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph, e
UTF16: opts.UTF16, UTF16: opts.UTF16,
}) })
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
ir, err := d2ir.Compile(ast, &d2ir.CompileOptions{ 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, FS: opts.FS,
}) })
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
g, err := compileIR(ast, ir) g, err := compileIR(ast, ir)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
g.SortObjectsByAST() g.SortObjectsByAST()
g.SortEdgesByAST() g.SortEdgesByAST()
return g, nil return g, compileConfig(ir), nil
} }
func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) { func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) {
@ -1285,3 +1285,45 @@ func parentSeqDiagram(n d2ir.Node) *d2ir.Map {
n = m 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
}

View file

@ -2702,7 +2702,7 @@ object: {
t.Parallel() t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) 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 tc.expErr != "" {
if err == nil { if err == nil {
t.Fatalf("expected error with: %q", tc.expErr) t.Fatalf("expected error with: %q", tc.expErr)
@ -2757,7 +2757,7 @@ func testBoards(t *testing.T) {
{ {
name: "root", name: "root",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, `base g, _ := assertCompile(t, `base
layers: { layers: {
one: { one: {
@ -2776,7 +2776,7 @@ layers: {
{ {
name: "recursive", name: "recursive",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, `base g, _ := assertCompile(t, `base
layers: { layers: {
one: { one: {
@ -2804,7 +2804,7 @@ layers: {
{ {
name: "isFolderOnly", name: "isFolderOnly",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
layers: { layers: {
one: { one: {
santa santa
@ -2939,7 +2939,7 @@ func testNulls(t *testing.T) {
{ {
name: "shape", name: "shape",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a a
a: null a: null
`, "") `, "")
@ -2949,7 +2949,7 @@ a: null
{ {
name: "edge", name: "edge",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a -> b a -> b
(a -> b)[0]: null (a -> b)[0]: null
`, "") `, "")
@ -2960,7 +2960,7 @@ a -> b
{ {
name: "attribute", name: "attribute",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a.style.opacity: 0.2 a.style.opacity: 0.2
a.style.opacity: null a.style.opacity: null
`, "") `, "")
@ -2992,7 +2992,7 @@ a.style.opacity: null
{ {
name: "shape", name: "shape",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a a
a: null a: null
a a
@ -3003,7 +3003,7 @@ a
{ {
name: "edge", name: "edge",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a -> b a -> b
(a -> b)[0]: null (a -> b)[0]: null
a -> b a -> b
@ -3015,7 +3015,7 @@ a -> b
{ {
name: "attribute-reset", name: "attribute-reset",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a.style.opacity: 0.2 a.style.opacity: 0.2
a: null a: null
a a
@ -3027,7 +3027,7 @@ a
{ {
name: "edge-reset", name: "edge-reset",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a -> b a -> b
a: null a: null
a a
@ -3039,7 +3039,7 @@ a
{ {
name: "children-reset", name: "children-reset",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
a.b.c a.b.c
a.b: null a.b: null
a.b a.b
@ -3072,7 +3072,7 @@ a.b
{ {
name: "delete-connection", name: "delete-connection",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
x -> y x -> y
y: null y: null
`, "") `, "")
@ -3083,7 +3083,7 @@ y: null
{ {
name: "delete-multiple-connections", name: "delete-multiple-connections",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
x -> y x -> y
z -> y z -> y
y -> a y -> a
@ -3096,7 +3096,7 @@ y: null
{ {
name: "no-delete-connection", name: "no-delete-connection",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
y: null y: null
x -> y x -> y
`, "") `, "")
@ -3107,7 +3107,7 @@ x -> y
{ {
name: "delete-children", name: "delete-children",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
x.y.z x.y.z
a.b.c a.b.c
@ -3142,7 +3142,7 @@ a.b: null
{ {
name: "scenario", name: "scenario",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
x x
scenarios: { scenarios: {
@ -3183,7 +3183,7 @@ func testVars(t *testing.T) {
{ {
name: "shape-label", name: "shape-label",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3196,7 +3196,7 @@ hi: ${x}
{ {
name: "style", name: "style",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
primary-color: red primary-color: red
} }
@ -3211,7 +3211,7 @@ hi: {
{ {
name: "number", name: "number",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
columns: 2 columns: 2
} }
@ -3226,7 +3226,7 @@ hi: {
{ {
name: "nested", name: "nested",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
colors: { colors: {
primary: { primary: {
@ -3244,7 +3244,7 @@ hi: {
{ {
name: "combined", name: "combined",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3256,7 +3256,7 @@ hi: 1 ${x} 2
{ {
name: "double-quoted", name: "double-quoted",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3268,7 +3268,7 @@ hi: "1 ${x} 2"
{ {
name: "single-quoted", name: "single-quoted",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3280,7 +3280,7 @@ hi: '1 ${x} 2'
{ {
name: "edge-label", name: "edge-label",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3293,7 +3293,7 @@ a -> b: ${x}
{ {
name: "edge-map", name: "edge-map",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3308,7 +3308,7 @@ a -> b: {
{ {
name: "quoted-var", name: "quoted-var",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
primaryColors: { primaryColors: {
button: { button: {
@ -3330,7 +3330,7 @@ button: {
{ {
name: "quoted-var-quoted-sub", name: "quoted-var-quoted-sub",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: "hi" x: "hi"
} }
@ -3343,7 +3343,7 @@ y: "hey ${x}"
{ {
name: "parent-scope", name: "parent-scope",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im root var x: im root var
} }
@ -3360,7 +3360,7 @@ a: {
{ {
name: "map", name: "map",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
cool-style: { cool-style: {
fill: red fill: red
@ -3379,7 +3379,7 @@ a -> b: ${arrows}
{ {
name: "primary-and-composite", name: "primary-and-composite",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: all { x: all {
a: b a: b
@ -3395,7 +3395,7 @@ z: ${x}
{ {
name: "spread", name: "spread",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: all { x: all {
a: b a: b
@ -3415,7 +3415,7 @@ z: {
{ {
name: "array", name: "array",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
base-constraints: [UNQ; NOT NULL] base-constraints: [UNQ; NOT NULL]
} }
@ -3431,7 +3431,7 @@ a: {
{ {
name: "spread-array", name: "spread-array",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
base-constraints: [UNQ; NOT NULL] base-constraints: [UNQ; NOT NULL]
} }
@ -3447,7 +3447,7 @@ a: {
{ {
name: "sub-array", name: "sub-array",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: all x: all
} }
@ -3460,7 +3460,7 @@ z.class: [a; ${x}]
{ {
name: "multi-part-array", name: "multi-part-array",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: all x: all
} }
@ -3473,7 +3473,7 @@ z.class: [a; ${x}together]
{ {
name: "double-quote-primary", name: "double-quote-primary",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: always { x: always {
a: b a: b
@ -3488,7 +3488,7 @@ z: "${x} be my maybe"
{ {
name: "spread-nested", name: "spread-nested",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
disclaimer: { disclaimer: {
I am not a lawyer I am not a lawyer
@ -3504,7 +3504,7 @@ custom-disclaimer: DRAFT DISCLAIMER {
{ {
name: "spread-edge", name: "spread-edge",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
connections: { connections: {
x -> a x -> a
@ -3543,7 +3543,7 @@ hi: {
{ {
name: "label", name: "label",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3557,7 +3557,7 @@ hi: not a var
{ {
name: "map", name: "map",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im root var x: im root var
} }
@ -3574,7 +3574,7 @@ a: {
{ {
name: "var-in-var", name: "var-in-var",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
surname: Smith surname: Smith
} }
@ -3592,7 +3592,7 @@ a: {
{ {
name: "recursive-var", name: "recursive-var",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: a x: a
} }
@ -3667,7 +3667,7 @@ a: {
{ {
name: "layer", name: "layer",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3685,7 +3685,7 @@ layers: {
{ {
name: "layer-2", name: "layer-2",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: root var x x: root var x
y: root var y y: root var y
@ -3710,7 +3710,7 @@ layers: {
{ {
name: "scenario", name: "scenario",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im a var x: im a var
} }
@ -3728,7 +3728,7 @@ scenarios: {
{ {
name: "overlay", name: "overlay",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im x var x: im x var
} }
@ -3763,7 +3763,7 @@ layers: {
{ {
name: "replace", name: "replace",
run: func(t *testing.T) { run: func(t *testing.T) {
g := assertCompile(t, ` g, _ := assertCompile(t, `
vars: { vars: {
x: im x var 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.Run("errors", func(t *testing.T) {
t.Parallel() 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()) 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 != "" { if expErr != "" {
assert.Error(t, err) assert.Error(t, err)
assert.ErrorString(t, err, expErr) 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) err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
assert.Success(t, err) assert.Success(t, err)
return g return g, config
} }

View file

@ -219,7 +219,7 @@ func run(t *testing.T, tc testCase) {
ctx = log.WithTB(ctx, t, nil) ctx = log.WithTB(ctx, t, nil)
ctx = log.Leveled(ctx, slog.LevelDebug) 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, UTF16: true,
}) })
if err != nil { if err != nil {

View file

@ -13,7 +13,7 @@ import (
func TestSerialization(t *testing.T) { func TestSerialization(t *testing.T) {
t.Parallel() 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) assert.Nil(t, err)
asserts := func(g *d2graph.Graph) { asserts := func(g *d2graph.Graph) {
@ -53,7 +53,7 @@ func TestCasingRegression(t *testing.T) {
script := `UserCreatedTypeField` script := `UserCreatedTypeField`
g, err := d2compiler.Compile("", strings.NewReader(script), nil) g, _, err := d2compiler.Compile("", strings.NewReader(script), nil)
assert.Nil(t, err) assert.Nil(t, err)
_, ok := g.Root.HasChild([]string{"UserCreatedTypeField"}) _, ok := g.Root.HasChild([]string{"UserCreatedTypeField"})

View file

@ -2,11 +2,14 @@ package d2ir
import ( import (
"io/fs" "io/fs"
"strconv"
"strings" "strings"
"oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/util-go/go2" "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 // don't resolve substitutions in vars with the current scope of vars
if f.Name == "vars" { if f.Name == "vars" {
c.compileSubstitutions(f.Map(), varsStack[1:]) c.compileSubstitutions(f.Map(), varsStack[1:])
c.validateConfigs(f.Map().GetField("d2-config"))
} else { } else {
c.compileSubstitutions(f.Map(), varsStack) 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) { func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) {
var subbed bool var subbed bool
var resolvedField *Field var resolvedField *Field

View file

@ -37,7 +37,7 @@ n2 -> n1: right to left
n1 -> n2 n1 -> n2
n2 -> n1 n2 -> n1
` `
g, err := d2compiler.Compile("", strings.NewReader(input), nil) g, _, err := d2compiler.Compile("", strings.NewReader(input), nil)
assert.Nil(t, err) assert.Nil(t, err)
n1, has := g.Root.HasChild([]string{"n1"}) n1, has := g.Root.HasChild([]string{"n1"})
@ -177,7 +177,7 @@ a.t2 -> b
b -> a.t2` b -> a.t2`
ctx := log.WithTB(context.Background(), t, nil) 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) assert.Nil(t, err)
g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
@ -298,7 +298,7 @@ c
container -> c: edge 1 container -> c: edge 1
` `
ctx := log.WithTB(context.Background(), t, nil) 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) assert.Nil(t, err)
container, has := g.Root.HasChild([]string{"container"}) container, has := g.Root.HasChild([]string{"container"})

View file

@ -15,17 +15,21 @@ import (
"oss.terrastruct.com/d2/d2layouts/d2near" "oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence" "oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/util-go/go2"
) )
type CompileOptions struct { type CompileOptions struct {
UTF16 bool UTF16 bool
FS fs.FS FS fs.FS
MeasuredTexts []*d2target.MText MeasuredTexts []*d2target.MText
Ruler *textmeasure.Ruler Ruler *textmeasure.Ruler
Layout func(context.Context, *d2graph.Graph) error LayoutResolver func(engine string) (d2graph.LayoutGraph, error)
ThemeID int64
Layout *string
// FontFamily controls the font family used for all texts that are not the following: // FontFamily controls the font family used for all texts that are not the following:
// - code // - code
@ -37,39 +41,42 @@ type CompileOptions struct {
InputPath string InputPath string
} }
func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) { func Compile(ctx context.Context, input string, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, *d2graph.Graph, error) {
if opts == nil { if compileOpts == nil {
opts = &CompileOptions{} compileOpts = &CompileOptions{}
}
if renderOpts == nil {
renderOpts = &d2svg.RenderOpts{}
} }
g, err := d2compiler.Compile(opts.InputPath, strings.NewReader(input), &d2compiler.CompileOptions{ g, config, err := d2compiler.Compile(compileOpts.InputPath, strings.NewReader(input), &d2compiler.CompileOptions{
UTF16: opts.UTF16, UTF16: compileOpts.UTF16,
FS: opts.FS, FS: compileOpts.FS,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
d, err := compile(ctx, g, opts) applyConfigs(config, compileOpts, renderOpts)
if err != nil { applyDefaults(compileOpts, renderOpts)
return nil, nil, err
} d, err := compile(ctx, g, compileOpts, renderOpts)
return d, g, nil return d, g, err
} }
func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) { func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, error) {
err := g.ApplyTheme(opts.ThemeID) err := g.ApplyTheme(*renderOpts.ThemeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(g.Objects) > 0 { 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 { if err != nil {
return nil, err return nil, err
} }
coreLayout, err := getLayout(opts) coreLayout, err := getLayout(compileOpts)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
for _, l := range g.Layers { for _, l := range g.Layers {
ld, err := compile(ctx, l, opts) ld, err := compile(ctx, l, compileOpts, renderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
d.Layers = append(d.Layers, ld) d.Layers = append(d.Layers, ld)
} }
for _, l := range g.Scenarios { for _, l := range g.Scenarios {
ld, err := compile(ctx, l, opts) ld, err := compile(ctx, l, compileOpts, renderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
d.Scenarios = append(d.Scenarios, ld) d.Scenarios = append(d.Scenarios, ld)
} }
for _, l := range g.Steps { for _, l := range g.Steps {
ld, err := compile(ctx, l, opts) ld, err := compile(ctx, l, compileOpts, renderOpts)
if err != nil { if err != nil {
return nil, err 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) { func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
if opts.Layout != nil { if opts.Layout != nil {
return opts.Layout, nil return opts.LayoutResolver(*opts.Layout)
} else if os.Getenv("D2_LAYOUT") == "dagre" { } else if os.Getenv("D2_LAYOUT") == "dagre" {
defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { defaultLayout := func(ctx context.Context, g *d2graph.Graph) error {
return d2dagrelayout.Layout(ctx, g, nil) return d2dagrelayout.Layout(ctx, g, nil)
@ -137,3 +144,53 @@ func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
return nil, errors.New("no available layout") 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)
}
}

View file

@ -305,7 +305,7 @@ func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Objec
func recompile(ast *d2ast.Map) (*d2graph.Graph, error) { func recompile(ast *d2ast.Map) (*d2graph.Graph, error) {
s := d2format.Format(ast) 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 { if err != nil {
return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err) return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err)
} }

View file

@ -7002,7 +7002,7 @@ type editTest struct {
func (tc editTest) run(t *testing.T) { func (tc editTest) run(t *testing.T) {
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -7265,7 +7265,7 @@ scenarios: {
t.Parallel() t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -7725,7 +7725,7 @@ z
t.Parallel() t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -8095,7 +8095,7 @@ layers: {
t.Parallel() t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -8325,7 +8325,7 @@ scenarios: {
t.Parallel() t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -49,10 +49,10 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
// TODO account for stroke width of root border // TODO account for stroke width of root border
tl, br := rootDiagram.NestedBoundingBox() tl, br := rootDiagram.NestedBoundingBox()
left := tl.X - renderOpts.Pad left := tl.X - int(*renderOpts.Pad)
top := tl.Y - renderOpts.Pad top := tl.Y - int(*renderOpts.Pad)
width := br.X - tl.X + renderOpts.Pad*2 width := br.X - tl.X + int(*renderOpts.Pad)*2
height := br.Y - tl.Y + renderOpts.Pad*2 height := br.Y - tl.Y + int(*renderOpts.Pad)*2
fitToScreenWrapperOpening := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d">`, fitToScreenWrapperOpening := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d">`,
version.Version, version.Version,
@ -93,7 +93,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
fmt.Fprintf(buf, `<style type="text/css">%s</style>`, css) fmt.Fprintf(buf, `<style type="text/css">%s</style>`, css)
} }
if renderOpts.Sketch { if renderOpts.Sketch != nil && *renderOpts.Sketch {
d2sketch.DefineFillPatterns(buf) d2sketch.DefineFillPatterns(buf)
} }

View file

@ -17,6 +17,7 @@ import (
"oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2elklayout" "oss.terrastruct.com/d2/d2layouts/d2elklayout"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
@ -1339,16 +1340,22 @@ func run(t *testing.T, tc testCase) {
return return
} }
layout := d2dagrelayout.DefaultLayout layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
if strings.EqualFold(tc.engine, "elk") { if strings.EqualFold(engine, "elk") {
layout = d2elklayout.DefaultLayout 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{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
Layout: layout, Layout: &tc.engine,
FontFamily: go2.Pointer(d2fonts.HandDrawn), LayoutResolver: layoutResolver,
ThemeID: tc.themeID, FontFamily: go2.Pointer(d2fonts.HandDrawn),
}) }, renderOpts)
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return
} }
@ -1356,11 +1363,7 @@ func run(t *testing.T, tc testCase) {
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/")) dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/"))
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ svgBytes, err := d2svg.Render(diagram, renderOpts)
Pad: d2svg.DEFAULT_PADDING,
Sketch: true,
ThemeID: tc.themeID,
})
assert.Success(t, err) assert.Success(t, err)
err = os.MkdirAll(dataPath, 0755) err = os.MkdirAll(dataPath, 0755)
assert.Success(t, err) assert.Success(t, err)

View file

@ -16,6 +16,7 @@ import (
"oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/d2svg"
@ -152,11 +153,17 @@ func run(t *testing.T, tc testCase) {
return 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{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout, LayoutResolver: layoutResolver,
ThemeID: tc.themeID, }, renderOpts)
})
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return
} }
@ -164,10 +171,7 @@ func run(t *testing.T, tc testCase) {
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestAppendix/")) dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestAppendix/"))
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ svgBytes, err := d2svg.Render(diagram, renderOpts)
Pad: d2svg.DEFAULT_PADDING,
ThemeID: tc.themeID,
})
assert.Success(t, err) assert.Success(t, err)
svgBytes = appendix.Append(diagram, ruler, svgBytes) svgBytes = appendix.Append(diagram, ruler, svgBytes)

View file

@ -69,10 +69,10 @@ var grain string
var paper string var paper string
type RenderOpts struct { type RenderOpts struct {
Pad int Pad *int64
Sketch bool Sketch *bool
Center bool Center *bool
ThemeID int64 ThemeID *int64
DarkThemeID *int64 DarkThemeID *int64
Font string Font string
// the svg will be scaled by this factor, if unset the svg will fit to screen // 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 var DEFAULT_DARK_THEME *int64 = nil // no theme selected
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
var sketchRunner *d2sketch.Runner var sketchRunner *d2sketch.Runner
pad := DEFAULT_PADDING pad := DEFAULT_PADDING
themeID := DEFAULT_THEME themeID := d2themescatalog.NeutralDefault.ID
darkThemeID := DEFAULT_DARK_THEME darkThemeID := DEFAULT_DARK_THEME
var scale *float64 var scale *float64
if opts != nil { if opts != nil {
pad = opts.Pad if opts.Pad != nil {
if opts.Sketch { pad = int(*opts.Pad)
}
if opts.Sketch != nil && *opts.Sketch {
var err error var err error
sketchRunner, err = d2sketch.InitSketchVM() sketchRunner, err = d2sketch.InitSketchVM()
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
themeID = opts.ThemeID if opts.ThemeID != nil {
themeID = *opts.ThemeID
}
darkThemeID = opts.DarkThemeID darkThemeID = opts.DarkThemeID
scale = opts.Scale scale = opts.Scale
} }
@ -1792,7 +1794,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
upperBuf := &bytes.Buffer{} upperBuf := &bytes.Buffer{}
if opts.MasterID == "" { 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` 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 { if err != nil {
return nil, err return nil, err
} }
@ -1913,7 +1915,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
} }
alignment := "xMinYMin" alignment := "xMinYMin"
if opts.Center { if opts.Center != nil && *opts.Center {
alignment = "xMidYMid" alignment = "xMidYMid"
} }
fitToScreenWrapperOpening := "" 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 // TODO include only colors that are being used to reduce size
func ThemeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) { func ThemeCSS(diagramHash string, themeID *int64, darkThemeID *int64) (stylesheet string, err error) {
out, err := singleThemeRulesets(diagramHash, themeID) if themeID == nil {
themeID = &d2themescatalog.NeutralDefault.ID
}
out, err := singleThemeRulesets(diagramHash, *themeID)
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -17,6 +17,7 @@ import (
"oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
@ -426,12 +427,18 @@ func run(t *testing.T, tc testCase) {
return 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{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout, LayoutResolver: layoutResolver,
FontFamily: go2.Pointer(d2fonts.HandDrawn), FontFamily: go2.Pointer(d2fonts.HandDrawn),
ThemeID: 200, }, renderOpts)
})
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return
} }
@ -439,10 +446,7 @@ func run(t *testing.T, tc testCase) {
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestDarkTheme/")) dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestDarkTheme/"))
pathGotSVG := filepath.Join(dataPath, "dark_theme.got.svg") pathGotSVG := filepath.Join(dataPath, "dark_theme.got.svg")
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ svgBytes, err := d2svg.Render(diagram, renderOpts)
Pad: d2svg.DEFAULT_PADDING,
ThemeID: 200,
})
assert.Success(t, err) assert.Success(t, err)
err = os.MkdirAll(dataPath, 0755) err = os.MkdirAll(dataPath, 0755)
assert.Success(t, err) assert.Success(t, err)

View file

@ -38,8 +38,24 @@ const (
var BorderOffset = geo.NewVector(5, 5) 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 { 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. // See docs on the same field in d2graph to understand what it means.
IsFolderOnly bool `json:"isFolderOnly"` IsFolderOnly bool `json:"isFolderOnly"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`

View file

@ -11,21 +11,24 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/util-go/go2"
) )
// Remember to add if err != nil checks in production. // Remember to add if err != nil checks in production.
func main() { func main() {
ruler, _ := textmeasure.NewRuler() ruler, _ := textmeasure.NewRuler()
defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
return d2dagrelayout.Layout(ctx, g, nil) return d2dagrelayout.DefaultLayout, nil
} }
diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ renderOpts := &d2svg.RenderOpts{
Layout: defaultLayout, Pad: go2.Pointer(int64(5)),
Ruler: ruler, ThemeID: &d2themescatalog.GrapeSoda.ID,
}) }
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ compileOpts := &d2lib.CompileOptions{
Pad: d2svg.DEFAULT_PADDING, LayoutResolver: layoutResolver,
ThemeID: d2themescatalog.GrapeSoda.ID, Ruler: ruler,
}) }
diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", compileOpts, renderOpts)
out, _ := d2svg.Render(diagram, renderOpts)
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2oracle"
@ -15,10 +16,14 @@ import (
func main() { func main() {
// From one.go // From one.go
ruler, _ := textmeasure.NewRuler() ruler, _ := textmeasure.NewRuler()
_, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
Layout: d2dagrelayout.DefaultLayout, return d2dagrelayout.DefaultLayout, nil
Ruler: ruler, }
}) compileOpts := &d2lib.CompileOptions{
LayoutResolver: layoutResolver,
Ruler: ruler,
}
_, graph, _ := d2lib.Compile(context.Background(), "x -> y", compileOpts, nil)
// Create a shape with the ID, "meow" // Create a shape with the ID, "meow"
graph, _, _ = d2oracle.Create(graph, nil, "meow") graph, _, _ = d2oracle.Create(graph, nil, "meow")

View file

@ -16,15 +16,14 @@ import (
// Remember to add if err != nil checks in production. // Remember to add if err != nil checks in production.
func main() { func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) graph, _, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
graph.ApplyTheme(d2themescatalog.NeutralDefault.ID) graph.ApplyTheme(d2themescatalog.NeutralDefault.ID)
ruler, _ := textmeasure.NewRuler() ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler, nil) _ = graph.SetDimensions(nil, ruler, nil)
_ = d2dagrelayout.Layout(context.Background(), graph, nil) _ = d2dagrelayout.Layout(context.Background(), graph, nil)
diagram, _ := d2exporter.Export(context.Background(), graph, nil) diagram, _ := d2exporter.Export(context.Background(), graph, nil)
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ 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) _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
} }

View file

@ -111,6 +111,38 @@ func TestCLI_E2E(t *testing.T) {
shape: text 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: { steps: {
1: { 1: {
Approach road Approach road
@ -404,6 +436,17 @@ steps: {
assert.Testdata(t, ".svg", svg) 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", name: "import_spread_nested",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { 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() ctx := context.Background()

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.5.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-855222762" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-855222762 .text-bold {
font-family: "d2-855222762-font-bold";
}
@font-face {
font-family: d2-855222762-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-855222762 .fill-N1{fill:#CDD6F4;}
.d2-855222762 .fill-N2{fill:#BAC2DE;}
.d2-855222762 .fill-N3{fill:#A6ADC8;}
.d2-855222762 .fill-N4{fill:#585B70;}
.d2-855222762 .fill-N5{fill:#45475A;}
.d2-855222762 .fill-N6{fill:#313244;}
.d2-855222762 .fill-N7{fill:#1E1E2E;}
.d2-855222762 .fill-B1{fill:#CBA6f7;}
.d2-855222762 .fill-B2{fill:#CBA6f7;}
.d2-855222762 .fill-B3{fill:#6C7086;}
.d2-855222762 .fill-B4{fill:#585B70;}
.d2-855222762 .fill-B5{fill:#45475A;}
.d2-855222762 .fill-B6{fill:#313244;}
.d2-855222762 .fill-AA2{fill:#f38BA8;}
.d2-855222762 .fill-AA4{fill:#45475A;}
.d2-855222762 .fill-AA5{fill:#313244;}
.d2-855222762 .fill-AB4{fill:#45475A;}
.d2-855222762 .fill-AB5{fill:#313244;}
.d2-855222762 .stroke-N1{stroke:#CDD6F4;}
.d2-855222762 .stroke-N2{stroke:#BAC2DE;}
.d2-855222762 .stroke-N3{stroke:#A6ADC8;}
.d2-855222762 .stroke-N4{stroke:#585B70;}
.d2-855222762 .stroke-N5{stroke:#45475A;}
.d2-855222762 .stroke-N6{stroke:#313244;}
.d2-855222762 .stroke-N7{stroke:#1E1E2E;}
.d2-855222762 .stroke-B1{stroke:#CBA6f7;}
.d2-855222762 .stroke-B2{stroke:#CBA6f7;}
.d2-855222762 .stroke-B3{stroke:#6C7086;}
.d2-855222762 .stroke-B4{stroke:#585B70;}
.d2-855222762 .stroke-B5{stroke:#45475A;}
.d2-855222762 .stroke-B6{stroke:#313244;}
.d2-855222762 .stroke-AA2{stroke:#f38BA8;}
.d2-855222762 .stroke-AA4{stroke:#45475A;}
.d2-855222762 .stroke-AA5{stroke:#313244;}
.d2-855222762 .stroke-AB4{stroke:#45475A;}
.d2-855222762 .stroke-AB5{stroke:#313244;}
.d2-855222762 .background-color-N1{background-color:#CDD6F4;}
.d2-855222762 .background-color-N2{background-color:#BAC2DE;}
.d2-855222762 .background-color-N3{background-color:#A6ADC8;}
.d2-855222762 .background-color-N4{background-color:#585B70;}
.d2-855222762 .background-color-N5{background-color:#45475A;}
.d2-855222762 .background-color-N6{background-color:#313244;}
.d2-855222762 .background-color-N7{background-color:#1E1E2E;}
.d2-855222762 .background-color-B1{background-color:#CBA6f7;}
.d2-855222762 .background-color-B2{background-color:#CBA6f7;}
.d2-855222762 .background-color-B3{background-color:#6C7086;}
.d2-855222762 .background-color-B4{background-color:#585B70;}
.d2-855222762 .background-color-B5{background-color:#45475A;}
.d2-855222762 .background-color-B6{background-color:#313244;}
.d2-855222762 .background-color-AA2{background-color:#f38BA8;}
.d2-855222762 .background-color-AA4{background-color:#45475A;}
.d2-855222762 .background-color-AA5{background-color:#313244;}
.d2-855222762 .background-color-AB4{background-color:#45475A;}
.d2-855222762 .background-color-AB5{background-color:#313244;}
.d2-855222762 .color-N1{color:#CDD6F4;}
.d2-855222762 .color-N2{color:#BAC2DE;}
.d2-855222762 .color-N3{color:#A6ADC8;}
.d2-855222762 .color-N4{color:#585B70;}
.d2-855222762 .color-N5{color:#45475A;}
.d2-855222762 .color-N6{color:#313244;}
.d2-855222762 .color-N7{color:#1E1E2E;}
.d2-855222762 .color-B1{color:#CBA6f7;}
.d2-855222762 .color-B2{color:#CBA6f7;}
.d2-855222762 .color-B3{color:#6C7086;}
.d2-855222762 .color-B4{color:#585B70;}
.d2-855222762 .color-B5{color:#45475A;}
.d2-855222762 .color-B6{color:#313244;}
.d2-855222762 .color-AA2{color:#f38BA8;}
.d2-855222762 .color-AA4{color:#45475A;}
.d2-855222762 .color-AA5{color:#313244;}
.d2-855222762 .color-AB4{color:#45475A;}
.d2-855222762 .color-AB5{color:#313244;}.appendix text.text{fill:#CDD6F4}.md{--color-fg-default:#CDD6F4;--color-fg-muted:#BAC2DE;--color-fg-subtle:#A6ADC8;--color-canvas-default:#1E1E2E;--color-canvas-subtle:#313244;--color-border-default:#CBA6f7;--color-border-muted:#CBA6f7;--color-neutral-muted:#313244;--color-accent-fg:#CBA6f7;--color-accent-emphasis:#CBA6f7;--color-attention-subtle:#BAC2DE;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-B2{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-B3{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-B4{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-B5{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B6{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-AA2{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-AA4{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-AA5{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-AB4{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-AB5{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N1{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N2{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N5{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N6{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N7{fill:url(#streaks-darker);mix-blend-mode:lighten}.light-code{display: none}.dark-code{display: block}]]></style><g id="x"><g class="shape" ><rect x="1.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape" ><rect x="0.000000" y="166.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -&gt; y)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 27.000000 68.000000 C 27.000000 106.000000 27.000000 126.000000 27.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-855222762)" /></g><mask id="d2-855222762" maskUnits="userSpaceOnUse" x="-101" y="-101" width="256" height="434">
<rect x="-101" y="-101" width="256" height="434" fill="white"></rect>
<rect x="23.500000" y="22.500000" width="8" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="22.500000" y="188.500000" width="9" height="21" fill="rgba(0,0,0,0.75)"></rect>
</mask></svg></svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,883 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.5.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 606 665"><svg id="d2-svg" width="606" height="665" viewBox="-246 -166 606 665"><style type="text/css"><![CDATA[
.d2-1132014075 .text-mono {
font-family: "d2-1132014075-font-mono";
}
@font-face {
font-family: d2-1132014075-font-mono;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-1132014075 .fill-N1{fill:#000410;}
.d2-1132014075 .fill-N2{fill:#0000B8;}
.d2-1132014075 .fill-N3{fill:#9499AB;}
.d2-1132014075 .fill-N4{fill:#CFD2DD;}
.d2-1132014075 .fill-N5{fill:#C3DEF3;}
.d2-1132014075 .fill-N6{fill:#EEF1F8;}
.d2-1132014075 .fill-N7{fill:#FFFFFF;}
.d2-1132014075 .fill-B1{fill:#000410;}
.d2-1132014075 .fill-B2{fill:#0000E4;}
.d2-1132014075 .fill-B3{fill:#5AA4DC;}
.d2-1132014075 .fill-B4{fill:#E7E9EE;}
.d2-1132014075 .fill-B5{fill:#F5F6F9;}
.d2-1132014075 .fill-B6{fill:#FFFFFF;}
.d2-1132014075 .fill-AA2{fill:#008566;}
.d2-1132014075 .fill-AA4{fill:#45BBA5;}
.d2-1132014075 .fill-AA5{fill:#7ACCBD;}
.d2-1132014075 .fill-AB4{fill:#F1C759;}
.d2-1132014075 .fill-AB5{fill:#F9E088;}
.d2-1132014075 .stroke-N1{stroke:#000410;}
.d2-1132014075 .stroke-N2{stroke:#0000B8;}
.d2-1132014075 .stroke-N3{stroke:#9499AB;}
.d2-1132014075 .stroke-N4{stroke:#CFD2DD;}
.d2-1132014075 .stroke-N5{stroke:#C3DEF3;}
.d2-1132014075 .stroke-N6{stroke:#EEF1F8;}
.d2-1132014075 .stroke-N7{stroke:#FFFFFF;}
.d2-1132014075 .stroke-B1{stroke:#000410;}
.d2-1132014075 .stroke-B2{stroke:#0000E4;}
.d2-1132014075 .stroke-B3{stroke:#5AA4DC;}
.d2-1132014075 .stroke-B4{stroke:#E7E9EE;}
.d2-1132014075 .stroke-B5{stroke:#F5F6F9;}
.d2-1132014075 .stroke-B6{stroke:#FFFFFF;}
.d2-1132014075 .stroke-AA2{stroke:#008566;}
.d2-1132014075 .stroke-AA4{stroke:#45BBA5;}
.d2-1132014075 .stroke-AA5{stroke:#7ACCBD;}
.d2-1132014075 .stroke-AB4{stroke:#F1C759;}
.d2-1132014075 .stroke-AB5{stroke:#F9E088;}
.d2-1132014075 .background-color-N1{background-color:#000410;}
.d2-1132014075 .background-color-N2{background-color:#0000B8;}
.d2-1132014075 .background-color-N3{background-color:#9499AB;}
.d2-1132014075 .background-color-N4{background-color:#CFD2DD;}
.d2-1132014075 .background-color-N5{background-color:#C3DEF3;}
.d2-1132014075 .background-color-N6{background-color:#EEF1F8;}
.d2-1132014075 .background-color-N7{background-color:#FFFFFF;}
.d2-1132014075 .background-color-B1{background-color:#000410;}
.d2-1132014075 .background-color-B2{background-color:#0000E4;}
.d2-1132014075 .background-color-B3{background-color:#5AA4DC;}
.d2-1132014075 .background-color-B4{background-color:#E7E9EE;}
.d2-1132014075 .background-color-B5{background-color:#F5F6F9;}
.d2-1132014075 .background-color-B6{background-color:#FFFFFF;}
.d2-1132014075 .background-color-AA2{background-color:#008566;}
.d2-1132014075 .background-color-AA4{background-color:#45BBA5;}
.d2-1132014075 .background-color-AA5{background-color:#7ACCBD;}
.d2-1132014075 .background-color-AB4{background-color:#F1C759;}
.d2-1132014075 .background-color-AB5{background-color:#F9E088;}
.d2-1132014075 .color-N1{color:#000410;}
.d2-1132014075 .color-N2{color:#0000B8;}
.d2-1132014075 .color-N3{color:#9499AB;}
.d2-1132014075 .color-N4{color:#CFD2DD;}
.d2-1132014075 .color-N5{color:#C3DEF3;}
.d2-1132014075 .color-N6{color:#EEF1F8;}
.d2-1132014075 .color-N7{color:#FFFFFF;}
.d2-1132014075 .color-B1{color:#000410;}
.d2-1132014075 .color-B2{color:#0000E4;}
.d2-1132014075 .color-B3{color:#5AA4DC;}
.d2-1132014075 .color-B4{color:#E7E9EE;}
.d2-1132014075 .color-B5{color:#F5F6F9;}
.d2-1132014075 .color-B6{color:#FFFFFF;}
.d2-1132014075 .color-AA2{color:#008566;}
.d2-1132014075 .color-AA4{color:#45BBA5;}
.d2-1132014075 .color-AA5{color:#7ACCBD;}
.d2-1132014075 .color-AB4{color:#F1C759;}
.d2-1132014075 .color-AB5{color:#F9E088;}.appendix text.text{fill:#000410}.md{--color-fg-default:#000410;--color-fg-muted:#0000B8;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#000410;--color-border-muted:#0000E4;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0000E4;--color-accent-emphasis:#0000E4;--color-attention-subtle:#0000B8;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-AA5{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-AB4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-AB5{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><style type="text/css">.md em,
.md dfn {
font-family: "d2-1132014075-font-italic";
}
.md b,
.md strong {
font-family: "d2-1132014075-font-bold";
}
.md code,
.md kbd,
.md pre,
.md samp {
font-family: "d2-1132014075-font-mono";
font-size: 1em;
}
.md {
tab-size: 4;
}
/* variables are provided in d2renderers/d2svg/d2svg.go */
.md {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: var(--color-fg-default);
background-color: transparent; /* we don't want to define the background color */
font-family: "d2-1132014075-font-regular";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.md details,
.md figcaption,
.md figure {
display: block;
}
.md summary {
display: list-item;
}
.md [hidden] {
display: none !important;
}
.md a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.md a:active,
.md a:hover {
outline-width: 0;
}
.md abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.md dfn {
font-style: italic;
}
.md h1 {
margin: 0.67em 0;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.md mark {
background-color: var(--color-attention-subtle);
color: var(--color-text-primary);
}
.md small {
font-size: 90%;
}
.md sub,
.md sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.md sub {
bottom: -0.25em;
}
.md sup {
top: -0.5em;
}
.md img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
}
.md figure {
margin: 1em 40px;
}
.md hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.md input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.md [type="button"],
.md [type="reset"],
.md [type="submit"] {
-webkit-appearance: button;
}
.md [type="button"]::-moz-focus-inner,
.md [type="reset"]::-moz-focus-inner,
.md [type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.md [type="button"]:-moz-focusring,
.md [type="reset"]:-moz-focusring,
.md [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.md [type="checkbox"],
.md [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.md [type="number"]::-webkit-inner-spin-button,
.md [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.md [type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
.md [type="search"]::-webkit-search-cancel-button,
.md [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.md ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.md ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.md a:hover {
text-decoration: underline;
}
.md hr::before {
display: table;
content: "";
}
.md hr::after {
display: table;
clear: both;
content: "";
}
.md table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.md td,
.md th {
padding: 0;
}
.md details summary {
cursor: pointer;
}
.md details:not([open]) > *:not(summary) {
display: none !important;
}
.md kbd {
display: inline-block;
padding: 3px 5px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.md h1,
.md h2,
.md h3,
.md h4,
.md h5,
.md h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 400;
line-height: 1.25;
font-family: "font-semibold";
}
.md h2 {
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.md h3 {
font-size: 1.25em;
}
.md h4 {
font-size: 1em;
}
.md h5 {
font-size: 0.875em;
}
.md h6 {
font-size: 0.85em;
color: var(--color-fg-muted);
}
.md p {
margin-top: 0;
margin-bottom: 10px;
}
.md blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: 0.25em solid var(--color-border-default);
}
.md ul,
.md ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.md ol ol,
.md ul ol {
list-style-type: lower-roman;
}
.md ul ul ol,
.md ul ol ol,
.md ol ul ol,
.md ol ol ol {
list-style-type: lower-alpha;
}
.md dd {
margin-left: 0;
}
.md pre {
margin-top: 0;
margin-bottom: 0;
word-wrap: normal;
}
.md ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.md input::-webkit-outer-spin-button,
.md input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.md::before {
display: table;
content: "";
}
.md::after {
display: table;
clear: both;
content: "";
}
.md > *:first-child {
margin-top: 0 !important;
}
.md > *:last-child {
margin-bottom: 0 !important;
}
.md a:not([href]) {
color: inherit;
text-decoration: none;
}
.md .absent {
color: var(--color-danger-fg);
}
.md .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.md .anchor:focus {
outline: none;
}
.md p,
.md blockquote,
.md ul,
.md ol,
.md dl,
.md table,
.md pre,
.md details {
margin-top: 0;
margin-bottom: 16px;
}
.md blockquote > :first-child {
margin-top: 0;
}
.md blockquote > :last-child {
margin-bottom: 0;
}
.md sup > a::before {
content: "[";
}
.md sup > a::after {
content: "]";
}
.md h1:hover .anchor,
.md h2:hover .anchor,
.md h3:hover .anchor,
.md h4:hover .anchor,
.md h5:hover .anchor,
.md h6:hover .anchor {
text-decoration: none;
}
.md h1 tt,
.md h1 code,
.md h2 tt,
.md h2 code,
.md h3 tt,
.md h3 code,
.md h4 tt,
.md h4 code,
.md h5 tt,
.md h5 code,
.md h6 tt,
.md h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.md ul.no-list,
.md ol.no-list {
padding: 0;
list-style-type: none;
}
.md ol[type="1"] {
list-style-type: decimal;
}
.md ol[type="a"] {
list-style-type: lower-alpha;
}
.md ol[type="i"] {
list-style-type: lower-roman;
}
.md div > ol:not([type]) {
list-style-type: decimal;
}
.md ul ul,
.md ul ol,
.md ol ol,
.md ol ul {
margin-top: 0;
margin-bottom: 0;
}
.md li > p {
margin-top: 16px;
}
.md li + li {
margin-top: 0.25em;
}
.md dl {
padding: 0;
}
.md dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-family: "font-semibold";
}
.md dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.md table th {
font-family: "font-semibold";
}
.md table th,
.md table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.md table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.md table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.md table img {
background-color: transparent;
}
.md img[align="right"] {
padding-left: 20px;
}
.md img[align="left"] {
padding-right: 20px;
}
.md span.frame {
display: block;
overflow: hidden;
}
.md span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.md span.frame span img {
display: block;
float: left;
}
.md span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.md span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.md span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.md span.align-center span img {
margin: 0 auto;
text-align: center;
}
.md span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.md span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.md span.align-right span img {
margin: 0;
text-align: right;
}
.md span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.md span.float-left span {
margin: 13px 0 0;
}
.md span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.md span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.md code,
.md tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.md code br,
.md tt br {
display: none;
}
.md del code {
text-decoration: inherit;
}
.md pre code {
font-size: 100%;
}
.md pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.md .highlight {
margin-bottom: 16px;
}
.md .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.md .highlight pre,
.md pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.md pre code,
.md pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.md .csv-data td,
.md .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.md .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.md .csv-data tr {
border-top: 0;
}
.md .csv-data th {
font-family: "font-semibold";
background: var(--color-canvas-subtle);
border-top: 0;
}
.md .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.md .footnotes ol {
padding-left: 16px;
}
.md .footnotes li {
position: relative;
}
.md .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.md .footnotes li:target {
color: var(--color-fg-default);
}
.md .task-list-item {
list-style-type: none;
}
.md .task-list-item label {
font-weight: 400;
}
.md .task-list-item.enabled label {
cursor: pointer;
}
.md .task-list-item + .task-list-item {
margin-top: 3px;
}
.md .task-list-item .handle {
display: none;
}
.md .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.md .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
</style><style type="text/css"><![CDATA[@keyframes d2Transition-d2-1132014075-0 {
0%, 0.000000% {
opacity: 0;
}
0.000000%, 24.982143% {
opacity: 1;
}
25.000000%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1132014075-1 {
0%, 24.982143% {
opacity: 0;
}
25.000000%, 49.982143% {
opacity: 1;
}
50.000000%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1132014075-2 {
0%, 49.982143% {
opacity: 0;
}
50.000000%, 74.982143% {
opacity: 1;
}
75.000000%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1132014075-3 {
0%, 74.982143% {
opacity: 0;
}
75.000000%, 100.000000% {
opacity: 1;
}
}]]></style><g style="animation: d2Transition-d2-1132014075-0 5600ms infinite" class="d2-1132014075" width="492" height="247" viewBox="-246 -166 492 247"><rect x="-246.000000" y="-166.000000" width="492.000000" height="247.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="0.000000" y="-30.000000" class="text-mono fill-N1" style="text-anchor:middle;font-size:35px">CHICKEN&#39;S PLAN</text></g><mask id="d2-1132014075" maskUnits="userSpaceOnUse" x="-246" y="-166" width="492" height="247">
<rect x="-246" y="-166" width="492" height="247" fill="white"></rect>
<rect x="-145.000000" y="-65.000000" width="290" height="45" fill="rgba(0,0,0,0.75)"></rect>
</mask></g><g style="animation: d2Transition-d2-1132014075-1 5600ms infinite" class="d2-1132014075" width="492" height="333" viewBox="-160 -166 492 333"><rect x="-160.000000" y="-166.000000" width="492.000000" height="333.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="Approach road"><g class="shape" ><rect x="0.000000" y="0.000000" width="171.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="85.500000" y="38.500000" class="text-mono fill-N1" style="text-anchor:middle;font-size:16px">APPROACH ROAD</text></g><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="86.000000" y="-30.000000" class="text-mono fill-N1" style="text-anchor:middle;font-size:35px">CHICKEN&#39;S PLAN</text></g><mask id="d2-3467174386" maskUnits="userSpaceOnUse" x="-160" y="-166" width="492" height="333">
<rect x="-160" y="-166" width="492" height="333" fill="white"></rect>
<rect x="22.500000" y="22.500000" width="126" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="-59.000000" y="-65.000000" width="290" height="45" fill="rgba(0,0,0,0.75)"></rect>
</mask></g><g style="animation: d2Transition-d2-1132014075-2 5600ms infinite" class="d2-1132014075" width="492" height="499" viewBox="-160 -166 492 499"><rect x="-160.000000" y="-166.000000" width="492.000000" height="499.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="Approach road"><g class="shape" ><rect x="0.000000" y="0.000000" width="171.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="85.500000" y="38.500000" class="text-mono fill-N1" style="text-anchor:middle;font-size:16px">APPROACH ROAD</text></g><g id="Cross road"><g class="shape" ><rect x="15.000000" y="166.000000" width="142.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="86.000000" y="204.500000" class="text-mono fill-N1" style="text-anchor:middle;font-size:16px">CROSS ROAD</text></g><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="86.000000" y="-30.000000" class="text-mono fill-N1" style="text-anchor:middle;font-size:35px">CHICKEN&#39;S PLAN</text></g><g id="(Approach road -&gt; Cross road)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 85.500000 68.000000 C 85.500000 106.000000 85.500000 126.000000 85.500000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2945557013)" /></g><mask id="d2-2945557013" maskUnits="userSpaceOnUse" x="-160" y="-166" width="492" height="499">
<rect x="-160" y="-166" width="492" height="499" fill="white"></rect>
<rect x="22.500000" y="22.500000" width="126" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="37.500000" y="188.500000" width="97" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="-59.000000" y="-65.000000" width="290" height="45" fill="rgba(0,0,0,0.75)"></rect>
</mask></g><g style="animation: d2Transition-d2-1132014075-3 5600ms infinite" class="d2-1132014075" width="492" height="665" viewBox="-132 -166 492 665"><rect x="-132.000000" y="-166.000000" width="492.000000" height="665.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="Approach road"><g class="shape" ><rect x="29.000000" y="0.000000" width="171.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="114.500000" y="38.500000" class="text-mono fill-N1" style="text-anchor:middle;font-size:16px">APPROACH ROAD</text></g><g id="Cross road"><g class="shape" ><rect x="43.000000" y="166.000000" width="142.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="114.000000" y="204.500000" class="text-mono fill-N1" style="text-anchor:middle;font-size:16px">CROSS ROAD</text></g><g id="Make you wonder why"><g class="shape" ><rect x="0.000000" y="332.000000" width="228.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="114.000000" y="370.500000" class="text-mono fill-N1" style="text-anchor:middle;font-size:16px">MAKE YOU WONDER WHY</text></g><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="114.000000" y="-30.000000" class="text-mono fill-N1" style="text-anchor:middle;font-size:35px">CHICKEN&#39;S PLAN</text></g><g id="(Approach road -&gt; Cross road)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 114.000000 68.000000 C 114.000000 106.000000 114.000000 126.000000 114.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2467861000)" /></g><g id="(Cross road -&gt; Make you wonder why)[0]"><path d="M 114.000000 234.000000 C 114.000000 272.000000 114.000000 292.000000 114.000000 328.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2467861000)" /></g><mask id="d2-2467861000" maskUnits="userSpaceOnUse" x="-132" y="-166" width="492" height="665">
<rect x="-132" y="-166" width="492" height="665" fill="white"></rect>
<rect x="51.500000" y="22.500000" width="126" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="65.500000" y="188.500000" width="97" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="22.500000" y="354.500000" width="183" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="-31.000000" y="-65.000000" width="290" height="45" fill="rgba(0,0,0,0.75)"></rect>
</mask></g></svg></svg>

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -15,6 +15,7 @@ import (
"oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
@ -87,7 +88,7 @@ type testCase struct {
dagreFeatureError string dagreFeatureError string
elkFeatureError string elkFeatureError string
expErr string expErr string
themeID int64 themeID *int64
} }
func runa(t *testing.T, tcs []testCase) { 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) { func serde(t *testing.T, tc testCase, ruler *textmeasure.Ruler) {
ctx := context.Background() ctx := context.Background()
ctx = log.WithTB(ctx, t, nil) 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, UTF16: false,
}) })
trequire.Nil(t, err) trequire.Nil(t, err)
@ -146,28 +147,39 @@ func run(t *testing.T, tc testCase) {
layoutsTested = append(layoutsTested, "elk") 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 { for _, layoutName := range layoutsTested {
var layout func(context.Context, *d2graph.Graph) error
var plugin d2plugin.Plugin var plugin d2plugin.Plugin
if layoutName == "dagre" { if layoutName == "dagre" {
layout = d2dagrelayout.DefaultLayout
plugin = &d2plugin.DagrePlugin plugin = &d2plugin.DagrePlugin
} else if layoutName == "elk" { } else if layoutName == "elk" {
// If measured texts exists, we are specifically exercising text measurements, no need to run on both layouts // If measured texts exists, we are specifically exercising text measurements, no need to run on both layouts
if tc.mtexts != nil { if tc.mtexts != nil {
continue continue
} }
layout = d2elklayout.DefaultLayout
plugin = &d2plugin.ELKPlugin plugin = &d2plugin.ELKPlugin
} }
diagram, g, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ compileOpts := &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
MeasuredTexts: tc.mtexts, MeasuredTexts: tc.mtexts,
Layout: layout, Layout: go2.Pointer(layoutName),
ThemeID: tc.themeID, 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 != "" { if tc.expErr != "" {
assert.Error(t, err) assert.Error(t, err)
assert.ErrorString(t, err, tc.expErr) 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) dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestE2E/"), layoutName)
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") 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 { if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
masterID, err := diagram.HashID() masterID, err := diagram.HashID()
assert.Success(t, err) assert.Success(t, err)

View file

@ -11,7 +11,7 @@ func testThemes(t *testing.T) {
tcs := []testCase{ tcs := []testCase{
{ {
name: "dark terrastruct flagship", name: "dark terrastruct flagship",
themeID: d2themescatalog.DarkFlagshipTerrastruct.ID, themeID: &d2themescatalog.DarkFlagshipTerrastruct.ID,
script: ` script: `
network: { network: {
cell tower: { cell tower: {
@ -118,7 +118,7 @@ ex: |tex
}, },
{ {
name: "terminal", name: "terminal",
themeID: d2themescatalog.Terminal.ID, themeID: &d2themescatalog.Terminal.ID,
script: ` script: `
network: { network: {
cell tower: { cell tower: {
@ -225,7 +225,7 @@ ex: |tex
}, },
{ {
name: "terminal_grayscale", name: "terminal_grayscale",
themeID: d2themescatalog.TerminalGrayscale.ID, themeID: &d2themescatalog.TerminalGrayscale.ID,
script: ` script: `
network: { network: {
cell tower: { cell tower: {
@ -279,7 +279,7 @@ network.data processor -> api server
}, },
{ {
name: "origami", name: "origami",
themeID: d2themescatalog.Origami.ID, themeID: &d2themescatalog.Origami.ID,
script: ` script: `
network: 通信網 { network: 通信網 {
cell tower: { cell tower: {

View file

@ -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
}

View file

@ -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\""
}
]
}
}

View file

@ -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"
}
]
}
}