layout with grids
This commit is contained in:
parent
5b22382cfd
commit
d139eeadad
11 changed files with 493 additions and 4 deletions
|
|
@ -362,6 +362,32 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
attrs.Constraint.Value = scalar.ScalarString()
|
||||
attrs.Constraint.MapKey = f.LastPrimaryKey()
|
||||
case "rows":
|
||||
v, err := strconv.Atoi(scalar.ScalarString())
|
||||
if err != nil {
|
||||
c.errorf(scalar, "non-integer rows %#v: %s", scalar.ScalarString(), err)
|
||||
return
|
||||
}
|
||||
if v < 0 {
|
||||
c.errorf(scalar, "rows must be a non-negative integer: %#v", scalar.ScalarString())
|
||||
return
|
||||
}
|
||||
attrs.Rows = &d2graph.Scalar{}
|
||||
attrs.Rows.Value = scalar.ScalarString()
|
||||
attrs.Rows.MapKey = f.LastPrimaryKey()
|
||||
case "columns":
|
||||
v, err := strconv.Atoi(scalar.ScalarString())
|
||||
if err != nil {
|
||||
c.errorf(scalar, "non-integer columns %#v: %s", scalar.ScalarString(), err)
|
||||
return
|
||||
}
|
||||
if v < 0 {
|
||||
c.errorf(scalar, "columns must be a non-negative integer: %#v", scalar.ScalarString())
|
||||
return
|
||||
}
|
||||
attrs.Columns = &d2graph.Scalar{}
|
||||
attrs.Columns.Value = scalar.ScalarString()
|
||||
attrs.Columns.MapKey = f.LastPrimaryKey()
|
||||
}
|
||||
|
||||
if attrs.Link != nil && attrs.Tooltip != nil {
|
||||
|
|
|
|||
|
|
@ -2268,6 +2268,26 @@ obj {
|
|||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
|
||||
},
|
||||
{
|
||||
name: "grid",
|
||||
text: `hey: {
|
||||
rows: 200
|
||||
columns: 230
|
||||
}
|
||||
`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
tassert.Equal(t, "200", g.Objects[0].Attributes.Rows.Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "grid_negative",
|
||||
text: `hey: {
|
||||
rows: 200
|
||||
columns: -200
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:11: columns must be a non-negative integer: "-200"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2exporter"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2grid"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
|
|
@ -231,7 +232,7 @@ func run(t *testing.T, tc testCase) {
|
|||
err = g.SetDimensions(nil, ruler, nil)
|
||||
assert.JSON(t, nil, err)
|
||||
|
||||
err = d2sequence.Layout(ctx, g, d2dagrelayout.DefaultLayout)
|
||||
err = d2sequence.Layout(ctx, g, d2grid.Layout(ctx, g, d2dagrelayout.DefaultLayout))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package d2graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
|
@ -67,6 +68,8 @@ func (g *Graph) RootBoard() *Graph {
|
|||
return g
|
||||
}
|
||||
|
||||
type LayoutGraph func(context.Context, *Graph) error
|
||||
|
||||
// TODO consider having different Scalar types
|
||||
// Right now we'll hold any types in Value and just convert, e.g. floats
|
||||
type Scalar struct {
|
||||
|
|
@ -129,6 +132,9 @@ type Attributes struct {
|
|||
|
||||
Direction Scalar `json:"direction"`
|
||||
Constraint Scalar `json:"constraint"`
|
||||
|
||||
Rows *Scalar `json:"rows,omitempty"`
|
||||
Columns *Scalar `json:"columns,omitempty"`
|
||||
}
|
||||
|
||||
// TODO references at the root scope should have their Scope set to root graph AST
|
||||
|
|
@ -1534,6 +1540,8 @@ var SimpleReservedKeywords = map[string]struct{}{
|
|||
"direction": {},
|
||||
"top": {},
|
||||
"left": {},
|
||||
"rows": {},
|
||||
"columns": {},
|
||||
}
|
||||
|
||||
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
|
||||
|
|
|
|||
6
d2graph/grid.go
Normal file
6
d2graph/grid.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package d2graph
|
||||
|
||||
func (obj *Object) IsGrid() bool {
|
||||
return obj != nil && obj.Attributes != nil && len(obj.ChildrenArray) != 0 &&
|
||||
(obj.Attributes.Rows != nil || obj.Attributes.Columns != nil)
|
||||
}
|
||||
223
d2layouts/d2grid/layout.go
Normal file
223
d2layouts/d2grid/layout.go
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
package d2grid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
const CONTAINER_PADDING = 60.
|
||||
const HORIZONTAL_PAD = 40.
|
||||
const VERTICAL_PAD = 40.
|
||||
|
||||
type grid struct {
|
||||
root *d2graph.Object
|
||||
nodes []*d2graph.Object
|
||||
rows int
|
||||
columns int
|
||||
|
||||
width float64
|
||||
height float64
|
||||
}
|
||||
|
||||
func newGrid(root *d2graph.Object) *grid {
|
||||
g := grid{root: root, nodes: root.ChildrenArray}
|
||||
if root.Attributes.Rows != nil {
|
||||
g.rows, _ = strconv.Atoi(root.Attributes.Rows.Value)
|
||||
}
|
||||
if root.Attributes.Columns != nil {
|
||||
g.columns, _ = strconv.Atoi(root.Attributes.Columns.Value)
|
||||
}
|
||||
|
||||
// compute exact row/column count based on values entered
|
||||
// TODO consider making this based on node dimensions
|
||||
if g.rows == 0 {
|
||||
// set rows based on number of columns
|
||||
if g.columns == 0 {
|
||||
// 0,0: put everything in one row
|
||||
g.rows = 1
|
||||
g.columns = len(g.nodes)
|
||||
} else {
|
||||
g.rows = len(g.nodes) / g.columns
|
||||
if len(g.nodes)%g.columns != 0 {
|
||||
g.rows++
|
||||
}
|
||||
}
|
||||
} else if g.columns == 0 {
|
||||
// set columns based on number of rows
|
||||
g.columns = len(g.nodes) / g.rows
|
||||
if len(g.nodes)%g.rows != 0 {
|
||||
g.columns++
|
||||
}
|
||||
} else {
|
||||
// rows and columns specified (add more rows if needed)
|
||||
capacity := g.rows * g.columns
|
||||
for capacity < len(g.nodes) {
|
||||
g.rows++
|
||||
capacity += g.columns
|
||||
}
|
||||
}
|
||||
|
||||
return &g
|
||||
}
|
||||
|
||||
func (g *grid) shift(dx, dy float64) {
|
||||
for _, obj := range g.nodes {
|
||||
obj.TopLeft.X += dx
|
||||
obj.TopLeft.Y += dy
|
||||
}
|
||||
}
|
||||
|
||||
// Layout runs the grid layout on containers with rows/columns
|
||||
// Note: children are not allowed edges or descendants
|
||||
//
|
||||
// 1. Traverse graph from root, skip objects with no rows/columns
|
||||
// 2. Construct a grid with the container children
|
||||
// 3. Remove the children from the main graph
|
||||
// 4. Run grid layout
|
||||
// 5. Set the resulting dimensions to the main graph shape
|
||||
// 6. Run core layouts (without grid children)
|
||||
// 7. Put grid children back in correct location
|
||||
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
|
||||
return func(ctx context.Context, g *d2graph.Graph) error {
|
||||
grids, objectOrder, err := withoutGrids(ctx, g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.Root.IsGrid() {
|
||||
g.Root.TopLeft = geo.NewPoint(0, 0)
|
||||
} else if err := layout(ctx, g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup(g, grids, objectOrder)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*grid, error) {
|
||||
grid := newGrid(obj)
|
||||
|
||||
// position nodes
|
||||
cursor := geo.NewPoint(0, 0)
|
||||
maxWidth := 0.
|
||||
for i := 0; i < grid.rows; i++ {
|
||||
maxHeight := 0.
|
||||
for j := 0; j < grid.columns; j++ {
|
||||
n := grid.nodes[i*grid.columns+j]
|
||||
n.TopLeft = cursor.Copy()
|
||||
cursor.X += n.Width + HORIZONTAL_PAD
|
||||
maxHeight = math.Max(maxHeight, n.Height)
|
||||
}
|
||||
maxWidth = math.Max(maxWidth, cursor.X-HORIZONTAL_PAD)
|
||||
cursor.X = 0
|
||||
cursor.Y += float64(maxHeight) + VERTICAL_PAD
|
||||
}
|
||||
grid.width = maxWidth
|
||||
grid.height = cursor.Y - VERTICAL_PAD
|
||||
|
||||
// position labels and icons
|
||||
for _, n := range grid.nodes {
|
||||
if n.Attributes.Icon != nil {
|
||||
n.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
n.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
} else {
|
||||
n.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
}
|
||||
|
||||
return grid, nil
|
||||
}
|
||||
|
||||
func withoutGrids(ctx context.Context, g *d2graph.Graph) (idToGrid map[string]*grid, objectOrder map[string]int, err error) {
|
||||
toRemove := make(map[*d2graph.Object]struct{})
|
||||
grids := make(map[string]*grid)
|
||||
|
||||
if len(g.Objects) > 0 {
|
||||
queue := make([]*d2graph.Object, 1, len(g.Objects))
|
||||
queue[0] = g.Root
|
||||
for len(queue) > 0 {
|
||||
obj := queue[0]
|
||||
queue = queue[1:]
|
||||
if len(obj.ChildrenArray) == 0 {
|
||||
continue
|
||||
}
|
||||
if !obj.IsGrid() {
|
||||
queue = append(queue, obj.ChildrenArray...)
|
||||
continue
|
||||
}
|
||||
|
||||
grid, err := layoutGrid(g, obj)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
obj.Children = make(map[string]*d2graph.Object)
|
||||
obj.ChildrenArray = nil
|
||||
obj.Box = geo.NewBox(nil, grid.width+CONTAINER_PADDING*2, grid.height+CONTAINER_PADDING*2)
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
grids[obj.AbsID()] = grid
|
||||
|
||||
for _, node := range grid.nodes {
|
||||
toRemove[node] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectOrder = make(map[string]int)
|
||||
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
|
||||
for i, obj := range g.Objects {
|
||||
objectOrder[obj.AbsID()] = i
|
||||
if _, exists := toRemove[obj]; !exists {
|
||||
layoutObjects = append(layoutObjects, obj)
|
||||
}
|
||||
}
|
||||
g.Objects = layoutObjects
|
||||
|
||||
return grids, objectOrder, nil
|
||||
}
|
||||
|
||||
// cleanup restores the graph after the core layout engine finishes
|
||||
// - translating the grid to its position placed by the core layout engine
|
||||
// - restore the children of the grid
|
||||
// - sorts objects to their original graph order
|
||||
func cleanup(g *d2graph.Graph, grids map[string]*grid, objectsOrder map[string]int) {
|
||||
var objects []*d2graph.Object
|
||||
if g.Root.IsGrid() {
|
||||
objects = []*d2graph.Object{g.Root}
|
||||
} else {
|
||||
objects = g.Objects
|
||||
}
|
||||
for _, obj := range objects {
|
||||
if _, exists := grids[obj.AbsID()]; !exists {
|
||||
continue
|
||||
}
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
sd := grids[obj.AbsID()]
|
||||
|
||||
// shift the grid from (0, 0)
|
||||
sd.shift(
|
||||
obj.TopLeft.X+CONTAINER_PADDING,
|
||||
obj.TopLeft.Y+CONTAINER_PADDING,
|
||||
)
|
||||
|
||||
obj.Children = make(map[string]*d2graph.Object)
|
||||
obj.ChildrenArray = make([]*d2graph.Object, 0)
|
||||
for _, child := range sd.nodes {
|
||||
obj.Children[child.ID] = child
|
||||
obj.ChildrenArray = append(obj.ChildrenArray, child)
|
||||
}
|
||||
|
||||
g.Objects = append(g.Objects, grids[obj.AbsID()].nodes...)
|
||||
}
|
||||
|
||||
sort.SliceStable(g.Objects, func(i, j int) bool {
|
||||
return objectsOrder[g.Objects[i].AbsID()] < objectsOrder[g.Objects[j].AbsID()]
|
||||
})
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]
|
|||
// 5. Set the resulting dimensions to the main graph shape
|
||||
// 6. Run core layouts (still without sequence diagram innards)
|
||||
// 7. Put back sequence diagram innards in correct location
|
||||
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
|
||||
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
|
||||
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2exporter"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2grid"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2near"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
|
|
@ -70,7 +71,9 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
|
|||
|
||||
constantNears := d2near.WithoutConstantNears(ctx, g)
|
||||
|
||||
err = d2sequence.Layout(ctx, g, coreLayout)
|
||||
layoutWithGrids := d2grid.Layout(ctx, g, coreLayout)
|
||||
|
||||
err = d2sequence.Layout(ctx, g, layoutWithGrids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -110,7 +113,7 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
|
|||
return d, nil
|
||||
}
|
||||
|
||||
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {
|
||||
func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
|
||||
if opts.Layout != nil {
|
||||
return opts.Layout, nil
|
||||
} else if os.Getenv("D2_LAYOUT") == "dagre" {
|
||||
|
|
|
|||
|
|
@ -314,6 +314,16 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
|
|||
attrs.Left.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "rows":
|
||||
if attrs.Rows != nil && attrs.Rows.MapKey != nil {
|
||||
attrs.Rows.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "columns":
|
||||
if attrs.Columns != nil && attrs.Columns.MapKey != nil {
|
||||
attrs.Columns.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "source-arrowhead", "target-arrowhead":
|
||||
if reservedKey == "source-arrowhead" {
|
||||
attrs = edge.SrcArrowhead
|
||||
|
|
|
|||
180
testdata/d2compiler/TestCompile/grid.exp.json
generated
vendored
Normal file
180
testdata/d2compiler/TestCompile/grid.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
{
|
||||
"graph": {
|
||||
"name": "",
|
||||
"isFolderOnly": false,
|
||||
"ast": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:0:0-4:0:34",
|
||||
"nodes": [
|
||||
{
|
||||
"map_key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:0:0-3:1:33",
|
||||
"key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:0:0-0:3:3",
|
||||
"path": [
|
||||
{
|
||||
"unquoted_string": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:0:0-0:3:3",
|
||||
"value": [
|
||||
{
|
||||
"string": "hey",
|
||||
"raw_string": "hey"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"primary": {},
|
||||
"value": {
|
||||
"map": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:5:5-3:0:32",
|
||||
"nodes": [
|
||||
{
|
||||
"map_key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,1:1:8-1:10:17",
|
||||
"key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,1:1:8-1:5:12",
|
||||
"path": [
|
||||
{
|
||||
"unquoted_string": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,1:1:8-1:5:12",
|
||||
"value": [
|
||||
{
|
||||
"string": "rows",
|
||||
"raw_string": "rows"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"primary": {},
|
||||
"value": {
|
||||
"number": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,1:7:14-1:10:17",
|
||||
"raw": "200",
|
||||
"value": "200"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"map_key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,2:1:19-2:13:31",
|
||||
"key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,2:1:19-2:8:26",
|
||||
"path": [
|
||||
{
|
||||
"unquoted_string": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,2:1:19-2:8:26",
|
||||
"value": [
|
||||
{
|
||||
"string": "columns",
|
||||
"raw_string": "columns"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"primary": {},
|
||||
"value": {
|
||||
"number": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,2:10:28-2:13:31",
|
||||
"raw": "230",
|
||||
"value": "230"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"root": {
|
||||
"id": "",
|
||||
"id_val": "",
|
||||
"label_dimensions": {
|
||||
"width": 0,
|
||||
"height": 0
|
||||
},
|
||||
"attributes": {
|
||||
"label": {
|
||||
"value": ""
|
||||
},
|
||||
"style": {},
|
||||
"near_key": null,
|
||||
"shape": {
|
||||
"value": ""
|
||||
},
|
||||
"direction": {
|
||||
"value": ""
|
||||
},
|
||||
"constraint": {
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"zIndex": 0
|
||||
},
|
||||
"edges": null,
|
||||
"objects": [
|
||||
{
|
||||
"id": "hey",
|
||||
"id_val": "hey",
|
||||
"label_dimensions": {
|
||||
"width": 0,
|
||||
"height": 0
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"key": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:0:0-0:3:3",
|
||||
"path": [
|
||||
{
|
||||
"unquoted_string": {
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid.d2,0:0:0-0:3:3",
|
||||
"value": [
|
||||
{
|
||||
"string": "hey",
|
||||
"raw_string": "hey"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"key_path_index": 0,
|
||||
"map_key_edge_index": -1
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"label": {
|
||||
"value": "hey"
|
||||
},
|
||||
"style": {},
|
||||
"near_key": null,
|
||||
"shape": {
|
||||
"value": "rectangle"
|
||||
},
|
||||
"direction": {
|
||||
"value": ""
|
||||
},
|
||||
"constraint": {
|
||||
"value": ""
|
||||
},
|
||||
"rows": {
|
||||
"value": "200"
|
||||
},
|
||||
"columns": {
|
||||
"value": "230"
|
||||
}
|
||||
},
|
||||
"zIndex": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"err": null
|
||||
}
|
||||
12
testdata/d2compiler/TestCompile/grid_negative.exp.json
generated
vendored
Normal file
12
testdata/d2compiler/TestCompile/grid_negative.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"graph": null,
|
||||
"err": {
|
||||
"ioerr": null,
|
||||
"errs": [
|
||||
{
|
||||
"range": "d2/testdata/d2compiler/TestCompile/grid_negative.d2,2:10:28-2:14:32",
|
||||
"errmsg": "d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:11: columns must be a non-negative integer: \"-200\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue