diff --git a/.gitignore b/.gitignore index 7af7b3572..e9e020ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.got.svg e2e_report.html bin +out diff --git a/ci/cov.sh b/ci/cov.sh new file mode 100755 index 000000000..63d5bb812 --- /dev/null +++ b/ci/cov.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." +. ./ci/sub/lib.sh + +main() { + if [ "$*" = "" ]; then + set ./... + fi + + mkdir -p out + capcode ./ci/test.sh -covermode=atomic -coverprofile=out/cov.prof "$@" + go tool cover -html=out/cov.prof -o=out/cov.html + go tool cover -func=out/cov.prof | grep '^total:' \ + | sed 's#^total:.*(statements)[[:space:]]*\([0-9.%]*\)#TOTAL:\t\1#' + return "$code" +} + +main "$@" diff --git a/ci/e2ereport.sh b/ci/e2ereport.sh index ebf45b912..02500ac58 100755 --- a/ci/e2ereport.sh +++ b/ci/e2ereport.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -export REPORT_OUTPUT="out/e2e_report.html" +export REPORT_OUTPUT="./e2etests/out/e2e_report.html" rm -f $REPORT_OUTPUT export E2E_REPORT=1 diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 72587383b..469a5ce39 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -6,6 +6,15 @@ - `d2 fmt` accepts multiple files to be formatted [#718](https://github.com/terrastruct/d2/issues/718) +- You can now use the reserved keywords `layers`/`scenarios`/`steps` to define diagrams + with multiple levels of abstractions. [#714](https://github.com/terrastruct/d2/pull/714) + Docs to come soon + - [#416](https://github.com/terrastruct/d2/issues/416) was also fixed so you can no + longer use keywords intended for use under `style` outside and vice versa. e.g. + `obj.style.shape` and `obj.double-border` are now illegal. The correct uses are + `obj.shape` and `obj.style.double-border`. + - Many other minor compiler bugs were fixed. + #### Improvements 🧹 - Code snippets use bold and italic font styles as determined by highlighter [#710](https://github.com/terrastruct/d2/issues/710), [#741](https://github.com/terrastruct/d2/issues/741) diff --git a/ci/sub b/ci/sub index 8ac704818..2009cdd52 160000 --- a/ci/sub +++ b/ci/sub @@ -1 +1 @@ -Subproject commit 8ac704818b5d7ab519e4b87caf5eb79716493709 +Subproject commit 2009cdd523e00cc2e9b8ba804095f71ca70d5671 diff --git a/d2ast/d2ast.go b/d2ast/d2ast.go index 2ffb448f1..85d6f8620 100644 --- a/d2ast/d2ast.go +++ b/d2ast/d2ast.go @@ -1,3 +1,5 @@ +// TODO: Remove boxes and cleanup like d2ir +// // d2ast implements the d2 language's abstract syntax tree. // // Special characters to think about in parser: @@ -149,6 +151,10 @@ func (r *Range) UnmarshalText(b []byte) (err error) { return r.End.UnmarshalText(end) } +func (r Range) Before(r2 Range) bool { + return r.Start.Before(r2.Start) +} + // Position represents a line:column and byte position in a file. // // note: Line and Column are zero indexed. @@ -257,6 +263,10 @@ func (p Position) SubtractString(s string, byUTF16 bool) Position { return p } +func (p Position) Before(p2 Position) bool { + return p.Byte < p2.Byte +} + // MapNode is implemented by nodes that may be children of Maps. type MapNode interface { Node @@ -402,7 +412,7 @@ func (s *SingleQuotedString) scalar() {} func (s *BlockString) scalar() {} // TODO: mistake, move into parse.go -func (n *Null) ScalarString() string { return n.Type() } +func (n *Null) ScalarString() string { return "" } func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) } func (n *Number) ScalarString() string { return n.Raw } func (s *UnquotedString) ScalarString() string { @@ -648,6 +658,21 @@ type KeyPath struct { Path []*StringBox `json:"path"` } +func MakeKeyPath(a []string) *KeyPath { + kp := &KeyPath{} + for _, el := range a { + kp.Path = append(kp.Path, MakeValueBox(RawString(el, true)).StringBox()) + } + return kp +} + +func (kp *KeyPath) IDA() (ida []string) { + for _, el := range kp.Path { + ida = append(ida, el.Unbox().ScalarString()) + } + return ida +} + type Edge struct { Range Range `json:"range"` @@ -729,6 +754,37 @@ type ArrayNodeBox struct { Map *Map `json:"map,omitempty"` } +func MakeArrayNodeBox(an ArrayNode) ArrayNodeBox { + var ab ArrayNodeBox + switch an := an.(type) { + case *Comment: + ab.Comment = an + case *BlockComment: + ab.BlockComment = an + case *Substitution: + ab.Substitution = an + case *Null: + ab.Null = an + case *Boolean: + ab.Boolean = an + case *Number: + ab.Number = an + case *UnquotedString: + ab.UnquotedString = an + case *DoubleQuotedString: + ab.DoubleQuotedString = an + case *SingleQuotedString: + ab.SingleQuotedString = an + case *BlockString: + ab.BlockString = an + case *Array: + ab.Array = an + case *Map: + ab.Map = an + } + return ab +} + func (ab ArrayNodeBox) Unbox() ArrayNode { switch { case ab.Comment != nil: diff --git a/d2chaos/d2chaos.go b/d2chaos/d2chaos.go index 7729454a8..c26ea3c80 100644 --- a/d2chaos/d2chaos.go +++ b/d2chaos/d2chaos.go @@ -18,10 +18,11 @@ import ( func GenDSL(maxi int) (_ string, err error) { gs := &dslGenState{ rand: mathrand.New(mathrand.NewSource(time.Now().UnixNano())), - g: d2graph.NewGraph(&d2ast.Map{}), + g: d2graph.NewGraph(), nodeShapes: make(map[string]string), nodeContainer: make(map[string]string), } + gs.g.AST = &d2ast.Map{} err = gs.gen(maxi) if err != nil { return "", err diff --git a/d2compiler/compile.go b/d2compiler/compile.go index aa3c0adfa..584dd20af 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -1,7 +1,6 @@ package d2compiler import ( - "errors" "fmt" "io" "net/url" @@ -13,12 +12,11 @@ import ( "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2ir" "oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2target" ) -// TODO: should Parse even be exported? guess not. IR should contain list of files and -// their AST. type CompileOptions struct { UTF16 bool } @@ -28,370 +26,180 @@ func Compile(path string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph opts = &CompileOptions{} } - var pe d2parser.ParseError - ast, err := d2parser.Parse(path, r, &d2parser.ParseOptions{ UTF16: opts.UTF16, }) if err != nil { - if !errors.As(err, &pe) { - return nil, err - } + return nil, err } - return compileAST(path, pe, ast) + ir, err := d2ir.Compile(ast) + if err != nil { + return nil, err + } + + g, err := compileIR(ast, ir) + if err != nil { + return nil, err + } + g.SortObjectsByAST() + g.SortEdgesByAST() + return g, nil } -func compileAST(path string, pe d2parser.ParseError, ast *d2ast.Map) (*d2graph.Graph, error) { - g := d2graph.NewGraph(ast) - - c := &compiler{ - path: path, - err: pe, - } - - c.compileKeys(g.Root, ast) - if len(c.err.Errors) == 0 { - c.validateKeys(g.Root, ast) - } - c.compileEdges(g.Root, ast) - // TODO: simplify removeContainer by running before compileEdges - c.compileShapes(g.Root) - c.validateNear(g) +func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) { + c := &compiler{} + g := d2graph.NewGraph() + g.AST = ast + c.compileBoard(g, m) if len(c.err.Errors) > 0 { return nil, c.err } return g, nil } +func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph { + ir = ir.Copy(nil).(*d2ir.Map) + // c.preprocessSeqDiagrams(ir) + c.compileMap(g.Root, ir) + if len(c.err.Errors) == 0 { + c.validateKeys(g.Root, ir) + } + c.validateNear(g) + + c.compileBoardsField(g, ir, "layers") + c.compileBoardsField(g, ir, "scenarios") + c.compileBoardsField(g, ir, "steps") + return g +} + +func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) { + layers := ir.GetField(fieldName) + if layers.Map() == nil { + return + } + for _, f := range layers.Map().Fields { + if f.Map() == nil { + continue + } + if g.GetBoard(f.Name) != nil { + c.errorf(f.References[0].AST(), "board name %v already used by another board", f.Name) + continue + } + g2 := d2graph.NewGraph() + g2.AST = g.AST + c.compileBoard(g2, f.Map()) + g2.Name = f.Name + switch fieldName { + case "layers": + g.Layers = append(g.Layers, g2) + case "scenarios": + g.Scenarios = append(g.Scenarios, g2) + case "steps": + g.Steps = append(g.Steps, g2) + } + } +} + type compiler struct { - path string - err d2parser.ParseError + inEdgeGroup bool + err d2parser.ParseError } -func (c *compiler) errorf(start d2ast.Position, end d2ast.Position, f string, v ...interface{}) { - r := d2ast.Range{ - Path: c.path, - Start: start, - End: end, +func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { + c.err.Errors = append(c.err.Errors, d2parser.Errorf(n, f, v...).(d2ast.Error)) +} + +func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) { + shape := m.GetField("shape") + if shape != nil { + c.compileField(obj, shape) } - f = "%v: " + f - v = append([]interface{}{r}, v...) - c.err.Errors = append(c.err.Errors, d2ast.Error{ - Range: r, - Message: fmt.Sprintf(f, v...), - }) -} - -func (c *compiler) compileKeys(obj *d2graph.Object, m *d2ast.Map) { - for _, n := range m.Nodes { - if n.MapKey != nil && n.MapKey.Key != nil && len(n.MapKey.Edges) == 0 { - c.compileKey(obj, m, n.MapKey) + for _, f := range m.Fields { + if f.Name == "shape" { + continue } + if _, ok := d2graph.BoardKeywords[f.Name]; ok { + continue + } + c.compileField(obj, f) + } + + switch obj.Attributes.Shape.Value { + case d2target.ShapeClass: + c.compileClass(obj) + case d2target.ShapeSQLTable: + c.compileSQLTable(obj) + } + + for _, e := range m.Edges { + c.compileEdge(obj, e) } } -func (c *compiler) compileEdges(obj *d2graph.Object, m *d2ast.Map) { - for _, n := range m.Nodes { - if n.MapKey != nil { - if len(n.MapKey.Edges) > 0 { - obj := obj - if n.MapKey.Key != nil { - ida := d2graph.Key(n.MapKey.Key) - parent, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, obj) - if err != nil { - c.errorf(n.MapKey.Range.Start, n.MapKey.Range.End, err.Error()) - return - } - unresolvedObj := obj - obj = parent.EnsureChild(resolvedIDA) +func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) { + keyword := strings.ToLower(f.Name) + _, isStyleReserved := d2graph.StyleKeywords[keyword] + if isStyleReserved { + c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name) + return + } + _, isReserved := d2graph.SimpleReservedKeywords[keyword] + if isReserved { + c.compileReserved(obj.Attributes, f) + return + } else if f.Name == "style" { + if f.Map() == nil { + return + } + c.compileStyle(obj.Attributes, f.Map()) + if obj.Attributes.Style.Animated != nil { + c.errorf(obj.Attributes.Style.Animated.MapKey, `key "animated" can only be applied to edges`) + } + return + } - parent.AppendReferences(ida, d2graph.Reference{ - Key: n.MapKey.Key, + obj = obj.EnsureChild(d2graphIDA([]string{f.Name})) + if f.Primary() != nil { + c.compileLabel(obj.Attributes, f) + } + if f.Map() != nil { + c.compileMap(obj, f.Map()) + } - MapKey: n.MapKey, - Scope: m, - }, unresolvedObj) - } - c.compileEdgeMapKey(obj, m, n.MapKey) - } - if n.MapKey.Key != nil && n.MapKey.Value.Map != nil { - c.compileEdges(obj.EnsureChild(d2graph.Key(n.MapKey.Key)), n.MapKey.Value.Map) + if obj.Attributes.Label.MapKey == nil { + obj.Attributes.Label.MapKey = f.LastPrimaryKey() + } + for _, fr := range f.References { + if fr.Primary() { + if fr.Context.Key.Value.Map != nil { + obj.Map = fr.Context.Key.Value.Map } } + scopeObjIDA := d2ir.IDA(fr.Context.ScopeMap) + scopeObj, _ := obj.Graph.Root.HasChild(scopeObjIDA) + obj.References = append(obj.References, d2graph.Reference{ + Key: fr.KeyPath, + KeyPathIndex: fr.KeyPathIndex(), + + MapKey: fr.Context.Key, + MapKeyEdgeIndex: fr.Context.EdgeIndex(), + Scope: fr.Context.Scope, + ScopeObj: scopeObj, + }) } } -// compileArrowheads compiles keywords for edge arrowhead attributes by -// 1. creating a fake, detached parent -// 2. compiling the arrowhead field as a fake object onto that fake parent -// 3. transferring the relevant attributes onto the edge -func (c *compiler) compileArrowheads(edge *d2graph.Edge, m *d2ast.Map, mk *d2ast.Key) bool { - arrowheadKey := mk.Key - if mk.EdgeKey != nil { - arrowheadKey = mk.EdgeKey - } - if arrowheadKey == nil || len(arrowheadKey.Path) == 0 { - return false - } - key := arrowheadKey.Path[0].Unbox().ScalarString() - var field *d2graph.Attributes - if key == "source-arrowhead" { - if edge.SrcArrowhead == nil { - edge.SrcArrowhead = &d2graph.Attributes{} - } - field = edge.SrcArrowhead - } else if key == "target-arrowhead" { - if edge.DstArrowhead == nil { - edge.DstArrowhead = &d2graph.Attributes{} - } - field = edge.DstArrowhead - } else { - return false - } - fakeParent := &d2graph.Object{ - Children: make(map[string]*d2graph.Object), - } - detachedMK := &d2ast.Key{ - Key: arrowheadKey, - Primary: mk.Primary, - Value: mk.Value, - } - c.compileKey(fakeParent, m, detachedMK) - fakeObj := fakeParent.ChildrenArray[0] - c.compileShapes(fakeObj) - - if fakeObj.Attributes.Shape.Value != "" { - field.Shape = fakeObj.Attributes.Shape - } - if fakeObj.Attributes.Label.Value != "" && fakeObj.Attributes.Label.Value != "source-arrowhead" && fakeObj.Attributes.Label.Value != "target-arrowhead" { - field.Label = fakeObj.Attributes.Label - } - if fakeObj.Attributes.Style.Filled != nil { - field.Style.Filled = fakeObj.Attributes.Style.Filled - } - - return true -} - -func (c *compiler) compileAttributes(attrs *d2graph.Attributes, mk *d2ast.Key) { - var reserved string - var ok bool - - if mk.EdgeKey != nil { - _, reserved, ok = c.compileFlatKey(mk.EdgeKey) - } else if mk.Key != nil { - _, reserved, ok = c.compileFlatKey(mk.Key) - } - if !ok { - return - } - - if reserved == "" || reserved == "label" { - attrs.Label.MapKey = mk - } else if reserved == "shape" { - attrs.Shape.MapKey = mk - } else if reserved == "opacity" { - attrs.Style.Opacity = &d2graph.Scalar{MapKey: mk} - } else if reserved == "stroke" { - attrs.Style.Stroke = &d2graph.Scalar{MapKey: mk} - } else if reserved == "fill" { - attrs.Style.Fill = &d2graph.Scalar{MapKey: mk} - } else if reserved == "stroke-width" { - attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: mk} - } else if reserved == "stroke-dash" { - attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: mk} - } else if reserved == "border-radius" { - attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: mk} - } else if reserved == "shadow" { - attrs.Style.Shadow = &d2graph.Scalar{MapKey: mk} - } else if reserved == "3d" { - // TODO this should be movd to validateKeys, as shape may not be set yet - if attrs.Shape.Value != "" && !strings.EqualFold(attrs.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(attrs.Shape.Value, d2target.ShapeRectangle) { - c.errorf(mk.Range.Start, mk.Range.End, `key "3d" can only be applied to squares and rectangles`) - return - } - attrs.Style.ThreeDee = &d2graph.Scalar{MapKey: mk} - } else if reserved == "multiple" { - attrs.Style.Multiple = &d2graph.Scalar{MapKey: mk} - } else if reserved == "font" { - attrs.Style.Font = &d2graph.Scalar{MapKey: mk} - } else if reserved == "font-size" { - attrs.Style.FontSize = &d2graph.Scalar{MapKey: mk} - } else if reserved == "font-color" { - attrs.Style.FontColor = &d2graph.Scalar{MapKey: mk} - } else if reserved == "animated" { - attrs.Style.Animated = &d2graph.Scalar{MapKey: mk} - } else if reserved == "bold" { - attrs.Style.Bold = &d2graph.Scalar{MapKey: mk} - } else if reserved == "italic" { - attrs.Style.Italic = &d2graph.Scalar{MapKey: mk} - } else if reserved == "underline" { - attrs.Style.Underline = &d2graph.Scalar{MapKey: mk} - } else if reserved == "filled" { - attrs.Style.Filled = &d2graph.Scalar{MapKey: mk} - } else if reserved == "width" { - attrs.Width = &d2graph.Scalar{MapKey: mk} - } else if reserved == "height" { - attrs.Height = &d2graph.Scalar{MapKey: mk} - } else if reserved == "double-border" { - if attrs.Shape.Value != "" && !strings.EqualFold(attrs.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(attrs.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(attrs.Shape.Value, d2target.ShapeCircle) && !strings.EqualFold(attrs.Shape.Value, d2target.ShapeOval) { - c.errorf(mk.Range.Start, mk.Range.End, `key "double-border" can only be applied to squares, rectangles, circles, ovals`) - return - } - attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: mk} - } -} - -func (c *compiler) compileKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key) { - ida, reserved, ok := c.compileFlatKey(mk.Key) - if !ok { - return - } - if reserved == "desc" { - return - } - - resolvedObj, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, obj) - if err != nil { - c.errorf(mk.Range.Start, mk.Range.End, err.Error()) - return - } - - parent := resolvedObj - if len(resolvedIDA) > 0 { - unresolvedObj := obj - obj = parent.EnsureChild(resolvedIDA) - parent.AppendReferences(ida, d2graph.Reference{ - Key: mk.Key, - - MapKey: mk, - Scope: m, - }, unresolvedObj) - } else if obj.Parent == nil { - // Top level reserved key set on root. - c.compileAttributes(&obj.Attributes, mk) - c.applyScalar(&obj.Attributes, reserved, mk.Value.ScalarBox()) - return - } - - if len(mk.Edges) > 0 { - return - } - - c.compileAttributes(&obj.Attributes, mk) - if obj.Attributes.Style.Animated != nil { - c.errorf(mk.Range.Start, mk.Range.End, `key "animated" can only be applied to edges`) - return - } - - c.applyScalar(&obj.Attributes, reserved, mk.Value.ScalarBox()) - if mk.Value.Map != nil { - if reserved != "" { - c.errorf(mk.Range.Start, mk.Range.End, "cannot set reserved key %q to a map", reserved) - return - } - obj.Map = mk.Value.Map - c.compileKeys(obj, mk.Value.Map) - } - - c.applyScalar(&obj.Attributes, reserved, mk.Primary) -} - -func (c *compiler) applyScalar(attrs *d2graph.Attributes, reserved string, box d2ast.ScalarBox) { - scalar := box.Unbox() - if scalar == nil { - return - } - - switch reserved { - case "shape": - in := d2target.IsShape(scalar.ScalarString()) - _, isArrowhead := d2target.Arrowheads[scalar.ScalarString()] - if !in && !isArrowhead { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, "unknown shape %q", scalar.ScalarString()) - return - } - if box.Null != nil { - attrs.Shape.Value = "" - } else { - attrs.Shape.Value = scalar.ScalarString() - } - if attrs.Shape.Value == d2target.ShapeCode { - // Explicit code shape is plaintext. - attrs.Language = d2target.ShapeText - } - return - case "icon": - iconURL, err := url.Parse(scalar.ScalarString()) - if err != nil { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, "bad icon url %#v: %s", scalar.ScalarString(), err) - return - } - attrs.Icon = iconURL - return - case "near": - nearKey, err := d2parser.ParseKey(scalar.ScalarString()) - if err != nil { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, "bad near key %#v: %s", scalar.ScalarString(), err) - return - } - attrs.NearKey = nearKey - return - case "tooltip": - attrs.Tooltip = scalar.ScalarString() - return - case "width": - _, err := strconv.Atoi(scalar.ScalarString()) - if err != nil { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, "non-integer width %#v: %s", scalar.ScalarString(), err) - return - } - attrs.Width.Value = scalar.ScalarString() - return - case "height": - _, err := strconv.Atoi(scalar.ScalarString()) - if err != nil { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, "non-integer height %#v: %s", scalar.ScalarString(), err) - return - } - attrs.Height.Value = scalar.ScalarString() - return - case "link": - attrs.Link = scalar.ScalarString() - return - case "direction": - dirs := []string{"up", "down", "right", "left"} - if !go2.Contains(dirs, scalar.ScalarString()) { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, `direction must be one of %v, got %q`, strings.Join(dirs, ", "), scalar.ScalarString()) - return - } - attrs.Direction.Value = scalar.ScalarString() - return - case "constraint": - // Compilation for shape-specific keywords happens elsewhere - return - } - - if _, ok := d2graph.StyleKeywords[reserved]; ok { - if err := attrs.Style.Apply(reserved, scalar.ScalarString()); err != nil { - c.errorf(scalar.GetRange().Start, scalar.GetRange().End, err.Error()) - } - return - } - - if box.Null != nil { - // TODO: delete obj - attrs.Label.Value = "" - } else { +func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) { + scalar := f.Primary().Value + switch scalar := scalar.(type) { + case *d2ast.Null: + // TODO: Delete instaed. attrs.Label.Value = scalar.ScalarString() - } - - bs := box.BlockString - if bs != nil && reserved == "" { - attrs.Language = bs.Tag - fullTag, ok := ShortToFullLanguageAliases[bs.Tag] + case *d2ast.BlockString: + attrs.Language = scalar.Tag + fullTag, ok := ShortToFullLanguageAliases[scalar.Tag] if ok { attrs.Language = fullTag } @@ -400,168 +208,243 @@ func (c *compiler) applyScalar(attrs *d2graph.Attributes, reserved string, box d } else { attrs.Shape.Value = d2target.ShapeCode } + attrs.Label.Value = scalar.ScalarString() + default: + attrs.Label.Value = scalar.ScalarString() } + attrs.Label.MapKey = f.LastPrimaryKey() } -func (c *compiler) compileEdgeMapKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key) { - if mk.EdgeIndex != nil { - edge, ok := obj.HasEdge(mk) - if ok { - c.appendEdgeReferences(obj, m, mk) - edge.References = append(edge.References, d2graph.EdgeReference{ - Edge: mk.Edges[0], - - MapKey: mk, - MapKeyEdgeIndex: 0, - Scope: m, - ScopeObj: obj, - }) - c.compileEdge(edge, m, mk) +func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) { + if f.Primary() == nil { + if f.Composite != nil { + c.errorf(f.LastPrimaryKey(), "reserved field %v does not accept composite", f.Name) } return } - for i, e := range mk.Edges { - if e.Src == nil || e.Dst == nil { - continue + scalar := f.Primary().Value + switch f.Name { + case "label": + c.compileLabel(attrs, f) + case "shape": + in := d2target.IsShape(scalar.ScalarString()) + _, isArrowhead := d2target.Arrowheads[scalar.ScalarString()] + if !in && !isArrowhead { + c.errorf(scalar, "unknown shape %q", scalar.ScalarString()) + return } - edge, err := obj.Connect(d2graph.Key(e.Src), d2graph.Key(e.Dst), e.SrcArrow == "<", e.DstArrow == ">", "") + attrs.Shape.Value = scalar.ScalarString() + if attrs.Shape.Value == d2target.ShapeCode { + // Explicit code shape is plaintext. + attrs.Language = d2target.ShapeText + } + attrs.Shape.MapKey = f.LastPrimaryKey() + case "icon": + iconURL, err := url.Parse(scalar.ScalarString()) if err != nil { - c.errorf(e.Range.Start, e.Range.End, err.Error()) + c.errorf(scalar, "bad icon url %#v: %s", scalar.ScalarString(), err) + return + } + attrs.Icon = iconURL + case "near": + nearKey, err := d2parser.ParseKey(scalar.ScalarString()) + if err != nil { + c.errorf(scalar, "bad near key %#v: %s", scalar.ScalarString(), err) + return + } + nearKey.Range = scalar.GetRange() + attrs.NearKey = nearKey + case "tooltip": + attrs.Tooltip = scalar.ScalarString() + case "width": + _, err := strconv.Atoi(scalar.ScalarString()) + if err != nil { + c.errorf(scalar, "non-integer width %#v: %s", scalar.ScalarString(), err) + return + } + attrs.Width = &d2graph.Scalar{} + attrs.Width.Value = scalar.ScalarString() + attrs.Width.MapKey = f.LastPrimaryKey() + case "height": + _, err := strconv.Atoi(scalar.ScalarString()) + if err != nil { + c.errorf(scalar, "non-integer height %#v: %s", scalar.ScalarString(), err) + return + } + attrs.Height = &d2graph.Scalar{} + attrs.Height.Value = scalar.ScalarString() + attrs.Height.MapKey = f.LastPrimaryKey() + case "link": + attrs.Link = scalar.ScalarString() + case "direction": + dirs := []string{"up", "down", "right", "left"} + if !go2.Contains(dirs, scalar.ScalarString()) { + c.errorf(scalar, `direction must be one of %v, got %q`, strings.Join(dirs, ", "), scalar.ScalarString()) + return + } + attrs.Direction.Value = scalar.ScalarString() + attrs.Direction.MapKey = f.LastPrimaryKey() + case "constraint": + if _, ok := scalar.(d2ast.String); !ok { + c.errorf(f.LastPrimaryKey(), "constraint value must be a string") + return + } + attrs.Constraint.Value = scalar.ScalarString() + attrs.Constraint.MapKey = f.LastPrimaryKey() + } +} + +func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) { + for _, f := range m.Fields { + c.compileStyleField(attrs, f) + } +} + +func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) { + if f.Primary() == nil { + return + } + compileStyleFieldInit(attrs, f) + scalar := f.Primary().Value + err := attrs.Style.Apply(f.Name, scalar.ScalarString()) + if err != nil { + c.errorf(scalar, err.Error()) + return + } +} + +func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) { + switch f.Name { + case "opacity": + attrs.Style.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "stroke": + attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "fill": + attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "stroke-width": + attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "stroke-dash": + attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "border-radius": + attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "shadow": + attrs.Style.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "3d": + attrs.Style.ThreeDee = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "multiple": + attrs.Style.Multiple = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "font": + attrs.Style.Font = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "font-size": + attrs.Style.FontSize = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "font-color": + attrs.Style.FontColor = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "animated": + attrs.Style.Animated = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "bold": + attrs.Style.Bold = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "italic": + attrs.Style.Italic = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "underline": + attrs.Style.Underline = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "filled": + attrs.Style.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "width": + attrs.Width = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "height": + attrs.Height = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + case "double-border": + attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + } +} + +func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) { + edge, err := obj.Connect(d2graphIDA(e.ID.SrcPath), d2graphIDA(e.ID.DstPath), e.ID.SrcArrow, e.ID.DstArrow, "") + if err != nil { + c.errorf(e.References[0].AST(), err.Error()) + return + } + + if e.Primary() != nil { + c.compileLabel(edge.Attributes, e) + } + if e.Map() != nil { + for _, f := range e.Map().Fields { + _, ok := d2graph.ReservedKeywords[f.Name] + if !ok { + c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`) + continue + } + c.compileEdgeField(edge, f) + } + } + + edge.Attributes.Label.MapKey = e.LastPrimaryKey() + for _, er := range e.References { + scopeObjIDA := d2ir.IDA(er.Context.ScopeMap) + scopeObj, _ := edge.Src.Graph.Root.HasChild(d2graphIDA(scopeObjIDA)) + edge.References = append(edge.References, d2graph.EdgeReference{ + Edge: er.Context.Edge, + MapKey: er.Context.Key, + MapKeyEdgeIndex: er.Context.EdgeIndex(), + Scope: er.Context.Scope, + ScopeObj: scopeObj, + }) + } +} + +func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) { + keyword := strings.ToLower(f.Name) + _, isReserved := d2graph.SimpleReservedKeywords[keyword] + if isReserved { + c.compileReserved(edge.Attributes, f) + return + } else if f.Name == "style" { + if f.Map() == nil { + return + } + c.compileStyle(edge.Attributes, f.Map()) + return + } + + if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" { + if f.Map() != nil { + c.compileArrowheads(edge, f) + } + } +} + +func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) { + var attrs *d2graph.Attributes + if f.Name == "source-arrowhead" { + edge.SrcArrowhead = &d2graph.Attributes{} + attrs = edge.SrcArrowhead + } else { + edge.DstArrowhead = &d2graph.Attributes{} + attrs = edge.DstArrowhead + } + + if f.Primary() != nil { + c.compileLabel(attrs, f) + } + + for _, f2 := range f.Map().Fields { + keyword := strings.ToLower(f2.Name) + _, isReserved := d2graph.SimpleReservedKeywords[keyword] + if isReserved { + c.compileReserved(attrs, f2) + continue + } else if f2.Name == "style" { + if f2.Map() == nil { + continue + } + c.compileStyle(attrs, f2.Map()) + continue + } else { + c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`) continue } - edge.References = append(edge.References, d2graph.EdgeReference{ - Edge: e, - - MapKey: mk, - MapKeyEdgeIndex: i, - Scope: m, - ScopeObj: obj, - }) - c.compileEdge(edge, m, mk) } - c.appendEdgeReferences(obj, m, mk) -} - -func (c *compiler) compileEdge(edge *d2graph.Edge, m *d2ast.Map, mk *d2ast.Key) { - if mk.Key == nil && mk.EdgeKey == nil { - if len(mk.Edges) == 1 { - edge.Attributes.Label.MapKey = mk - } - c.applyScalar(&edge.Attributes, "", mk.Value.ScalarBox()) - c.applyScalar(&edge.Attributes, "", mk.Primary) - } else { - c.compileEdgeKey(edge, m, mk) - } - if mk.Value.Map != nil && mk.EdgeKey == nil { - for _, n := range mk.Value.Map.Nodes { - if n.MapKey == nil { - continue - } - if len(n.MapKey.Edges) > 0 { - c.errorf(mk.Range.Start, mk.Range.End, `edges cannot be nested within another edge`) - continue - } - if n.MapKey.Key == nil { - continue - } - for _, p := range n.MapKey.Key.Path { - _, ok := d2graph.ReservedKeywords[strings.ToLower(p.Unbox().ScalarString())] - if !ok { - c.errorf(mk.Range.Start, mk.Range.End, `edge map keys must be reserved keywords`) - return - } - } - c.compileEdgeKey(edge, m, n.MapKey) - } - } -} - -func (c *compiler) compileEdgeKey(edge *d2graph.Edge, m *d2ast.Map, mk *d2ast.Key) { - var r string - var ok bool - - // Give precedence to EdgeKeys - // x.(a -> b)[0].style.opacity: 0.4 - // We want to compile the style.opacity, not the x - if mk.EdgeKey != nil { - _, r, ok = c.compileFlatKey(mk.EdgeKey) - } else if mk.Key != nil { - _, r, ok = c.compileFlatKey(mk.Key) - } - if !ok { - return - } - - ok = c.compileArrowheads(edge, m, mk) - if ok { - return - } - c.compileAttributes(&edge.Attributes, mk) - c.applyScalar(&edge.Attributes, r, mk.Value.ScalarBox()) - if mk.Value.Map != nil { - for _, n := range mk.Value.Map.Nodes { - if n.MapKey != nil { - c.compileEdgeKey(edge, m, n.MapKey) - } - } - } -} - -func (c *compiler) appendEdgeReferences(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key) { - for i, e := range mk.Edges { - if e.Src != nil { - ida := d2graph.Key(e.Src) - - parent, _, err := d2graph.ResolveUnderscoreKey(ida, obj) - if err != nil { - c.errorf(mk.Range.Start, mk.Range.End, err.Error()) - return - } - parent.AppendReferences(ida, d2graph.Reference{ - Key: e.Src, - - MapKey: mk, - MapKeyEdgeIndex: i, - Scope: m, - }, obj) - } - if e.Dst != nil { - ida := d2graph.Key(e.Dst) - - parent, _, err := d2graph.ResolveUnderscoreKey(ida, obj) - if err != nil { - c.errorf(mk.Range.Start, mk.Range.End, err.Error()) - return - } - parent.AppendReferences(ida, d2graph.Reference{ - Key: e.Dst, - - MapKey: mk, - MapKeyEdgeIndex: i, - Scope: m, - }, obj) - } - } -} - -func (c *compiler) compileFlatKey(k *d2ast.KeyPath) ([]string, string, bool) { - k2 := *k - var reserved string - for i, s := range k.Path { - keyword := strings.ToLower(s.Unbox().ScalarString()) - _, isReserved := d2graph.ReservedKeywords[keyword] - _, isReservedHolder := d2graph.ReservedKeywordHolders[keyword] - if isReserved && !isReservedHolder { - reserved = keyword - k2.Path = k2.Path[:i] - break - } - } - if len(k2.Path) < len(k.Path)-1 { - c.errorf(k.Range.Start, k.Range.End, "reserved key %q cannot have children", reserved) - return nil, "", false - } - return d2graph.Key(&k2), reserved, true } // TODO add more, e.g. C, bash @@ -576,56 +459,9 @@ var ShortToFullLanguageAliases = map[string]string{ } var FullToShortLanguageAliases map[string]string -func (c *compiler) compileShapes(obj *d2graph.Object) { - for _, obj := range obj.ChildrenArray { - switch obj.Attributes.Shape.Value { - case d2target.ShapeClass: - c.compileClass(obj) - case d2target.ShapeSQLTable: - c.compileSQLTable(obj) - case d2target.ShapeImage: - c.compileImage(obj) - } - c.compileShapes(obj) - } - - for i := 0; i < len(obj.ChildrenArray); i++ { - ch := obj.ChildrenArray[i] - switch ch.Attributes.Shape.Value { - case d2target.ShapeClass, d2target.ShapeSQLTable: - flattenContainer(obj.Graph, ch) - } - if ch.IDVal == "style" { - obj.Attributes.Style = ch.Attributes.Style - if obj.Graph != nil { - flattenContainer(obj.Graph, ch) - for i := 0; i < len(obj.Graph.Objects); i++ { - if obj.Graph.Objects[i] == ch { - obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...) - break - } - } - delete(obj.Children, ch.ID) - obj.ChildrenArray = append(obj.ChildrenArray[:i], obj.ChildrenArray[i+1:]...) - i-- - } - } - } -} - -func (c *compiler) compileImage(obj *d2graph.Object) { - if obj.Attributes.Icon == nil { - c.errorf(obj.Attributes.Shape.MapKey.Range.Start, obj.Attributes.Shape.MapKey.Range.End, `image shape must include an "icon" field`) - } -} - func (c *compiler) compileClass(obj *d2graph.Object) { obj.Class = &d2target.Class{} - for _, f := range obj.ChildrenArray { - if f.IDVal == "style" { - continue - } visiblity := "public" name := f.IDVal // See https://www.uml-diagrams.org/visibility.html @@ -666,17 +502,22 @@ func (c *compiler) compileClass(obj *d2graph.Object) { }) } } + + for _, ch := range obj.ChildrenArray { + for i := 0; i < len(obj.Graph.Objects); i++ { + if obj.Graph.Objects[i] == ch { + obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...) + i-- + } + } + } + obj.Children = nil + obj.ChildrenArray = nil } func (c *compiler) compileSQLTable(obj *d2graph.Object) { obj.SQLTable = &d2target.SQLTable{} - - parentID := obj.Parent.AbsID() - tableIDPrefix := obj.AbsID() + "." for _, col := range obj.ChildrenArray { - if col.IDVal == "style" { - continue - } typ := col.Attributes.Label.Value if typ == col.IDVal { // Not great, AST should easily allow specifying alternate primary field @@ -687,182 +528,87 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) { Name: d2target.Text{Label: col.IDVal}, Type: d2target.Text{Label: typ}, } - // The only map a sql table field could have is to specify constraint - if col.Map != nil { - for _, n := range col.Map.Nodes { - if n.MapKey.Key == nil || len(n.MapKey.Key.Path) == 0 { - continue - } - if n.MapKey.Key.Path[0].Unbox().ScalarString() == "constraint" { - if n.MapKey.Value.StringBox().Unbox() == nil { - c.errorf(n.MapKey.GetRange().Start, n.MapKey.GetRange().End, "constraint value must be a string") - return - } - d2Col.Constraint = n.MapKey.Value.StringBox().Unbox().ScalarString() - } - } + if col.Attributes.Constraint.Value != "" { + d2Col.Constraint = col.Attributes.Constraint.Value } - - absID := col.AbsID() - for _, e := range obj.Graph.Edges { - srcID := e.Src.AbsID() - dstID := e.Dst.AbsID() - // skip edges between columns of the same table - if strings.HasPrefix(srcID, tableIDPrefix) && strings.HasPrefix(dstID, tableIDPrefix) { - continue - } - if srcID == absID { - d2Col.Reference = strings.TrimPrefix(dstID, parentID+".") - e.SrcTableColumnIndex = new(int) - *e.SrcTableColumnIndex = len(obj.SQLTable.Columns) - } else if dstID == absID { - e.DstTableColumnIndex = new(int) - *e.DstTableColumnIndex = len(obj.SQLTable.Columns) - } - } - obj.SQLTable.Columns = append(obj.SQLTable.Columns, d2Col) } + + for _, ch := range obj.ChildrenArray { + for i := 0; i < len(obj.Graph.Objects); i++ { + if obj.Graph.Objects[i] == ch { + obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...) + i-- + } + } + } + obj.Children = nil + obj.ChildrenArray = nil } -func flattenContainer(g *d2graph.Graph, obj *d2graph.Object) { - absID := obj.AbsID() - - toRemove := map[*d2graph.Edge]struct{}{} - toAdd := []*d2graph.Edge{} - for i := 0; i < len(g.Edges); i++ { - e := g.Edges[i] - srcID := e.Src.AbsID() - dstID := e.Dst.AbsID() - - srcIsChild := strings.HasPrefix(srcID, absID+".") - dstIsChild := strings.HasPrefix(dstID, absID+".") - if srcIsChild && dstIsChild { - toRemove[e] = struct{}{} - } else if srcIsChild { - toRemove[e] = struct{}{} - if dstID == absID { - continue - } - toAdd = append(toAdd, e) - } else if dstIsChild { - toRemove[e] = struct{}{} - if srcID == absID { - continue - } - toAdd = append(toAdd, e) - } - } - for _, e := range toAdd { - var newEdge *d2graph.Edge - if strings.HasPrefix(e.Src.AbsID(), absID+".") { - newEdge, _ = g.Root.Connect(obj.AbsIDArray(), e.Dst.AbsIDArray(), e.SrcArrow, e.DstArrow, e.Attributes.Label.Value) - } else { - newEdge, _ = g.Root.Connect(e.Src.AbsIDArray(), obj.AbsIDArray(), e.SrcArrow, e.DstArrow, e.Attributes.Label.Value) - } - // TODO more attributes - if e.SrcTableColumnIndex != nil { - newEdge.SrcTableColumnIndex = new(int) - newEdge.SrcArrowhead = e.SrcArrowhead - *newEdge.SrcTableColumnIndex = *e.SrcTableColumnIndex - } - if e.DstTableColumnIndex != nil { - newEdge.DstTableColumnIndex = new(int) - newEdge.DstArrowhead = e.DstArrowhead - *newEdge.DstTableColumnIndex = *e.DstTableColumnIndex - } - newEdge.Attributes = e.Attributes - newEdge.References = e.References - } - updatedEdges := []*d2graph.Edge{} - for _, e := range g.Edges { - if _, is := toRemove[e]; is { +func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) { + for _, f := range m.Fields { + if _, ok := d2graph.BoardKeywords[f.Name]; ok { continue } - updatedEdges = append(updatedEdges, e) + c.validateKey(obj, f) } - g.Edges = updatedEdges +} - for i := 0; i < len(g.Objects); i++ { - child := g.Objects[i] - if strings.HasPrefix(child.AbsID(), absID+".") { - g.Objects = append(g.Objects[:i], g.Objects[i+1:]...) - i-- - delete(obj.Children, child.ID) - for i, child2 := range obj.ChildrenArray { - if child == child2 { - obj.ChildrenArray = append(obj.ChildrenArray[:i], obj.ChildrenArray[i+1:]...) - break - } +func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) { + keyword := strings.ToLower(f.Name) + _, isReserved := d2graph.ReservedKeywords[keyword] + if isReserved { + switch obj.Attributes.Shape.Value { + case d2target.ShapeSQLTable, d2target.ShapeClass: + default: + if len(obj.Children) > 0 && (f.Name == "width" || f.Name == "height") { + c.errorf(f.LastPrimaryKey(), fmt.Sprintf("%s cannot be used on container: %s", f.Name, obj.AbsID())) } } - } -} -func (c *compiler) validateKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key) { - ida, reserved, ok := c.compileFlatKey(mk.Key) - if !ok { + switch obj.Attributes.Shape.Value { + case d2target.ShapeCircle, d2target.ShapeSquare: + checkEqual := (keyword == "width" && obj.Attributes.Height != nil) || (keyword == "height" && obj.Attributes.Width != nil) + if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value { + c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Attributes.Shape.Value) + } + } + + switch f.Name { + case "style": + if obj.Attributes.Style.ThreeDee != nil { + if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) { + c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares and rectangles`) + } + } + if obj.Attributes.Style.DoubleBorder != nil { + if obj.Attributes.Shape.Value != "" && obj.Attributes.Shape.Value != d2target.ShapeSquare && obj.Attributes.Shape.Value != d2target.ShapeRectangle && obj.Attributes.Shape.Value != d2target.ShapeCircle && obj.Attributes.Shape.Value != d2target.ShapeOval { + c.errorf(obj.Attributes.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`) + } + } + case "shape": + if obj.Attributes.Shape.Value == d2target.ShapeImage && obj.Attributes.Icon == nil { + c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`) + } + + in := d2target.IsShape(obj.Attributes.Shape.Value) + _, arrowheadIn := d2target.Arrowheads[obj.Attributes.Shape.Value] + if !in && arrowheadIn { + c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value)) + } + } return } - switch strings.ToLower(obj.Attributes.Shape.Value) { - case d2target.ShapeImage: - if reserved == "" { - c.errorf(mk.Range.Start, mk.Range.End, "image shapes cannot have children.") - } - case d2target.ShapeCircle, d2target.ShapeSquare: - checkEqual := (reserved == "width" && obj.Attributes.Height != nil) || - (reserved == "height" && obj.Attributes.Width != nil) - - if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value { - c.errorf(mk.Range.Start, mk.Range.End, fmt.Sprintf("width and height must be equal for %s shapes", obj.Attributes.Shape.Value)) - } - } - - in := d2target.IsShape(obj.Attributes.Shape.Value) - _, arrowheadIn := d2target.Arrowheads[obj.Attributes.Shape.Value] - if !in && arrowheadIn { - c.errorf(mk.Range.Start, mk.Range.End, fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value)) - } - - resolvedObj, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, obj) - if err != nil { - c.errorf(mk.Range.Start, mk.Range.End, err.Error()) - return - } - if resolvedObj != obj { - obj = resolvedObj - } - - parent := obj - if len(resolvedIDA) > 0 { - obj, _ = parent.HasChild(resolvedIDA) - } else if obj.Parent == nil { + if obj.Attributes.Shape.Value == d2target.ShapeImage { + c.errorf(f.LastRef().AST(), "image shapes cannot have children.") return } - switch strings.ToLower(obj.Attributes.Shape.Value) { - case d2target.ShapeSQLTable, d2target.ShapeClass: - default: - if len(obj.Children) > 0 && !(len(obj.Children) == 1 && obj.ChildrenArray[0].ID == "style") && (reserved == "width" || reserved == "height") { - c.errorf(mk.Range.Start, mk.Range.End, fmt.Sprintf("%s cannot be used on container: %s", reserved, obj.AbsID())) - } - } - - if len(mk.Edges) > 0 { - return - } - - if mk.Value.Map != nil { - c.validateKeys(obj, mk.Value.Map) - } -} - -func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ast.Map) { - for _, n := range m.Nodes { - if n.MapKey != nil && n.MapKey.Key != nil && len(n.MapKey.Edges) == 0 { - c.validateKey(obj, m, n.MapKey) - } + obj, ok := obj.HasChild([]string{f.Name}) + if ok && f.Map() != nil { + c.validateKeys(obj, f.Map()) } } @@ -872,15 +618,15 @@ func (c *compiler) validateNear(g *d2graph.Graph) { _, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) _, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]] if !isKey && !isConst { - c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.Attributes.NearKey), strings.Join(d2graph.NearConstantsArray, ", ")) + c.errorf(obj.Attributes.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.Attributes.NearKey), strings.Join(d2graph.NearConstantsArray, ", ")) continue } if !isKey && isConst && obj.Parent != g.Root { - c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys can only be set on root level shapes") + c.errorf(obj.Attributes.NearKey, "constant near keys can only be set on root level shapes") continue } if !isKey && isConst && len(obj.ChildrenArray) > 0 { - c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys cannot be set on shapes with children") + c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on shapes with children") continue } if !isKey && isConst { @@ -892,7 +638,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) { } } if is { - c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys cannot be set on connected shapes") + c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on connected shapes") continue } } @@ -906,3 +652,137 @@ func init() { FullToShortLanguageAliases[v] = k } } + +func d2graphIDA(irIDA []string) (ida []string) { + for _, el := range irIDA { + n := &d2ast.KeyPath{ + Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(el, true)).StringBox()}, + } + ida = append(ida, d2format.Format(n)) + } + return ida +} + +// Unused for now until shape: edge_group +func (c *compiler) preprocessSeqDiagrams(m *d2ir.Map) { + for _, f := range m.Fields { + if f.Name == "shape" && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram { + c.preprocessEdgeGroup(m, m) + return + } + if f.Map() != nil { + c.preprocessSeqDiagrams(f.Map()) + } + } +} + +func (c *compiler) preprocessEdgeGroup(seqDiagram, m *d2ir.Map) { + // Any child of a sequence diagram can be either an actor, edge group or a span. + // 1. Actors are shapes without edges inside them defined at the top level scope of a + // sequence diagram. + // 2. Spans are the children of actors. For our purposes we can ignore them. + // 3. Edge groups are defined as having at least one connection within them and also not + // being connected to anything. All direct children of an edge group are either edge + // groups or top level actors. + + // Go through all the fields and hoist actors from edge groups while also processing + // the edge groups recursively. + for _, f := range m.Fields { + if isEdgeGroup(f) { + if f.Map() != nil { + c.preprocessEdgeGroup(seqDiagram, f.Map()) + } + } else { + if m == seqDiagram { + // Ignore for root. + continue + } + hoistActor(seqDiagram, f) + } + } + + // We need to adjust all edges recursively to point to actual actors instead. + for _, e := range m.Edges { + if isCrossEdgeGroupEdge(m, e) { + c.errorf(e.References[0].AST(), "illegal edge between edge groups") + continue + } + + if m == seqDiagram { + // Root edges between actors directly do not require hoisting. + continue + } + + srcParent := seqDiagram + for i, el := range e.ID.SrcPath { + f := srcParent.GetField(el) + if !isEdgeGroup(f) { + for j := 0; j < i+1; j++ { + e.ID.SrcPath = append([]string{"_"}, e.ID.SrcPath...) + e.ID.DstPath = append([]string{"_"}, e.ID.DstPath...) + } + break + } + srcParent = f.Map() + } + } +} + +func hoistActor(seqDiagram *d2ir.Map, f *d2ir.Field) { + f2 := seqDiagram.GetField(f.Name) + if f2 == nil { + seqDiagram.Fields = append(seqDiagram.Fields, f.Copy(seqDiagram).(*d2ir.Field)) + } else { + d2ir.OverlayField(f2, f) + d2ir.ParentMap(f).DeleteField(f.Name) + } +} + +func isCrossEdgeGroupEdge(m *d2ir.Map, e *d2ir.Edge) bool { + srcParent := m + for _, el := range e.ID.SrcPath { + f := srcParent.GetField(el) + if f == nil { + // Hoisted already. + break + } + if isEdgeGroup(f) { + return true + } + srcParent = f.Map() + } + + dstParent := m + for _, el := range e.ID.DstPath { + f := dstParent.GetField(el) + if f == nil { + // Hoisted already. + break + } + if isEdgeGroup(f) { + return true + } + dstParent = f.Map() + } + + return false +} + +func isEdgeGroup(n d2ir.Node) bool { + return n.Map().EdgeCountRecursive() > 0 +} + +func parentSeqDiagram(n d2ir.Node) *d2ir.Map { + for { + m := d2ir.ParentMap(n) + if m == nil { + return nil + } + for _, f := range m.Fields { + if f.Name == "shape" && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram { + return m + } + } + n = m + } +} diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index a432f078e..37e68632c 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -8,12 +8,13 @@ import ( tassert "github.com/stretchr/testify/assert" + "oss.terrastruct.com/util-go/assert" + "oss.terrastruct.com/util-go/diff" + "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2target" - "oss.terrastruct.com/util-go/assert" - "oss.terrastruct.com/util-go/diff" ) func TestCompile(t *testing.T) { @@ -123,8 +124,7 @@ x: { } `, expErr: `d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:3:2: width and height must be equal for circle shapes -d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes -`, +d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes`, }, { name: "single_dimension_on_circle", @@ -207,8 +207,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:16:3: height c d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:25:3: width cannot be used on container: containers.oval container d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:26:3: height cannot be used on container: containers.oval container d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:36:3: width cannot be used on container: containers.hexagon container -d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height cannot be used on container: containers.hexagon container -`, +d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height cannot be used on container: containers.hexagon container`, }, { name: "dimension_with_style", @@ -241,8 +240,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c } } `, - expErr: `d2/testdata/d2compiler/TestCompile/shape_unquoted_hex.d2:3:10: missing value after colon -`, + expErr: `d2/testdata/d2compiler/TestCompile/shape_unquoted_hex.d2:3:10: missing value after colon`, }, { name: "edge_unquoted_hex", @@ -253,8 +251,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c } } `, - expErr: `d2/testdata/d2compiler/TestCompile/edge_unquoted_hex.d2:3:10: missing value after colon -`, + expErr: `d2/testdata/d2compiler/TestCompile/edge_unquoted_hex.d2:3:10: missing value after colon`, }, { name: "blank_underscore", @@ -264,8 +261,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c _ } `, - expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: invalid use of parent "_" -`, + expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: field key must contain more than underscores`, }, { name: "image_non_style", @@ -276,8 +272,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c name: y } `, - expErr: `d2/testdata/d2compiler/TestCompile/image_non_style.d2:4:3: image shapes cannot have children. -`, + expErr: `d2/testdata/d2compiler/TestCompile/image_non_style.d2:4:3: image shapes cannot have children.`, }, { name: "stroke-width", @@ -302,8 +297,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c style.stroke-width: -1 } `, - expErr: `d2/testdata/d2compiler/TestCompile/illegal-stroke-width.d2:2:23: expected "stroke-width" to be a number between 0 and 15 -`, + expErr: `d2/testdata/d2compiler/TestCompile/illegal-stroke-width.d2:2:23: expected "stroke-width" to be a number between 0 and 15`, }, { name: "underscore_parent_create", @@ -340,8 +334,7 @@ x: { `, assertions: func(t *testing.T, g *d2graph.Graph) { tassert.Equal(t, "y", g.Objects[1].ID) - tassert.Equal(t, g.Root.AbsID(), g.Objects[1].References[0].ScopeObj.AbsID()) - tassert.Equal(t, g.Objects[0].AbsID(), g.Objects[1].References[0].UnresolvedScopeObj.AbsID()) + tassert.Equal(t, g.Objects[0].AbsID(), g.Objects[1].References[0].ScopeObj.AbsID()) }, }, { @@ -456,8 +449,7 @@ x: { text: ` _.x `, - expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: parent "_" cannot be used in the root scope -`, + expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: invalid underscore: no parent`, }, { name: "underscore_parent_middle_path", @@ -467,8 +459,7 @@ x: { y._.z } `, - expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:3: parent "_" can only be used in the beginning of paths, e.g. "_.x" -`, + expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:5: parent "_" can only be used in the beginning of paths, e.g. "_.x"`, }, { name: "underscore_parent_sandwich_path", @@ -478,8 +469,7 @@ x: { _.z._ } `, - expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:3: parent "_" can only be used in the beginning of paths, e.g. "_.x" -`, + expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:7: parent "_" can only be used in the beginning of paths, e.g. "_.x"`, }, { name: "underscore_edge", @@ -996,8 +986,7 @@ x -> y: { text: `x: {shape: triangle} `, - expErr: `d2/testdata/d2compiler/TestCompile/object_arrowhead_shape.d2:1:5: invalid shape, can only set "triangle" for arrowheads -`, + expErr: `d2/testdata/d2compiler/TestCompile/object_arrowhead_shape.d2:1:5: invalid shape, can only set "triangle" for arrowheads`, }, { name: "edge_flat_label_arrowhead", @@ -1083,8 +1072,7 @@ x -> y: { space -> stars } `, - expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:1:1: edges cannot be nested within another edge -`, + expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:2:3: cannot create edge inside edge`, }, { name: "shape_edge_style", @@ -1094,8 +1082,7 @@ x: { style.animated: true } `, - expErr: `d2/testdata/d2compiler/TestCompile/shape_edge_style.d2:3:2: key "animated" can only be applied to edges -`, + expErr: `d2/testdata/d2compiler/TestCompile/shape_edge_style.d2:3:2: key "animated" can only be applied to edges`, }, { name: "edge_chain_map", @@ -1351,8 +1338,7 @@ x -> y: { z } `, - expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:2:1: edge map keys must be reserved keywords -`, + expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:3:3: edge map keys must be reserved keywords`, }, { name: "url_link", @@ -1397,8 +1383,7 @@ x -> y: { text: `x.near: txop-center `, - expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:1: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right -`, + expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:9: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`, }, { name: "near_bad_container", @@ -1408,8 +1393,7 @@ x -> y: { y } `, - expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:1:1: constant near keys cannot be set on shapes with children -`, + expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:2:9: constant near keys cannot be set on shapes with children`, }, { name: "near_bad_connected", @@ -1419,16 +1403,14 @@ x -> y: { } x -> y `, - expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:1:1: constant near keys cannot be set on connected shapes -`, + expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:2:9: constant near keys cannot be set on connected shapes`, }, { name: "nested_near_constant", text: `x.y.near: top-center `, - expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:1: constant near keys can only be set on root level shapes -`, + expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:11: constant near keys can only be set on root level shapes`, }, { name: "reserved_icon_near_style", @@ -1474,17 +1456,14 @@ y } `, expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme -d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:4:18: expected "opacity" to be a number between 0.0 and 1.0 d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0 -d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right -`, +d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:2:9: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`, }, { name: "errors/missing_shape_icon", - text: `x.shape: image`, - expErr: `d2/testdata/d2compiler/TestCompile/errors/missing_shape_icon.d2:1:1: image shape must include an "icon" field -`, + text: `x.shape: image`, + expErr: `d2/testdata/d2compiler/TestCompile/errors/missing_shape_icon.d2:1:1: image shape must include an "icon" field`, }, { name: "edge_in_column", @@ -1500,8 +1479,7 @@ d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key " text: `x: {style.opacity: 0.4} y -> x.style `, - expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:1: cannot connect to reserved keyword -`, + expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:8: reserved keywords are prohibited in edges`, }, { name: "escaped_id", @@ -1581,7 +1559,7 @@ b`, g.Objects[0].Attributes.Label.Value) GetType(): string style: { opacity: 0.4 - color: blue + font-color: blue } } `, @@ -1680,10 +1658,9 @@ x.y -> a.b: { { name: "3d_oval", - text: `SVP1.style.shape: oval + text: `SVP1.shape: oval SVP1.style.3d: true`, - expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles -`, + expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles`, }, { name: "edge_column_index", text: `src: { @@ -1740,8 +1717,7 @@ dst.id <-> src.dst_id } b -> x.a `, - expErr: `d2/testdata/d2compiler/TestCompile/leaky_sequence.d2:5:1: connections within sequence diagrams can connect only to other objects within the same sequence diagram -`, + expErr: `d2/testdata/d2compiler/TestCompile/leaky_sequence.d2:5:1: connections within sequence diagrams can connect only to other objects within the same sequence diagram`, }, { name: "sequence_scoping", @@ -1775,6 +1751,35 @@ choo: { tassert.Equal(t, 3, len(g.Root.ChildrenArray)) }, }, + { + name: "sequence_container", + + text: `shape: sequence_diagram +x.y.q -> j.y.p +ok: { + x.y.q -> j.y.p +} +`, + assertions: func(t *testing.T, g *d2graph.Graph) { + tassert.Equal(t, 7, len(g.Objects)) + tassert.Equal(t, 3, len(g.Root.ChildrenArray)) + }, + }, + { + name: "sequence_container_2", + + text: `shape: sequence_diagram +x.y.q +ok: { + x.y.q -> j.y.p + meow +} +`, + assertions: func(t *testing.T, g *d2graph.Graph) { + tassert.Equal(t, 8, len(g.Objects)) + tassert.Equal(t, 2, len(g.Root.ChildrenArray)) + }, + }, { name: "root_direction", @@ -1818,8 +1823,7 @@ choo: { text: `x: { direction: diagonal }`, - expErr: `d2/testdata/d2compiler/TestCompile/invalid_direction.d2:2:14: direction must be one of up, down, right, left, got "diagonal" -`, + expErr: `d2/testdata/d2compiler/TestCompile/invalid_direction.d2:2:14: direction must be one of up, down, right, left, got "diagonal"`, }, { name: "self-referencing", @@ -1868,8 +1872,7 @@ choo: { test_id: varchar(64) {constraint: [primary_key, foreign_key]} } `, - expErr: `d2/testdata/d2compiler/TestCompile/sql-panic.d2:3:27: constraint value must be a string -`, + expErr: `d2/testdata/d2compiler/TestCompile/sql-panic.d2:3:27: reserved field constraint does not accept composite`, }, { name: "wrong_column_index", @@ -1939,3 +1942,167 @@ Chinchillas_Collectibles.chinchilla -> Chinchillas.id`, }) } } + +func TestCompile2(t *testing.T) { + t.Parallel() + + t.Run("boards", testBoards) + t.Run("seqdiagrams", testSeqDiagrams) +} + +func testBoards(t *testing.T) { + t.Parallel() + + tca := []struct { + name string + run func(t *testing.T) + }{ + { + name: "root", + run: func(t *testing.T) { + g := assertCompile(t, `base + +layers: { + one: { + santa + } + two: { + clause + } +} +`, "") + assert.JSON(t, 2, len(g.Layers)) + assert.JSON(t, "one", g.Layers[0].Name) + assert.JSON(t, "two", g.Layers[1].Name) + }, + }, + { + name: "recursive", + run: func(t *testing.T) { + g := assertCompile(t, `base + +layers: { + one: { + santa + } + two: { + clause + steps: { + seinfeld: { + reindeer + } + missoula: { + montana + } + } + } +} +`, "") + assert.Equal(t, 2, len(g.Layers)) + assert.Equal(t, "one", g.Layers[0].Name) + assert.Equal(t, "two", g.Layers[1].Name) + assert.Equal(t, 2, len(g.Layers[1].Steps)) + }, + }, + { + name: "errs/duplicate_board", + run: func(t *testing.T) { + assertCompile(t, `base + +layers: { + one: { + santa + } +} +steps: { + one: { + clause + } +} +`, `d2/testdata/d2compiler/TestCompile2/boards/errs/duplicate_board.d2:9:2: board name one already used by another board`) + }, + }, + } + + for _, tc := range tca { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.run(t) + }) + } +} + +func testSeqDiagrams(t *testing.T) { + t.Parallel() + + t.Run("errs", func(t *testing.T) { + t.Parallel() + + tca := []struct { + name string + skip bool + run func(t *testing.T) + }{ + { + name: "sequence_diagram_edge_between_edge_groups", + // New sequence diagram scoping implementation is disabled. + skip: true, + run: func(t *testing.T) { + assertCompile(t, ` +Office chatter: { + shape: sequence_diagram + alice: Alice + bob: Bobby + awkward small talk: { + alice -> bob: uhm, hi + bob -> alice: oh, hello + icebreaker attempt: { + alice -> bob: what did you have for lunch? + } + unfortunate outcome: { + bob -> alice: that's personal + } + } + awkward small talk.icebreaker attempt.alice -> awkward small talk.unfortunate outcome.bob +} +`, "d2/testdata/d2compiler/TestCompile2/seqdiagrams/errs/sequence_diagram_edge_between_edge_groups.d2:16:3: edges between edge groups are not allowed") + }, + }, + } + + for _, tc := range tca { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.skip { + t.SkipNow() + } + tc.run(t) + }) + } + }) +} + +func assertCompile(t *testing.T, text string, expErr string) *d2graph.Graph { + d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) + g, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil) + if expErr != "" { + assert.Error(t, err) + assert.ErrorString(t, err, expErr) + } else { + assert.Success(t, err) + } + + got := struct { + Graph *d2graph.Graph `json:"graph"` + Err error `json:"err"` + }{ + Graph: g, + Err: err, + } + + err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got) + assert.Success(t, err) + return g +} diff --git a/d2exporter/export.go b/d2exporter/export.go index 8a636759e..06628af70 100644 --- a/d2exporter/export.go +++ b/d2exporter/export.go @@ -4,18 +4,20 @@ import ( "context" "strconv" + "oss.terrastruct.com/util-go/go2" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/d2themes/d2themescatalog" - "oss.terrastruct.com/util-go/go2" ) func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) { theme := d2themescatalog.Find(themeID) diagram := d2target.NewDiagram() + diagram.Name = g.Name if fontFamily == nil { fontFamily = go2.Pointer(d2fonts.SourceSansPro) } diff --git a/d2format/format.go b/d2format/format.go index a45d55f63..e979a6fea 100644 --- a/d2format/format.go +++ b/d2format/format.go @@ -397,3 +397,15 @@ func (p *printer) edgeIndex(ei *d2ast.EdgeIndex) { } p.sb.WriteByte(']') } + +func KeyPath(kp *d2ast.KeyPath) (ida []string) { + for _, s := range kp.Path { + // We format each string of the key to ensure the resulting strings can be parsed + // correctly. + n := &d2ast.KeyPath{ + Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()}, + } + ida = append(ida, Format(n)) + } + return ida +} diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index e7f9035e8..9a89ed7e3 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "net/url" + "sort" "strconv" "strings" @@ -24,25 +25,26 @@ import ( const INNER_LABEL_PADDING int = 5 const DEFAULT_SHAPE_PADDING = 100. -// TODO: Refactor with a light abstract layer on top of AST implementing scenarios, -// variables, imports, substitutions and then a final set of structures representing -// a final graph. type Graph struct { - AST *d2ast.Map `json:"ast"` + Name string `json:"name"` + AST *d2ast.Map `json:"ast"` Root *Object `json:"root"` Edges []*Edge `json:"edges"` Objects []*Object `json:"objects"` + + Layers []*Graph `json:"layers,omitempty"` + Scenarios []*Graph `json:"scenarios,omitempty"` + Steps []*Graph `json:"steps,omitempty"` } -func NewGraph(ast *d2ast.Map) *Graph { - d := &Graph{ - AST: ast, - } +func NewGraph() *Graph { + d := &Graph{} d.Root = &Object{ - Graph: d, - Parent: nil, - Children: make(map[string]*Object), + Graph: d, + Parent: nil, + Children: make(map[string]*Object), + Attributes: &Attributes{}, } return d } @@ -82,7 +84,7 @@ type Object struct { Children map[string]*Object `json:"-"` ChildrenArray []*Object `json:"-"` - Attributes Attributes `json:"attributes"` + Attributes *Attributes `json:"attributes,omitempty"` ZIndex int `json:"zIndex"` } @@ -105,7 +107,8 @@ type Attributes struct { // TODO: default to ShapeRectangle instead of empty string Shape Scalar `json:"shape"` - Direction Scalar `json:"direction"` + Direction Scalar `json:"direction"` + Constraint Scalar `json:"constraint"` } // TODO references at the root scope should have their Scope set to root graph AST @@ -116,9 +119,7 @@ type Reference struct { MapKey *d2ast.Key `json:"-"` MapKeyEdgeIndex int `json:"map_key_edge_index"` Scope *d2ast.Map `json:"-"` - // The ScopeObj and UnresolvedScopeObj are the same except when the key contains underscores - ScopeObj *Object `json:"-"` - UnresolvedScopeObj *Object `json:"-"` + ScopeObj *Object `json:"-"` } func (r Reference) MapKeyEdgeDest() bool { @@ -500,10 +501,13 @@ func (obj *Object) newObject(id string) *Object { child := &Object{ ID: id, IDVal: idval, - Attributes: Attributes{ + Attributes: &Attributes{ Label: Scalar{ Value: idval, }, + Shape: Scalar{ + Value: d2target.ShapeRectangle, + }, }, Graph: obj.Graph, @@ -523,6 +527,9 @@ func (obj *Object) newObject(id string) *Object { } func (obj *Object) HasChild(ids []string) (*Object, bool) { + if len(ids) == 0 { + return obj, true + } if len(ids) == 1 && ids[0] != "style" { _, ok := ReservedKeywords[ids[0]] if ok { @@ -557,6 +564,7 @@ func (obj *Object) HasEdge(mk *d2ast.Key) (*Edge, bool) { return nil, false } +// TODO: remove once not used anywhere func ResolveUnderscoreKey(ida []string, obj *Object) (resolvedObj *Object, resolvedIDA []string, _ error) { if len(ida) > 0 && !obj.IsSequenceDiagram() { objSD := obj.OuterSequenceDiagram() @@ -637,34 +645,71 @@ func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) { return ea, true } +func (obj *Object) ensureChildEdge(ida []string) *Object { + for i := range ida { + switch obj.Attributes.Shape.Value { + case d2target.ShapeClass, d2target.ShapeSQLTable: + // This will only be called for connecting edges where we want to truncate to the + // container. + return obj + default: + obj = obj.EnsureChild(ida[i : i+1]) + } + } + return obj +} + // EnsureChild grabs the child by ids or creates it if it does not exist including all // intermediate nodes. -func (obj *Object) EnsureChild(ids []string) *Object { - _, is := ReservedKeywordHolders[ids[0]] - if len(ids) == 1 && !is { - _, ok := ReservedKeywords[ids[0]] +func (obj *Object) EnsureChild(ida []string) *Object { + seq := obj.OuterSequenceDiagram() + if seq != nil { + for _, c := range seq.ChildrenArray { + if c.ID == ida[0] { + if obj.ID == ida[0] { + // In cases of a.a where EnsureChild is called on the parent a, the second a should + // be created as a child of a and not as a child of the diagram. This is super + // unfortunate code but alas. + break + } + obj = seq + break + } + } + } + + if len(ida) == 0 { + return obj + } + + _, is := ReservedKeywordHolders[ida[0]] + if len(ida) == 1 && !is { + _, ok := ReservedKeywords[ida[0]] if ok { return obj } } - id := ids[0] - ids = ids[1:] + id := ida[0] + ida = ida[1:] + + if id == "_" { + return obj.Parent.EnsureChild(ida) + } child, ok := obj.Children[strings.ToLower(id)] if !ok { child = obj.newObject(id) } - if len(ids) >= 1 { - return child.EnsureChild(ids) + if len(ida) >= 1 { + return child.EnsureChild(ida) } return child } func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *Object) { - ref.ScopeObj = obj - ref.UnresolvedScopeObj = unresolvedObj + ref.ScopeObj = unresolvedObj numUnderscores := 0 for i := range ida { if ida[i] == "_" { @@ -866,7 +911,7 @@ type Edge struct { DstArrowhead *Attributes `json:"dstArrowhead,omitempty"` References []EdgeReference `json:"references,omitempty"` - Attributes Attributes `json:"attributes"` + Attributes *Attributes `json:"attributes,omitempty"` ZIndex int `json:"zIndex"` } @@ -938,15 +983,6 @@ func (e *Edge) AbsID() string { } func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label string) (*Edge, error) { - srcObj, srcID, err := ResolveUnderscoreKey(srcID, obj) - if err != nil { - return nil, err - } - dstObj, dstID, err := ResolveUnderscoreKey(dstID, obj) - if err != nil { - return nil, err - } - for _, id := range [][]string{srcID, dstID} { for _, p := range id { if _, ok := ReservedKeywords[p]; ok { @@ -955,15 +991,15 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label } } - src := srcObj.EnsureChild(srcID) - dst := dstObj.EnsureChild(dstID) + src := obj.ensureChildEdge(srcID) + dst := obj.ensureChildEdge(dstID) if src.OuterSequenceDiagram() != dst.OuterSequenceDiagram() { return nil, errors.New("connections within sequence diagrams can connect only to other objects within the same sequence diagram") } - edge := &Edge{ - Attributes: Attributes{ + e := &Edge{ + Attributes: &Attributes{ Label: Scalar{ Value: label, }, @@ -973,10 +1009,47 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label Dst: dst, DstArrow: dstArrow, } - edge.initIndex() + e.initIndex() - obj.Graph.Edges = append(obj.Graph.Edges, edge) - return edge, nil + addSQLTableColumnIndices(e, srcID, dstID, obj, src, dst) + + obj.Graph.Edges = append(obj.Graph.Edges, e) + return e, nil +} + +func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) { + if src.Attributes.Shape.Value == d2target.ShapeSQLTable { + if src == dst { + // Ignore edge to column inside table. + return + } + objAbsID := obj.AbsIDArray() + srcAbsID := src.AbsIDArray() + if len(objAbsID)+len(srcID) > len(srcAbsID) { + for i, d2col := range src.SQLTable.Columns { + if d2col.Name.Label == srcID[len(srcID)-1] { + d2col.Reference = dst.AbsID() + e.SrcTableColumnIndex = new(int) + *e.SrcTableColumnIndex = i + break + } + } + } + } + if dst.Attributes.Shape.Value == d2target.ShapeSQLTable { + objAbsID := obj.AbsIDArray() + dstAbsID := dst.AbsIDArray() + if len(objAbsID)+len(dstID) > len(dstAbsID) { + for i, d2col := range dst.SQLTable.Columns { + if d2col.Name.Label == dstID[len(dstID)-1] { + d2col.Reference = dst.AbsID() + e.DstTableColumnIndex = new(int) + *e.DstTableColumnIndex = i + break + } + } + } + } } // TODO: Treat undirectional/bidirectional edge here and in HasEdge flipped. Same with @@ -1252,19 +1325,17 @@ func (g *Graph) Texts() []*d2target.MText { } func Key(k *d2ast.KeyPath) []string { - var ids []string - for _, s := range k.Path { - // We format each string of the key to ensure the resulting strings can be parsed - // correctly. - n := &d2ast.KeyPath{ - Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()}, - } - ids = append(ids, d2format.Format(n)) - } - return ids + return d2format.KeyPath(k) } -var ReservedKeywords = map[string]struct{}{ +// All reserved keywords. See init below. +var ReservedKeywords map[string]struct{} + +// All reserved keywords not including style keywords. +var ReservedKeywords2 map[string]struct{} + +// Non Style/Holder keywords. +var SimpleReservedKeywords = map[string]struct{}{ "label": {}, "desc": {}, "shape": {}, @@ -1331,15 +1402,88 @@ var NearConstantsArray = []string{ } var NearConstants map[string]struct{} +// BoardKeywords contains the keywords that create new boards. +var BoardKeywords = map[string]struct{}{ + "layers": {}, + "scenarios": {}, + "steps": {}, +} + func init() { + ReservedKeywords = make(map[string]struct{}) + for k, v := range SimpleReservedKeywords { + ReservedKeywords[k] = v + } for k, v := range StyleKeywords { ReservedKeywords[k] = v } for k, v := range ReservedKeywordHolders { ReservedKeywords[k] = v } + for k, v := range BoardKeywords { + ReservedKeywords[k] = v + } + + ReservedKeywords2 = make(map[string]struct{}) + for k, v := range SimpleReservedKeywords { + ReservedKeywords2[k] = v + } + for k, v := range ReservedKeywordHolders { + ReservedKeywords2[k] = v + } + for k, v := range BoardKeywords { + ReservedKeywords2[k] = v + } + NearConstants = make(map[string]struct{}, len(NearConstantsArray)) for _, k := range NearConstantsArray { NearConstants[k] = struct{}{} } } + +func (g *Graph) GetBoard(name string) *Graph { + for _, l := range g.Layers { + if l.Name == name { + return l + } + } + for _, l := range g.Scenarios { + if l.Name == name { + return l + } + } + for _, l := range g.Steps { + if l.Name == name { + return l + } + } + return nil +} + +func (g *Graph) SortObjectsByAST() { + objects := append([]*Object(nil), g.Objects...) + sort.Slice(objects, func(i, j int) bool { + o1 := objects[i] + o2 := objects[j] + if len(o1.References) == 0 || len(o2.References) == 0 { + return i < j + } + r1 := o1.References[0] + r2 := o2.References[0] + return r1.Key.Path[r1.KeyPathIndex].Unbox().GetRange().Before(r2.Key.Path[r2.KeyPathIndex].Unbox().GetRange()) + }) + g.Objects = objects +} + +func (g *Graph) SortEdgesByAST() { + edges := append([]*Edge(nil), g.Edges...) + sort.Slice(edges, func(i, j int) bool { + e1 := edges[i] + e2 := edges[j] + if len(e1.References) == 0 || len(e2.References) == 0 { + return i < j + } + return e1.References[0].Edge.Range.Before(e2.References[0].Edge.Range) + }) + g.Edges = edges +} diff --git a/d2graph/seqdiagram.go b/d2graph/seqdiagram.go index 15d7bc266..b9cddac8d 100644 --- a/d2graph/seqdiagram.go +++ b/d2graph/seqdiagram.go @@ -3,7 +3,7 @@ package d2graph import "oss.terrastruct.com/d2/d2target" func (obj *Object) IsSequenceDiagram() bool { - return obj != nil && obj.Attributes.Shape.Value == d2target.ShapeSequenceDiagram + return obj != nil && obj.Attributes != nil && obj.Attributes.Shape.Value == d2target.ShapeSequenceDiagram } func (obj *Object) OuterSequenceDiagram() *Object { @@ -65,7 +65,7 @@ func (obj *Object) ContainsAnyObject(objects []*Object) bool { func (o *Object) ContainedBy(obj *Object) bool { for _, ref := range o.References { - curr := ref.UnresolvedScopeObj + curr := ref.ScopeObj for curr != nil { if curr == obj { return true diff --git a/d2ir/compile.go b/d2ir/compile.go new file mode 100644 index 000000000..03cc04b85 --- /dev/null +++ b/d2ir/compile.go @@ -0,0 +1,252 @@ +package d2ir + +import ( + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2parser" +) + +type compiler struct { + err d2parser.ParseError +} + +func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { + c.err.Errors = append(c.err.Errors, d2parser.Errorf(n, f, v...).(d2ast.Error)) +} + +func Compile(ast *d2ast.Map) (*Map, error) { + c := &compiler{} + m := &Map{} + m.initRoot() + m.parent.(*Field).References[0].Context.Scope = ast + c.compileMap(m, ast) + c.compileScenarios(m) + c.compileSteps(m) + if !c.err.Empty() { + return nil, c.err + } + return m, nil +} + +func (c *compiler) compileScenarios(m *Map) { + scenariosf := m.GetField("scenarios") + if scenariosf == nil { + return + } + scenarios := scenariosf.Map() + if scenarios == nil { + return + } + + for _, sf := range scenarios.Fields { + if sf.Map() == nil { + continue + } + base := m.CopyBase(sf) + OverlayMap(base, sf.Map()) + sf.Composite = base + c.compileScenarios(sf.Map()) + c.compileSteps(sf.Map()) + } +} + +func (c *compiler) compileSteps(m *Map) { + stepsf := m.GetField("steps") + if stepsf == nil { + return + } + steps := stepsf.Map() + if steps == nil { + return + } + for i, sf := range steps.Fields { + if sf.Map() == nil { + continue + } + var base *Map + if i == 0 { + base = m.CopyBase(sf) + } else { + base = steps.Fields[i-1].Map().CopyBase(sf) + } + OverlayMap(base, sf.Map()) + sf.Composite = base + c.compileScenarios(sf.Map()) + c.compileSteps(sf.Map()) + } +} + +func (c *compiler) compileMap(dst *Map, ast *d2ast.Map) { + for _, n := range ast.Nodes { + switch { + case n.MapKey != nil: + c.compileKey(&RefContext{ + Key: n.MapKey, + Scope: ast, + ScopeMap: dst, + }) + case n.Substitution != nil: + panic("TODO") + } + } +} + +func (c *compiler) compileKey(refctx *RefContext) { + if len(refctx.Key.Edges) == 0 { + c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx) + } else { + c.compileEdges(refctx) + } +} + +func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) { + f, err := dst.EnsureField(kp, refctx) + if err != nil { + c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) + return + } + + if refctx.Key.Primary.Unbox() != nil { + f.Primary_ = &Scalar{ + parent: f, + Value: refctx.Key.Primary.Unbox(), + } + } + if refctx.Key.Value.Array != nil { + a := &Array{ + parent: f, + } + c.compileArray(a, refctx.Key.Value.Array) + f.Composite = a + } else if refctx.Key.Value.Map != nil { + if f.Map() == nil { + f.Composite = &Map{ + parent: f, + } + } + c.compileMap(f.Map(), refctx.Key.Value.Map) + } else if refctx.Key.Value.ScalarBox().Unbox() != nil { + f.Primary_ = &Scalar{ + parent: f, + Value: refctx.Key.Value.ScalarBox().Unbox(), + } + } +} + +func (c *compiler) compileEdges(refctx *RefContext) { + if refctx.Key.Key != nil { + f, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx) + if err != nil { + c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) + return + } + if _, ok := f.Composite.(*Array); ok { + c.errorf(refctx.Key.Key, "cannot index into array") + return + } + if f.Map() == nil { + f.Composite = &Map{ + parent: f, + } + } + refctx.ScopeMap = f.Map() + } + + eida := NewEdgeIDs(refctx.Key) + for i, eid := range eida { + refctx = refctx.Copy() + refctx.Edge = refctx.Key.Edges[i] + + var e *Edge + if eid.Index != nil { + ea := refctx.ScopeMap.GetEdges(eid) + if len(ea) == 0 { + c.errorf(refctx.Edge, "indexed edge does not exist") + continue + } + e = ea[0] + e.References = append(e.References, &EdgeReference{ + Context: refctx, + }) + refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx) + refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx) + } else { + _, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx) + if err != nil { + c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) + continue + } + _, err = refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx) + if err != nil { + c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) + continue + } + + e, err = refctx.ScopeMap.CreateEdge(eid, refctx) + if err != nil { + c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) + continue + } + } + + if refctx.Key.EdgeKey != nil { + if e.Map_ == nil { + e.Map_ = &Map{ + parent: e, + } + } + c.compileField(e.Map_, refctx.Key.EdgeKey, refctx) + } else { + if refctx.Key.Primary.Unbox() != nil { + e.Primary_ = &Scalar{ + parent: e, + Value: refctx.Key.Primary.Unbox(), + } + } + if refctx.Key.Value.Array != nil { + c.errorf(refctx.Key.Value.Unbox(), "edges cannot be assigned arrays") + continue + } else if refctx.Key.Value.Map != nil { + if e.Map_ == nil { + e.Map_ = &Map{ + parent: e, + } + } + c.compileMap(e.Map_, refctx.Key.Value.Map) + } else if refctx.Key.Value.ScalarBox().Unbox() != nil { + e.Primary_ = &Scalar{ + parent: e, + Value: refctx.Key.Value.ScalarBox().Unbox(), + } + } + } + } +} + +func (c *compiler) compileArray(dst *Array, a *d2ast.Array) { + for _, an := range a.Nodes { + var irv Value + switch v := an.Unbox().(type) { + case *d2ast.Array: + ira := &Array{ + parent: dst, + } + c.compileArray(ira, v) + irv = ira + case *d2ast.Map: + irm := &Map{ + parent: dst, + } + c.compileMap(irm, v) + irv = irm + case d2ast.Scalar: + irv = &Scalar{ + parent: dst, + Value: v, + } + case *d2ast.Substitution: + // panic("TODO") + } + + dst.Values = append(dst.Values, irv) + } +} diff --git a/d2ir/compile_test.go b/d2ir/compile_test.go new file mode 100644 index 000000000..bd21d9ae2 --- /dev/null +++ b/d2ir/compile_test.go @@ -0,0 +1,476 @@ +package d2ir_test + +import ( + "fmt" + "math/big" + "path/filepath" + "strings" + "testing" + + "oss.terrastruct.com/util-go/assert" + "oss.terrastruct.com/util-go/diff" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2ir" + "oss.terrastruct.com/d2/d2parser" +) + +func TestCompile(t *testing.T) { + t.Parallel() + + t.Run("fields", testCompileFields) + t.Run("edges", testCompileEdges) + t.Run("layers", testCompileLayers) + t.Run("scenarios", testCompileScenarios) + t.Run("steps", testCompileSteps) +} + +type testCase struct { + name string + run func(testing.TB) +} + +func runa(t *testing.T, tca []testCase) { + for _, tc := range tca { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.run(t) + }) + } +} + +func compile(t testing.TB, text string) (*d2ir.Map, error) { + t.Helper() + + d2Path := fmt.Sprintf("%v.d2", t.Name()) + ast, err := d2parser.Parse(d2Path, strings.NewReader(text), nil) + assert.Success(t, err) + + m, err := d2ir.Compile(ast) + if err != nil { + return nil, err + } + + err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2ir", t.Name()), m) + if err != nil { + return nil, err + } + return m, nil +} + +func assertQuery(t testing.TB, n d2ir.Node, nfields, nedges int, primary interface{}, idStr string) d2ir.Node { + t.Helper() + + m := n.Map() + p := n.Primary() + + if idStr != "" { + var err error + n, err = m.Query(idStr) + assert.Success(t, err) + assert.NotEqual(t, n, nil) + + p = n.Primary() + m = n.Map() + } + + assert.Equal(t, nfields, m.FieldCountRecursive()) + assert.Equal(t, nedges, m.EdgeCountRecursive()) + if !makeScalar(p).Equal(makeScalar(primary)) { + t.Fatalf("expected primary %#v but got %s", primary, p) + } + + return n +} + +func makeScalar(v interface{}) *d2ir.Scalar { + s := &d2ir.Scalar{} + switch v := v.(type) { + case *d2ir.Scalar: + if v == nil { + s.Value = &d2ast.Null{} + return s + } + return v + case bool: + s.Value = &d2ast.Boolean{ + Value: v, + } + case float64: + bv := &big.Rat{} + bv.SetFloat64(v) + s.Value = &d2ast.Number{ + Value: bv, + } + case int: + s.Value = &d2ast.Number{ + Value: big.NewRat(int64(v), 1), + } + case string: + s.Value = d2ast.FlatDoubleQuotedString(v) + default: + if v != nil { + panic(fmt.Sprintf("d2ir: unexpected type to makeScalar: %#v", v)) + } + s.Value = &d2ast.Null{} + } + return s +} + +func testCompileFields(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "root", + run: func(t testing.TB) { + m, err := compile(t, `x`) + assert.Success(t, err) + assertQuery(t, m, 1, 0, nil, "") + + assertQuery(t, m, 0, 0, nil, "x") + }, + }, + { + name: "label", + run: func(t testing.TB) { + m, err := compile(t, `x: yes`) + assert.Success(t, err) + assertQuery(t, m, 1, 0, nil, "") + + assertQuery(t, m, 0, 0, "yes", "x") + }, + }, + { + name: "nested", + run: func(t testing.TB) { + m, err := compile(t, `x.y: yes`) + assert.Success(t, err) + assertQuery(t, m, 2, 0, nil, "") + + assertQuery(t, m, 1, 0, nil, "x") + assertQuery(t, m, 0, 0, "yes", "x.y") + }, + }, + { + name: "array", + run: func(t testing.TB) { + m, err := compile(t, `x: [1;2;3;4]`) + assert.Success(t, err) + assertQuery(t, m, 1, 0, nil, "") + + f := assertQuery(t, m, 0, 0, nil, "x").(*d2ir.Field) + assert.String(t, `[1; 2; 3; 4]`, f.Composite.String()) + }, + }, + { + name: "null", + run: func(t testing.TB) { + m, err := compile(t, `pq: pq +pq: null`) + assert.Success(t, err) + assertQuery(t, m, 1, 0, nil, "") + // null doesn't delete pq from *Map so that for language tooling + // we maintain the references. + // Instead d2compiler will ensure it doesn't get rendered. + assertQuery(t, m, 0, 0, nil, "pq") + }, + }, + } + runa(t, tca) + t.Run("primary", func(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "root", + run: func(t testing.TB) { + m, err := compile(t, `x: yes { pqrs }`) + assert.Success(t, err) + assertQuery(t, m, 2, 0, nil, "") + + assertQuery(t, m, 1, 0, "yes", "x") + assertQuery(t, m, 0, 0, nil, "x.pqrs") + }, + }, + { + name: "nested", + run: func(t testing.TB) { + m, err := compile(t, `x.y: yes { pqrs }`) + assert.Success(t, err) + assertQuery(t, m, 3, 0, nil, "") + + assertQuery(t, m, 2, 0, nil, "x") + assertQuery(t, m, 1, 0, "yes", "x.y") + assertQuery(t, m, 0, 0, nil, "x.y.pqrs") + }, + }, + } + runa(t, tca) + }) +} + +func testCompileEdges(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "root", + run: func(t testing.TB) { + m, err := compile(t, `x -> y`) + assert.Success(t, err) + assertQuery(t, m, 2, 1, nil, "") + assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`) + + assertQuery(t, m, 0, 0, nil, "x") + assertQuery(t, m, 0, 0, nil, "y") + }, + }, + { + name: "nested", + run: func(t testing.TB) { + m, err := compile(t, `x.y -> z.p`) + assert.Success(t, err) + assertQuery(t, m, 4, 1, nil, "") + + assertQuery(t, m, 1, 0, nil, "x") + assertQuery(t, m, 0, 0, nil, "x.y") + + assertQuery(t, m, 1, 0, nil, "z") + assertQuery(t, m, 0, 0, nil, "z.p") + + assertQuery(t, m, 0, 0, nil, "(x.y -> z.p)[0]") + }, + }, + { + name: "underscore", + run: func(t testing.TB) { + m, err := compile(t, `p: { _.x -> z }`) + assert.Success(t, err) + assertQuery(t, m, 3, 1, nil, "") + + assertQuery(t, m, 0, 0, nil, "x") + assertQuery(t, m, 1, 0, nil, "p") + + assertQuery(t, m, 0, 0, nil, "(x -> p.z)[0]") + }, + }, + { + name: "chain", + run: func(t testing.TB) { + m, err := compile(t, `a -> b -> c -> d`) + assert.Success(t, err) + assertQuery(t, m, 4, 3, nil, "") + + assertQuery(t, m, 0, 0, nil, "a") + assertQuery(t, m, 0, 0, nil, "b") + assertQuery(t, m, 0, 0, nil, "c") + assertQuery(t, m, 0, 0, nil, "d") + assertQuery(t, m, 0, 0, nil, "(a -> b)[0]") + assertQuery(t, m, 0, 0, nil, "(b -> c)[0]") + assertQuery(t, m, 0, 0, nil, "(c -> d)[0]") + }, + }, + } + runa(t, tca) + t.Run("errs", func(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "bad_edge", + run: func(t testing.TB) { + _, err := compile(t, `(x -> y): { p -> q }`) + assert.ErrorString(t, err, `TestCompile/edges/errs/bad_edge.d2:1:13: cannot create edge inside edge`) + }, + }, + } + runa(t, tca) + }) +} + +func testCompileLayers(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "root", + run: func(t testing.TB) { + m, err := compile(t, `x -> y +layers: { + bingo: { p.q.z } +}`) + assert.Success(t, err) + + assertQuery(t, m, 7, 1, nil, "") + assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`) + + assertQuery(t, m, 0, 0, nil, "x") + assertQuery(t, m, 0, 0, nil, "y") + + assertQuery(t, m, 3, 0, nil, "layers.bingo") + }, + }, + } + runa(t, tca) + t.Run("errs", func(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "1/bad_edge", + run: func(t testing.TB) { + _, err := compile(t, `layers.x -> layers.y`) + assert.ErrorString(t, err, `TestCompile/layers/errs/1/bad_edge.d2:1:1: cannot create edges between boards`) + }, + }, + { + name: "2/bad_edge", + run: func(t testing.TB) { + _, err := compile(t, `layers -> scenarios`) + assert.ErrorString(t, err, `TestCompile/layers/errs/2/bad_edge.d2:1:1: edge with board keyword alone doesn't make sense`) + }, + }, + { + name: "3/bad_edge", + run: func(t testing.TB) { + _, err := compile(t, `layers.x.y -> steps.z.p`) + assert.ErrorString(t, err, `TestCompile/layers/errs/3/bad_edge.d2:1:1: cannot create edges between boards`) + }, + }, + { + name: "4/good_edge", + run: func(t testing.TB) { + _, err := compile(t, `layers.x.y -> layers.x.y`) + assert.Success(t, err) + }, + }, + } + runa(t, tca) + }) +} + +func testCompileScenarios(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "root", + run: func(t testing.TB) { + m, err := compile(t, `x -> y +scenarios: { + bingo: { p.q.z } + nuclear: { quiche } +}`) + assert.Success(t, err) + + assertQuery(t, m, 13, 3, nil, "") + + assertQuery(t, m, 0, 0, nil, "x") + assertQuery(t, m, 0, 0, nil, "y") + assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`) + + assertQuery(t, m, 5, 1, nil, "scenarios.bingo") + assertQuery(t, m, 0, 0, nil, "scenarios.bingo.x") + assertQuery(t, m, 0, 0, nil, "scenarios.bingo.y") + assertQuery(t, m, 0, 0, nil, `scenarios.bingo.(x -> y)[0]`) + assertQuery(t, m, 2, 0, nil, "scenarios.bingo.p") + assertQuery(t, m, 1, 0, nil, "scenarios.bingo.p.q") + assertQuery(t, m, 0, 0, nil, "scenarios.bingo.p.q.z") + + assertQuery(t, m, 3, 1, nil, "scenarios.nuclear") + assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.x") + assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.y") + assertQuery(t, m, 0, 0, nil, `scenarios.nuclear.(x -> y)[0]`) + assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.quiche") + }, + }, + } + runa(t, tca) +} + +func testCompileSteps(t *testing.T) { + t.Parallel() + tca := []testCase{ + { + name: "root", + run: func(t testing.TB) { + m, err := compile(t, `x -> y +steps: { + bingo: { p.q.z } + nuclear: { quiche } +}`) + assert.Success(t, err) + + assertQuery(t, m, 16, 3, nil, "") + + assertQuery(t, m, 0, 0, nil, "x") + assertQuery(t, m, 0, 0, nil, "y") + assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`) + + assertQuery(t, m, 5, 1, nil, "steps.bingo") + assertQuery(t, m, 0, 0, nil, "steps.bingo.x") + assertQuery(t, m, 0, 0, nil, "steps.bingo.y") + assertQuery(t, m, 0, 0, nil, `steps.bingo.(x -> y)[0]`) + assertQuery(t, m, 2, 0, nil, "steps.bingo.p") + assertQuery(t, m, 1, 0, nil, "steps.bingo.p.q") + assertQuery(t, m, 0, 0, nil, "steps.bingo.p.q.z") + + assertQuery(t, m, 6, 1, nil, "steps.nuclear") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.x") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.y") + assertQuery(t, m, 0, 0, nil, `steps.nuclear.(x -> y)[0]`) + assertQuery(t, m, 2, 0, nil, "steps.nuclear.p") + assertQuery(t, m, 1, 0, nil, "steps.nuclear.p.q") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.p.q.z") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche") + }, + }, + { + name: "recursive", + run: func(t testing.TB) { + m, err := compile(t, `x -> y +steps: { + bingo: { p.q.z } + nuclear: { + quiche + scenarios: { + bavarian: { + perseverance + } + } + } +}`) + assert.Success(t, err) + + assertQuery(t, m, 25, 4, nil, "") + + assertQuery(t, m, 0, 0, nil, "x") + assertQuery(t, m, 0, 0, nil, "y") + assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`) + + assertQuery(t, m, 5, 1, nil, "steps.bingo") + assertQuery(t, m, 0, 0, nil, "steps.bingo.x") + assertQuery(t, m, 0, 0, nil, "steps.bingo.y") + assertQuery(t, m, 0, 0, nil, `steps.bingo.(x -> y)[0]`) + assertQuery(t, m, 2, 0, nil, "steps.bingo.p") + assertQuery(t, m, 1, 0, nil, "steps.bingo.p.q") + assertQuery(t, m, 0, 0, nil, "steps.bingo.p.q.z") + + assertQuery(t, m, 15, 2, nil, "steps.nuclear") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.x") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.y") + assertQuery(t, m, 0, 0, nil, `steps.nuclear.(x -> y)[0]`) + assertQuery(t, m, 2, 0, nil, "steps.nuclear.p") + assertQuery(t, m, 1, 0, nil, "steps.nuclear.p.q") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.p.q.z") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche") + + assertQuery(t, m, 7, 1, nil, "steps.nuclear.scenarios.bavarian") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.x") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.y") + assertQuery(t, m, 0, 0, nil, `steps.nuclear.scenarios.bavarian.(x -> y)[0]`) + assertQuery(t, m, 2, 0, nil, "steps.nuclear.scenarios.bavarian.p") + assertQuery(t, m, 1, 0, nil, "steps.nuclear.scenarios.bavarian.p.q") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.p.q.z") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.quiche") + assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.perseverance") + }, + }, + } + runa(t, tca) +} diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go new file mode 100644 index 000000000..1a90d8d5c --- /dev/null +++ b/d2ir/d2ir.go @@ -0,0 +1,1061 @@ +// Package d2ir implements a tree data structure to keep track of the resolved value of D2 +// keys. +package d2ir + +import ( + "errors" + "fmt" + "strings" + + "oss.terrastruct.com/util-go/go2" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2parser" +) + +// Most errors returned by a node should be created with d2parser.Errorf +// to indicate the offending AST node. +type Node interface { + node() + Copy(newParent Node) Node + Parent() Node + Primary() *Scalar + Map() *Map + + ast() d2ast.Node + fmt.Stringer + + LastRef() Reference + LastPrimaryKey() *d2ast.Key +} + +var _ Node = &Scalar{} +var _ Node = &Field{} +var _ Node = &Edge{} +var _ Node = &Array{} +var _ Node = &Map{} + +type Value interface { + Node + value() +} + +var _ Value = &Scalar{} +var _ Value = &Array{} +var _ Value = &Map{} + +type Composite interface { + Node + Value + composite() +} + +var _ Composite = &Array{} +var _ Composite = &Map{} + +func (n *Scalar) node() {} +func (n *Field) node() {} +func (n *Edge) node() {} +func (n *Array) node() {} +func (n *Map) node() {} + +func (n *Scalar) Parent() Node { return n.parent } +func (n *Field) Parent() Node { return n.parent } +func (n *Edge) Parent() Node { return n.parent } +func (n *Array) Parent() Node { return n.parent } +func (n *Map) Parent() Node { return n.parent } + +func (n *Scalar) Primary() *Scalar { return n } +func (n *Field) Primary() *Scalar { return n.Primary_ } +func (n *Edge) Primary() *Scalar { return n.Primary_ } +func (n *Array) Primary() *Scalar { return nil } +func (n *Map) Primary() *Scalar { return nil } + +func (n *Scalar) Map() *Map { return nil } +func (n *Field) Map() *Map { + if n == nil { + return nil + } + if n.Composite == nil { + return nil + } + return n.Composite.Map() +} +func (n *Edge) Map() *Map { + if n == nil { + return nil + } + return n.Map_ +} +func (n *Array) Map() *Map { return nil } +func (n *Map) Map() *Map { return n } + +func (n *Scalar) value() {} +func (n *Array) value() {} +func (n *Map) value() {} + +func (n *Array) composite() {} +func (n *Map) composite() {} + +func (n *Scalar) String() string { return d2format.Format(n.ast()) } +func (n *Field) String() string { return d2format.Format(n.ast()) } +func (n *Edge) String() string { return d2format.Format(n.ast()) } +func (n *Array) String() string { return d2format.Format(n.ast()) } +func (n *Map) String() string { return d2format.Format(n.ast()) } + +func (n *Scalar) LastRef() Reference { return parentRef(n) } +func (n *Map) LastRef() Reference { return parentRef(n) } +func (n *Array) LastRef() Reference { return parentRef(n) } + +func (n *Scalar) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) } +func (n *Map) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) } +func (n *Array) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) } + +type Reference interface { + reference() + // Most specific AST node for the reference. + AST() d2ast.Node + Primary() bool +} + +var _ Reference = &FieldReference{} +var _ Reference = &EdgeReference{} + +func (r *FieldReference) reference() {} +func (r *EdgeReference) reference() {} + +type Scalar struct { + parent Node + Value d2ast.Scalar `json:"value"` +} + +func (s *Scalar) Copy(newParent Node) Node { + tmp := *s + s = &tmp + + s.parent = newParent + return s +} + +func (s *Scalar) Equal(s2 *Scalar) bool { + if _, ok := s.Value.(d2ast.String); ok { + if _, ok = s2.Value.(d2ast.String); ok { + return s.Value.ScalarString() == s2.Value.ScalarString() + } + } + return s.Value.Type() == s2.Value.Type() && s.Value.ScalarString() == s2.Value.ScalarString() +} + +type Map struct { + parent Node + Fields []*Field `json:"fields"` + Edges []*Edge `json:"edges"` +} + +func (m *Map) initRoot() { + m.parent = &Field{ + Name: "", + References: []*FieldReference{{ + Context: &RefContext{ + ScopeMap: m, + }, + }}, + } +} + +func (m *Map) Copy(newParent Node) Node { + tmp := *m + m = &tmp + + m.parent = newParent + pfields := m.Fields + m.Fields = make([]*Field, 0, len(pfields)) + for _, f := range pfields { + m.Fields = append(m.Fields, f.Copy(m).(*Field)) + } + m.Edges = append([]*Edge(nil), m.Edges...) + for i := range m.Edges { + m.Edges[i] = m.Edges[i].Copy(m).(*Edge) + } + if m.parent == nil { + m.initRoot() + } + return m +} + +// CopyBase copies the map m without layers/scenarios/steps. +func (m *Map) CopyBase(newParent Node) *Map { + layers := m.DeleteField("layers") + scenarios := m.DeleteField("scenarios") + steps := m.DeleteField("steps") + m2 := m.Copy(newParent).(*Map) + if layers != nil { + m.Fields = append(m.Fields, layers) + } + if scenarios != nil { + m.Fields = append(m.Fields, scenarios) + } + if steps != nil { + m.Fields = append(m.Fields, steps) + } + return m2 +} + +// Root reports whether the Map is the root of the D2 tree. +func (m *Map) Root() bool { + // m.parent exists even on the root map as we store the root AST in + // m.parent.References[0].Context.Map for reporting error messages about the whole IR. + // Or if otherwise needed. + f, ok := m.parent.(*Field) + if !ok { + return false + } + return f.Root() +} + +func (f *Field) Root() bool { + return f.parent == nil +} + +type BoardKind string + +const ( + BoardLayer BoardKind = "layer" + BoardScenario BoardKind = "scenario" + BoardStep BoardKind = "step" +) + +// NodeBoardKind reports whether n represents the root of a board. +// n should be *Field or *Map +func NodeBoardKind(n Node) BoardKind { + var f *Field + switch n := n.(type) { + case *Field: + if n.Name == "" { + return BoardLayer + } + f = ParentField(n) + case *Map: + f = ParentField(n) + if f.Root() { + return BoardLayer + } + f = ParentField(f) + } + if f == nil { + return "" + } + switch f.Name { + case "layers": + return BoardLayer + case "scenarios": + return BoardScenario + case "steps": + return BoardStep + default: + return "" + } +} + +type Field struct { + // *Map. + parent Node + + Name string `json:"name"` + + // Primary_ to avoid clashing with Primary(). We need to keep it exported for + // encoding/json to marshal it so cannot prefix _ instead. + Primary_ *Scalar `json:"primary,omitempty"` + Composite Composite `json:"composite,omitempty"` + + References []*FieldReference `json:"references,omitempty"` +} + +func (f *Field) Copy(newParent Node) Node { + tmp := *f + f = &tmp + + f.parent = newParent + f.References = append([]*FieldReference(nil), f.References...) + if f.Primary_ != nil { + f.Primary_ = f.Primary_.Copy(f).(*Scalar) + } + if f.Composite != nil { + f.Composite = f.Composite.Copy(f).(Composite) + } + return f +} + +func (f *Field) lastPrimaryRef() *FieldReference { + for i := len(f.References) - 1; i >= 0; i-- { + if f.References[i].Primary() { + return f.References[i] + } + } + return nil +} + +func (f *Field) LastPrimaryKey() *d2ast.Key { + fr := f.lastPrimaryRef() + if fr == nil { + return nil + } + return fr.Context.Key +} + +func (f *Field) LastRef() Reference { + return f.References[len(f.References)-1] +} + +type EdgeID struct { + SrcPath []string `json:"src_path"` + SrcArrow bool `json:"src_arrow"` + + DstPath []string `json:"dst_path"` + DstArrow bool `json:"dst_arrow"` + + // If nil, then any EdgeID with equal src/dst/arrows matches. + Index *int `json:"index"` +} + +func NewEdgeIDs(k *d2ast.Key) (eida []*EdgeID) { + for _, ke := range k.Edges { + eid := &EdgeID{ + SrcPath: ke.Src.IDA(), + SrcArrow: ke.SrcArrow == "<", + DstPath: ke.Dst.IDA(), + DstArrow: ke.DstArrow == ">", + } + if k.EdgeIndex != nil { + eid.Index = k.EdgeIndex.Int + } + eida = append(eida, eid) + } + return eida +} + +func (eid *EdgeID) Copy() *EdgeID { + tmp := *eid + eid = &tmp + + eid.SrcPath = append([]string(nil), eid.SrcPath...) + eid.DstPath = append([]string(nil), eid.DstPath...) + return eid +} + +func (eid *EdgeID) Match(eid2 *EdgeID) bool { + if eid.Index != nil && eid2.Index != nil { + if *eid.Index != *eid2.Index { + return false + } + } + + if len(eid.SrcPath) != len(eid2.SrcPath) { + return false + } + if eid.SrcArrow != eid2.SrcArrow { + return false + } + for i, s := range eid.SrcPath { + if !strings.EqualFold(s, eid2.SrcPath[i]) { + return false + } + } + + if len(eid.DstPath) != len(eid2.DstPath) { + return false + } + if eid.DstArrow != eid2.DstArrow { + return false + } + for i, s := range eid.DstPath { + if !strings.EqualFold(s, eid2.DstPath[i]) { + return false + } + } + + return true +} + +func (eid *EdgeID) resolveUnderscores(m *Map) (*EdgeID, *Map, error) { + eid = eid.Copy() + maxUnderscores := go2.Max(countUnderscores(eid.SrcPath), countUnderscores(eid.DstPath)) + for i := 0; i < maxUnderscores; i++ { + if eid.SrcPath[0] == "_" { + eid.SrcPath = eid.SrcPath[1:] + } else { + mf := ParentField(m) + eid.SrcPath = append([]string{mf.Name}, eid.SrcPath...) + } + if eid.DstPath[0] == "_" { + eid.DstPath = eid.DstPath[1:] + } else { + mf := ParentField(m) + eid.DstPath = append([]string{mf.Name}, eid.DstPath...) + } + m = ParentMap(m) + if m == nil { + return nil, nil, errors.New("invalid underscore") + } + } + return eid, m, nil +} + +func (eid *EdgeID) trimCommon() (common []string, _ *EdgeID) { + eid = eid.Copy() + for len(eid.SrcPath) > 1 && len(eid.DstPath) > 1 { + if !strings.EqualFold(eid.SrcPath[0], eid.DstPath[0]) { + return common, eid + } + common = append(common, eid.SrcPath[0]) + eid.SrcPath = eid.SrcPath[1:] + eid.DstPath = eid.DstPath[1:] + } + return common, eid +} + +type Edge struct { + // *Map + parent Node + + ID *EdgeID `json:"edge_id"` + + Primary_ *Scalar `json:"primary,omitempty"` + Map_ *Map `json:"map,omitempty"` + + References []*EdgeReference `json:"references,omitempty"` +} + +func (e *Edge) Copy(newParent Node) Node { + tmp := *e + e = &tmp + + e.parent = newParent + e.References = append([]*EdgeReference(nil), e.References...) + if e.Primary_ != nil { + e.Primary_ = e.Primary_.Copy(e).(*Scalar) + } + if e.Map_ != nil { + e.Map_ = e.Map_.Copy(e).(*Map) + } + return e +} + +func (e *Edge) lastPrimaryRef() *EdgeReference { + for i := len(e.References) - 1; i >= 0; i-- { + fr := e.References[i] + if fr.Context.Key.EdgeKey == nil { + return fr + } + } + return nil +} + +func (e *Edge) LastPrimaryKey() *d2ast.Key { + er := e.lastPrimaryRef() + if er == nil { + return nil + } + return er.Context.Key +} + +func (e *Edge) LastRef() Reference { + return e.References[len(e.References)-1] +} + +type Array struct { + parent Node + Values []Value `json:"values"` +} + +func (a *Array) Copy(newParent Node) Node { + tmp := *a + a = &tmp + + a.parent = newParent + a.Values = append([]Value(nil), a.Values...) + for i := range a.Values { + a.Values[i] = a.Values[i].Copy(a).(Value) + } + return a +} + +type FieldReference struct { + String d2ast.String `json:"string"` + KeyPath *d2ast.KeyPath `json:"key_path"` + + Context *RefContext `json:"context"` +} + +// Primary returns true if the Value in Context.Key.Value corresponds to the Field +// represented by String. +func (fr *FieldReference) Primary() bool { + if fr.KeyPath == fr.Context.Key.Key { + return len(fr.Context.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1 + } else if fr.KeyPath == fr.Context.Key.EdgeKey { + return len(fr.Context.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1 + } + return false +} + +func (fr *FieldReference) KeyPathIndex() int { + for i, sb := range fr.KeyPath.Path { + if sb.Unbox() == fr.String { + return i + } + } + panic("d2ir.KeyReference.KeyPathIndex: String not in KeyPath?") +} + +func (fr *FieldReference) EdgeDest() bool { + return fr.KeyPath == fr.Context.Edge.Dst +} + +func (fr *FieldReference) InEdge() bool { + return fr.Context.Edge != nil +} + +func (fr *FieldReference) AST() d2ast.Node { + if fr.String == nil { + // Root map. + return fr.Context.Scope + } + return fr.String +} + +type EdgeReference struct { + Context *RefContext `json:"context"` +} + +func (er *EdgeReference) AST() d2ast.Node { + return er.Context.Edge +} + +// Primary returns true if the Value in Context.Key.Value corresponds to the *Edge +// represented by Context.Edge +func (er *EdgeReference) Primary() bool { + return len(er.Context.Key.Edges) == 1 && er.Context.Key.EdgeKey == nil +} + +type RefContext struct { + Edge *d2ast.Edge `json:"edge"` + Key *d2ast.Key `json:"key"` + Scope *d2ast.Map `json:"-"` + ScopeMap *Map `json:"-"` +} + +func (rc *RefContext) Copy() *RefContext { + tmp := *rc + return &tmp +} + +func (rc *RefContext) EdgeIndex() int { + for i, e := range rc.Key.Edges { + if e == rc.Edge { + return i + } + } + return -1 +} + +func (m *Map) FieldCountRecursive() int { + if m == nil { + return 0 + } + acc := len(m.Fields) + for _, f := range m.Fields { + if f.Map() != nil { + acc += f.Map().FieldCountRecursive() + } + } + for _, e := range m.Edges { + if e.Map_ != nil { + acc += e.Map_.FieldCountRecursive() + } + } + return acc +} + +func (m *Map) EdgeCountRecursive() int { + if m == nil { + return 0 + } + acc := len(m.Edges) + for _, f := range m.Fields { + if f.Map() != nil { + acc += f.Map().EdgeCountRecursive() + } + } + for _, e := range m.Edges { + if e.Map_ != nil { + acc += e.Map_.EdgeCountRecursive() + } + } + return acc +} + +func (m *Map) GetField(ida ...string) *Field { + for len(ida) > 0 && ida[0] == "_" { + m = ParentMap(m) + if m == nil { + return nil + } + } + return m.getField(ida) +} + +func (m *Map) getField(ida []string) *Field { + if len(ida) == 0 { + return nil + } + + s := ida[0] + rest := ida[1:] + + if s == "_" { + return nil + } + + for _, f := range m.Fields { + if !strings.EqualFold(f.Name, s) { + continue + } + if len(rest) == 0 { + return f + } + if f.Map() != nil { + return f.Map().getField(rest) + } + } + return nil +} + +func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext) (*Field, error) { + i := 0 + for kp.Path[i].Unbox().ScalarString() == "_" { + m = ParentMap(m) + if m == nil { + return nil, d2parser.Errorf(kp.Path[i].Unbox(), "invalid underscore: no parent") + } + if i+1 == len(kp.Path) { + return nil, d2parser.Errorf(kp.Path[i].Unbox(), "field key must contain more than underscores") + } + i++ + } + return m.ensureField(i, kp, refctx) +} + +func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext) (*Field, error) { + head := kp.Path[i].Unbox().ScalarString() + + if head == "_" { + return nil, d2parser.Errorf(kp.Path[i].Unbox(), `parent "_" can only be used in the beginning of paths, e.g. "_.x"`) + } + + if findBoardKeyword(head) != -1 && NodeBoardKind(m) == "" { + return nil, d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head) + } + + for _, f := range m.Fields { + if !strings.EqualFold(f.Name, head) { + continue + } + + // Don't add references for fake common KeyPath from trimCommon in CreateEdge. + if refctx != nil { + f.References = append(f.References, &FieldReference{ + String: kp.Path[i].Unbox(), + KeyPath: kp, + Context: refctx, + }) + } + + if i+1 == len(kp.Path) { + return f, nil + } + if _, ok := f.Composite.(*Array); ok { + return nil, d2parser.Errorf(kp.Path[i].Unbox(), "cannot index into array") + } + if f.Map() == nil { + f.Composite = &Map{ + parent: f, + } + } + return f.Map().ensureField(i+1, kp, refctx) + } + + f := &Field{ + parent: m, + Name: head, + } + // Don't add references for fake common KeyPath from trimCommon in CreateEdge. + if refctx != nil { + f.References = append(f.References, &FieldReference{ + String: kp.Path[i].Unbox(), + KeyPath: kp, + Context: refctx, + }) + } + m.Fields = append(m.Fields, f) + if i+1 == len(kp.Path) { + return f, nil + } + f.Composite = &Map{ + parent: f, + } + return f.Map().ensureField(i+1, kp, refctx) +} + +func (m *Map) DeleteField(ida ...string) *Field { + if len(ida) == 0 { + return nil + } + + s := ida[0] + rest := ida[1:] + + for i, f := range m.Fields { + if !strings.EqualFold(f.Name, s) { + continue + } + if len(rest) == 0 { + m.Fields = append(m.Fields[:i], m.Fields[i+1:]...) + return f + } + if f.Map() != nil { + return f.Map().DeleteField(rest...) + } + } + return nil +} + +func (m *Map) GetEdges(eid *EdgeID) []*Edge { + eid, m, err := eid.resolveUnderscores(m) + if err != nil { + return nil + } + common, eid := eid.trimCommon() + if len(common) > 0 { + f := m.GetField(common...) + if f == nil { + return nil + } + if f.Map() != nil { + return f.Map().GetEdges(eid) + } + return nil + } + + var ea []*Edge + for _, e := range m.Edges { + if e.ID.Match(eid) { + ea = append(ea, e) + } + } + return ea +} + +func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext) (*Edge, error) { + if ParentEdge(m) != nil { + return nil, d2parser.Errorf(refctx.Edge, "cannot create edge inside edge") + } + + eid, m, err := eid.resolveUnderscores(m) + if err != nil { + return nil, d2parser.Errorf(refctx.Edge, err.Error()) + } + common, eid := eid.trimCommon() + if len(common) > 0 { + tmp := *refctx.Edge.Src + kp := &tmp + kp.Path = kp.Path[:len(common)] + f, err := m.EnsureField(kp, nil) + if err != nil { + return nil, err + } + if _, ok := f.Composite.(*Array); ok { + return nil, d2parser.Errorf(refctx.Edge.Src, "cannot index into array") + } + if f.Map() == nil { + f.Composite = &Map{ + parent: f, + } + } + return f.Map().CreateEdge(eid, refctx) + } + + ij := findProhibitedEdgeKeyword(eid.SrcPath...) + if ij != -1 { + return nil, d2parser.Errorf(refctx.Edge.Src.Path[ij].Unbox(), "reserved keywords are prohibited in edges") + } + ij = findBoardKeyword(eid.SrcPath...) + if ij == len(eid.SrcPath)-1 { + return nil, d2parser.Errorf(refctx.Edge.Src.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense") + } + src := m.GetField(eid.SrcPath...) + if NodeBoardKind(src) != "" { + return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards") + } + + ij = findProhibitedEdgeKeyword(eid.DstPath...) + if ij != -1 { + return nil, d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "reserved keywords are prohibited in edges") + } + ij = findBoardKeyword(eid.DstPath...) + if ij == len(eid.DstPath)-1 { + return nil, d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense") + } + dst := m.GetField(eid.DstPath...) + if NodeBoardKind(dst) != "" { + return nil, d2parser.Errorf(refctx.Edge.Dst, "cannot create edges between boards") + } + + if ParentBoard(src) != ParentBoard(dst) { + return nil, d2parser.Errorf(refctx.Edge, "cannot create edges between boards") + } + + eid.Index = nil + ea := m.GetEdges(eid) + index := len(ea) + eid.Index = &index + e := &Edge{ + parent: m, + ID: eid, + References: []*EdgeReference{{ + Context: refctx, + }}, + } + m.Edges = append(m.Edges, e) + + return e, nil +} + +func (s *Scalar) ast() d2ast.Node { + return s.Value +} + +func (f *Field) ast() d2ast.Node { + k := &d2ast.Key{ + Key: &d2ast.KeyPath{ + Path: []*d2ast.StringBox{ + d2ast.MakeValueBox(d2ast.RawString(f.Name, true)).StringBox(), + }, + }, + } + + if f.Primary_ != nil { + k.Primary = d2ast.MakeValueBox(f.Primary_.ast().(d2ast.Value)).ScalarBox() + } + if f.Composite != nil { + k.Value = d2ast.MakeValueBox(f.Composite.ast().(d2ast.Value)) + } + + return k +} + +func (e *Edge) ast() d2ast.Node { + astEdge := &d2ast.Edge{} + + astEdge.Src = d2ast.MakeKeyPath(e.ID.SrcPath) + if e.ID.SrcArrow { + astEdge.SrcArrow = "<" + } + astEdge.Dst = d2ast.MakeKeyPath(e.ID.DstPath) + if e.ID.DstArrow { + astEdge.DstArrow = ">" + } + + k := &d2ast.Key{ + Edges: []*d2ast.Edge{astEdge}, + } + + if e.Primary_ != nil { + k.Primary = d2ast.MakeValueBox(e.Primary_.ast().(d2ast.Value)).ScalarBox() + } + if e.Map_ != nil { + k.Value = d2ast.MakeValueBox(e.Map_.ast().(*d2ast.Map)) + } + + return k +} + +func (a *Array) ast() d2ast.Node { + if a == nil { + return nil + } + astArray := &d2ast.Array{} + for _, av := range a.Values { + astArray.Nodes = append(astArray.Nodes, d2ast.MakeArrayNodeBox(av.ast().(d2ast.ArrayNode))) + } + return astArray +} + +func (m *Map) ast() d2ast.Node { + if m == nil { + return nil + } + astMap := &d2ast.Map{} + if m.Root() { + astMap.Range = d2ast.MakeRange(",0:0:0-1:0:0") + } else { + astMap.Range = d2ast.MakeRange(",1:0:0-2:0:0") + } + for _, f := range m.Fields { + astMap.Nodes = append(astMap.Nodes, d2ast.MakeMapNodeBox(f.ast().(d2ast.MapNode))) + } + for _, e := range m.Edges { + astMap.Nodes = append(astMap.Nodes, d2ast.MakeMapNodeBox(e.ast().(d2ast.MapNode))) + } + return astMap +} + +func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext) { + sb := kp.Path[i] + f := m.GetField(sb.Unbox().ScalarString()) + if f == nil { + return + } + + f.References = append(f.References, &FieldReference{ + String: sb.Unbox(), + KeyPath: kp, + Context: refctx, + }) + if i+1 == len(kp.Path) { + return + } + if f.Map() != nil { + f.Map().appendFieldReferences(i+1, kp, refctx) + } +} + +func ParentMap(n Node) *Map { + for { + n = n.Parent() + if n == nil { + return nil + } + if m, ok := n.(*Map); ok { + return m + } + } +} + +func ParentField(n Node) *Field { + for { + n = n.Parent() + if n == nil { + return nil + } + if f, ok := n.(*Field); ok { + return f + } + } +} + +func ParentBoard(n Node) Node { + for { + n = n.Parent() + if n == nil { + return nil + } + if NodeBoardKind(n) != "" { + return n + } + } +} + +func ParentEdge(n Node) *Edge { + for { + n = n.Parent() + if n == nil { + return nil + } + if e, ok := n.(*Edge); ok { + return e + } + } +} + +func countUnderscores(p []string) int { + for i, el := range p { + if el != "_" { + return i + } + } + return 0 +} + +func findBoardKeyword(ida ...string) int { + for i := range ida { + if _, ok := d2graph.BoardKeywords[ida[i]]; ok { + return i + } + } + return -1 +} + +func findProhibitedEdgeKeyword(ida ...string) int { + for i := range ida { + if _, ok := d2graph.SimpleReservedKeywords[ida[i]]; ok { + return i + } + if _, ok := d2graph.ReservedKeywordHolders[ida[i]]; ok { + return i + } + } + return -1 +} + +func parentRef(n Node) Reference { + f := ParentField(n) + if f != nil { + return f.LastRef() + } + e := ParentEdge(n) + if e != nil { + return e.LastRef() + } + return nil +} + +func parentPrimaryKey(n Node) *d2ast.Key { + f := ParentField(n) + if f != nil { + return f.LastPrimaryKey() + } + e := ParentEdge(n) + if e != nil { + return e.LastPrimaryKey() + } + return nil +} + +func IDA(n Node) (ida []string) { + for { + f, ok := n.(*Field) + if ok { + if f.Root() { + reverseIDA(ida) + return ida + } + ida = append(ida, f.Name) + } + f = ParentField(n) + if f == nil { + reverseIDA(ida) + return ida + } + n = f + } +} + +func reverseIDA(ida []string) { + for i := 0; i < len(ida)/2; i++ { + tmp := ida[i] + ida[i] = ida[len(ida)-i-1] + ida[len(ida)-i-1] = tmp + } +} diff --git a/d2ir/d2ir_test.go b/d2ir/d2ir_test.go new file mode 100644 index 000000000..c8b34dbc0 --- /dev/null +++ b/d2ir/d2ir_test.go @@ -0,0 +1,69 @@ +package d2ir_test + +import ( + "testing" + + "oss.terrastruct.com/util-go/assert" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2ir" +) + +func TestCopy(t *testing.T) { + t.Parallel() + + const scalStr = `Those who claim the dead never return to life haven't ever been around.` + s := &d2ir.Scalar{ + Value: d2ast.FlatUnquotedString(scalStr), + } + a := &d2ir.Array{ + Values: []d2ir.Value{ + &d2ir.Scalar{ + Value: &d2ast.Boolean{ + Value: true, + }, + }, + }, + } + m2 := &d2ir.Map{ + Fields: []*d2ir.Field{ + {Primary_: s}, + }, + } + + const keyStr = `Absence makes the heart grow frantic.` + f := &d2ir.Field{ + Name: keyStr, + + Primary_: s, + Composite: a, + } + e := &d2ir.Edge{ + Primary_: s, + Map_: m2, + } + m := &d2ir.Map{ + Fields: []*d2ir.Field{f}, + Edges: []*d2ir.Edge{e}, + } + + m = m.Copy(nil).(*d2ir.Map) + f.Name = `Many a wife thinks her husband is the world's greatest lover.` + + assert.Equal(t, m, m.Fields[0].Parent()) + assert.Equal(t, keyStr, m.Fields[0].Name) + assert.Equal(t, m.Fields[0], m.Fields[0].Primary_.Parent()) + assert.Equal(t, m.Fields[0], m.Fields[0].Composite.(*d2ir.Array).Parent()) + + assert.Equal(t, + m.Fields[0].Composite, + m.Fields[0].Composite.(*d2ir.Array).Values[0].(*d2ir.Scalar).Parent(), + ) + + assert.Equal(t, m, m.Edges[0].Parent()) + assert.Equal(t, m.Edges[0], m.Edges[0].Primary_.Parent()) + assert.Equal(t, m.Edges[0], m.Edges[0].Map_.Parent()) + + assert.Equal(t, m.Edges[0].Map_, m.Edges[0].Map_.Fields[0].Parent()) + assert.Equal(t, m.Edges[0].Map_.Fields[0], m.Edges[0].Map_.Fields[0].Primary_.Parent()) +} diff --git a/d2ir/merge.go b/d2ir/merge.go new file mode 100644 index 000000000..15ba2a9ec --- /dev/null +++ b/d2ir/merge.go @@ -0,0 +1,52 @@ +package d2ir + +func OverlayMap(base, overlay *Map) { + for _, of := range overlay.Fields { + bf := base.GetField(of.Name) + if bf == nil { + base.Fields = append(base.Fields, of.Copy(base).(*Field)) + continue + } + OverlayField(bf, of) + } + + for _, oe := range overlay.Edges { + bea := base.GetEdges(oe.ID) + if len(bea) == 0 { + base.Edges = append(base.Edges, oe.Copy(base).(*Edge)) + continue + } + be := bea[0] + OverlayEdge(be, oe) + } +} + +func OverlayField(bf, of *Field) { + if of.Primary_ != nil { + bf.Primary_ = of.Primary_.Copy(bf).(*Scalar) + } + + if of.Composite != nil { + if bf.Map() != nil && of.Map() != nil { + OverlayMap(bf.Map(), of.Map()) + } else { + bf.Composite = of.Composite.Copy(bf).(*Map) + } + } + + bf.References = append(bf.References, of.References...) +} + +func OverlayEdge(be, oe *Edge) { + if oe.Primary_ != nil { + be.Primary_ = oe.Primary_.Copy(be).(*Scalar) + } + if oe.Map_ != nil { + if be.Map_ != nil { + OverlayMap(be.Map(), oe.Map_) + } else { + be.Map_ = oe.Map_.Copy(be).(*Map) + } + } + be.References = append(be.References, oe.References...) +} diff --git a/d2ir/query.go b/d2ir/query.go new file mode 100644 index 000000000..e70ad88df --- /dev/null +++ b/d2ir/query.go @@ -0,0 +1,62 @@ +package d2ir + +import ( + "fmt" + + "oss.terrastruct.com/d2/d2parser" +) + +// QueryAll is only for tests and debugging. +func (m *Map) QueryAll(idStr string) (na []Node, _ error) { + k, err := d2parser.ParseMapKey(idStr) + if err != nil { + return nil, err + } + + if k.Key != nil { + f := m.GetField(k.Key.IDA()...) + if f == nil { + return nil, nil + } + if len(k.Edges) == 0 { + na = append(na, f) + return na, nil + } + m = f.Map() + if m == nil { + return nil, nil + } + } + + eida := NewEdgeIDs(k) + for _, eid := range eida { + ea := m.GetEdges(eid) + for _, e := range ea { + if k.EdgeKey == nil { + na = append(na, e) + } else if e.Map_ != nil { + f := e.Map_.GetField(k.EdgeKey.IDA()...) + if f != nil { + na = append(na, f) + } + } + } + } + return na, nil +} + +// Query is only for tests and debugging. +func (m *Map) Query(idStr string) (Node, error) { + na, err := m.QueryAll(idStr) + if err != nil { + return nil, err + } + + if len(na) == 0 { + return nil, nil + } + if len(na) > 1 { + return nil, fmt.Errorf("expected only one query result but got: %#v", err) + } + return na[0], nil +} diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 14e4c6f80..8f2c6e9e3 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -5,11 +5,12 @@ import ( "sort" "strings" + "oss.terrastruct.com/util-go/go2" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" - "oss.terrastruct.com/util-go/go2" ) func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) { @@ -113,6 +114,7 @@ func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiag func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) { edgeOrder := make(map[string]int) layoutEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(toRemove)) + for i, edge := range g.Edges { edgeOrder[edge.AbsID()] = i if _, exists := toRemove[edge]; !exists { diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index 50c1e3ffe..20220ce03 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -377,7 +377,7 @@ container -> c: edge 1 } func TestSelfEdges(t *testing.T) { - g := d2graph.NewGraph(nil) + g := d2graph.NewGraph() g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} n1 := g.Root.EnsureChild([]string{"n1"}) n1.Box = geo.NewBox(nil, 100, 100) @@ -387,7 +387,7 @@ func TestSelfEdges(t *testing.T) { Src: n1, Dst: n1, Index: 0, - Attributes: d2graph.Attributes{ + Attributes: &d2graph.Attributes{ Label: d2graph.Scalar{Value: "left to right"}, }, }, @@ -413,11 +413,11 @@ func TestSelfEdges(t *testing.T) { } func TestSequenceToDescendant(t *testing.T) { - g := d2graph.NewGraph(nil) + g := d2graph.NewGraph() g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} a := g.Root.EnsureChild([]string{"a"}) a.Box = geo.NewBox(nil, 100, 100) - a.Attributes = d2graph.Attributes{ + a.Attributes = &d2graph.Attributes{ Shape: d2graph.Scalar{Value: shape.PERSON_TYPE}, } a_t1 := a.EnsureChild([]string{"t1"}) @@ -425,13 +425,15 @@ func TestSequenceToDescendant(t *testing.T) { g.Edges = []*d2graph.Edge{ { - Src: a, - Dst: a_t1, - Index: 0, + Src: a, + Dst: a_t1, + Index: 0, + Attributes: &d2graph.Attributes{}, }, { - Src: a_t1, - Dst: a, - Index: 0, + Src: a_t1, + Dst: a, + Index: 0, + Attributes: &d2graph.Attributes{}, }, } diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go index 22d94d17f..8ef3e91a2 100644 --- a/d2layouts/d2sequence/sequence_diagram.go +++ b/d2layouts/d2sequence/sequence_diagram.go @@ -225,7 +225,7 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) { for _, n := range sd.notes { inGroup := false for _, ref := range n.References { - curr := ref.UnresolvedScopeObj + curr := ref.ScopeObj for curr != nil { if curr == group { inGroup = true @@ -324,7 +324,7 @@ func (sd *sequenceDiagram) addLifelineEdges() { actorLifelineEnd := actor.Center() actorLifelineEnd.Y = endY sd.lifelines = append(sd.lifelines, &d2graph.Edge{ - Attributes: d2graph.Attributes{ + Attributes: &d2graph.Attributes{ Style: d2graph.Style{ StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)}, StrokeWidth: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_WIDTH)}, diff --git a/d2lib/d2.go b/d2lib/d2.go index bfe9713a9..fbd8222d5 100644 --- a/d2lib/d2.go +++ b/d2lib/d2.go @@ -44,32 +44,65 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target return nil, nil, err } + d, err := compile(ctx, g, opts) + if err != nil { + return nil, nil, err + } + return d, g, nil +} + +func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) { if len(g.Objects) > 0 { - err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily) + err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily) if err != nil { - return nil, nil, err + return nil, err } coreLayout, err := getLayout(opts) if err != nil { - return nil, nil, err + return nil, err } constantNears := d2near.WithoutConstantNears(ctx, g) err = d2sequence.Layout(ctx, g, coreLayout) if err != nil { - return nil, nil, err + return nil, err } err = d2near.Layout(ctx, g, constantNears) if err != nil { - return nil, nil, err + return nil, err } } - diagram, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily) - return diagram, g, err + d, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily) + if err != nil { + return nil, err + } + + for _, l := range g.Layers { + ld, err := compile(ctx, l, opts) + if err != nil { + return nil, err + } + d.Layers = append(d.Layers, ld) + } + for _, l := range g.Scenarios { + ld, err := compile(ctx, l, opts) + if err != nil { + return nil, err + } + d.Scenarios = append(d.Scenarios, ld) + } + for _, l := range g.Steps { + ld, err := compile(ctx, l, opts) + if err != nil { + return nil, err + } + d.Steps = append(d.Steps, ld) + } + return d, nil } func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) { diff --git a/d2oracle/edit.go b/d2oracle/edit.go index 48f7d0692..1d3103a28 100644 --- a/d2oracle/edit.go +++ b/d2oracle/edit.go @@ -396,7 +396,6 @@ func Delete(g *d2graph.Graph, key string) (_ *d2graph.Graph, err error) { if g != g2 { return g2, nil } - g = g2 if len(mk.Edges) == 1 { obj := g.Root @@ -1109,7 +1108,7 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) { Key: detachedMK.Key, MapKey: detachedMK, Scope: mostNestedRef.Scope, - }, mostNestedRef.UnresolvedScopeObj) + }, mostNestedRef.ScopeObj) } } @@ -1284,8 +1283,8 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) { // We don't want this to be underscore-resolved scope. We want to ignore underscores var scopeak []string - if ref.UnresolvedScopeObj != g.Root { - scopek, err := d2parser.ParseKey(ref.UnresolvedScopeObj.AbsID()) + if ref.ScopeObj != g.Root { + scopek, err := d2parser.ParseKey(ref.ScopeObj.AbsID()) if err != nil { return nil, err } diff --git a/d2oracle/edit_test.go b/d2oracle/edit_test.go index 7feba4572..dd3b430fc 100644 --- a/d2oracle/edit_test.go +++ b/d2oracle/edit_test.go @@ -1373,12 +1373,12 @@ more.(ok.q.z -> p.k): "furbling, v.:" { name: "complex_edge_1", - text: `a.b.(x -> y).q.z + text: `a.b.(x -> y).style.animated `, key: "a.b", newName: "ooo", - exp: `a.ooo.(x -> y).q.z + exp: `a.ooo.(x -> y).style.animated `, assertions: func(t *testing.T, g *d2graph.Graph) { if len(g.Objects) != 4 { @@ -1392,12 +1392,12 @@ more.(ok.q.z -> p.k): "furbling, v.:" { name: "complex_edge_2", - text: `a.b.(x -> y).q.z + text: `a.b.(x -> y).style.animated `, key: "a.b.x", newName: "papa", - exp: `a.b.(papa -> y).q.z + exp: `a.b.(papa -> y).style.animated `, assertions: func(t *testing.T, g *d2graph.Graph) { if len(g.Objects) != 4 { @@ -1454,12 +1454,12 @@ more.(ok.q.z -> p.k): "furbling, v.:" { name: "arrows_complex", - text: `a.b.(x -- y).q.z + text: `a.b.(x -- y).style.animated `, key: "a.b.(x -- y)[0]", newName: "(x <-> y)[0]", - exp: `a.b.(x <-> y).q.z + exp: `a.b.(x <-> y).style.animated `, assertions: func(t *testing.T, g *d2graph.Graph) { if len(g.Objects) != 4 { @@ -3025,7 +3025,7 @@ d if err == nil { objectsAfter := len(g.Objects) if objectsBefore != objectsAfter { - println(d2format.Format(g.AST)) + t.Log(d2format.Format(g.AST)) return nil, fmt.Errorf("move cannot destroy or create objects: found %d objects before and %d objects after", objectsBefore, objectsAfter) } } diff --git a/d2parser/parse.go b/d2parser/parse.go index d8351dcbc..b4c3bf9e8 100644 --- a/d2parser/parse.go +++ b/d2parser/parse.go @@ -45,7 +45,7 @@ func Parse(path string, r io.RuneReader, opts *ParseOptions) (*d2ast.Map, error) } m := p.parseMap(true) - if !p.err.empty() { + if !p.err.Empty() { return m, p.err } return m, nil @@ -57,7 +57,7 @@ func ParseKey(key string) (*d2ast.KeyPath, error) { } k := p.parseKey() - if !p.err.empty() { + if !p.err.Empty() { return nil, fmt.Errorf("failed to parse key %q: %w", key, p.err) } if k == nil { @@ -72,7 +72,7 @@ func ParseMapKey(mapKey string) (*d2ast.Key, error) { } mk := p.parseMapKey() - if !p.err.empty() { + if !p.err.Empty() { return nil, fmt.Errorf("failed to parse map key %q: %w", mapKey, p.err) } if mk == nil { @@ -87,7 +87,7 @@ func ParseValue(value string) (d2ast.Value, error) { } v := p.parseValue() - if !p.err.empty() { + if !p.err.Empty() { return nil, fmt.Errorf("failed to parse value %q: %w", value, p.err) } if v.Unbox() == nil { @@ -130,7 +130,16 @@ type ParseError struct { Errors []d2ast.Error `json:"errs"` } -func (pe ParseError) empty() bool { +func Errorf(n d2ast.Node, f string, v ...interface{}) error { + f = "%v: " + f + v = append([]interface{}{n.GetRange()}, v...) + return d2ast.Error{ + Range: n.GetRange(), + Message: fmt.Sprintf(f, v...), + } +} + +func (pe ParseError) Empty() bool { return pe.IOError == nil && len(pe.Errors) == 0 } @@ -138,11 +147,12 @@ func (pe ParseError) Error() string { var sb strings.Builder if pe.IOError != nil { sb.WriteString(pe.IOError.Error()) - sb.WriteByte('\n') } - for _, err := range pe.Errors { + for i, err := range pe.Errors { + if pe.IOError != nil || i > 0 { + sb.WriteByte('\n') + } sb.WriteString(err.Error()) - sb.WriteByte('\n') } return sb.String() } diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go index f431a62a2..ba733435a 100644 --- a/d2renderers/d2sketch/sketch_test.go +++ b/d2renderers/d2sketch/sketch_test.go @@ -63,7 +63,7 @@ func TestSketch(t *testing.T) { } People discovery: "People discovery \nservice" admixer: Ad mixer { - fill: "#c1a2f3" + style.fill: "#c1a2f3" } onboarding service: "Onboarding \nservice" @@ -107,7 +107,7 @@ Android: { web -> twitter fe timeline scorer: "Timeline\nScorer" { - fill: "#ffdef1" + style.fill "#ffdef1" } home ranker: Home Ranker @@ -119,7 +119,7 @@ timeline mixer -> home ranker: { } timeline mixer -> timeline service home mixer: Home mixer { - # fill: "#c1a2f3" + # style.fill "#c1a2f3" } container0.graphql -> home mixer: { style.stroke-dash: 4 @@ -146,7 +146,7 @@ prediction service2: Prediction Service { icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png } home scorer: Home Scorer { - fill: "#ffdef1" + style.fill "#ffdef1" } manhattan: Manhattan memcache: Memcache { @@ -154,15 +154,15 @@ memcache: Memcache { } fetch: Fetch { - multiple: true + style.multiple: true shape: step } feature: Feature { - multiple: true + style.multiple: true shape: step } scoring: Scoring { - multiple: true + style.multiple: true shape: step } fetch -> feature diff --git a/d2renderers/d2sketch/testdata/animated/sketch.exp.svg b/d2renderers/d2sketch/testdata/animated/sketch.exp.svg index 683a58ec4..e737ee989 100644 --- a/d2renderers/d2sketch/testdata/animated/sketch.exp.svg +++ b/d2renderers/d2sketch/testdata/animated/sketch.exp.svg @@ -51,7 +51,7 @@ width="561" height="982" viewBox="-102 -102 561 982"> diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index 0fa299c0d..78d639d2b 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -79,23 +79,23 @@ callout -> stored_data -> person diamond -> oval -> circle hexagon -> cloud -rectangle.multiple: true -square.multiple: true -page.multiple: true -parallelogram.multiple: true -document.multiple: true -cylinder.multiple: true -queue.multiple: true -package.multiple: true -step.multiple: true -callout.multiple: true -stored_data.multiple: true -person.multiple: true -diamond.multiple: true -oval.multiple: true -circle.multiple: true -hexagon.multiple: true -cloud.multiple: true +rectangle.style.multiple: true +square.style.multiple: true +page.style.multiple: true +parallelogram.style.multiple: true +document.style.multiple: true +cylinder.style.multiple: true +queue.style.multiple: true +package.style.multiple: true +step.style.multiple: true +callout.style.multiple: true +stored_data.style.multiple: true +person.style.multiple: true +diamond.style.multiple: true +oval.style.multiple: true +circle.style.multiple: true +hexagon.style.multiple: true +cloud.style.multiple: true `, }, { @@ -126,23 +126,23 @@ callout -> stored_data -> person diamond -> oval -> circle hexagon -> cloud -rectangle.shadow: true -square.shadow: true -page.shadow: true -parallelogram.shadow: true -document.shadow: true -cylinder.shadow: true -queue.shadow: true -package.shadow: true -step.shadow: true -callout.shadow: true -stored_data.shadow: true -person.shadow: true -diamond.shadow: true -oval.shadow: true -circle.shadow: true -hexagon.shadow: true -cloud.shadow: true +rectangle.style.shadow: true +square.style.shadow: true +page.style.shadow: true +parallelogram.style.shadow: true +document.style.shadow: true +cylinder.style.shadow: true +queue.style.shadow: true +package.style.shadow: true +step.style.shadow: true +callout.style.shadow: true +stored_data.style.shadow: true +person.style.shadow: true +diamond.style.shadow: true +oval.style.shadow: true +circle.style.shadow: true +hexagon.style.shadow: true +cloud.style.shadow: true `, }, { @@ -153,8 +153,8 @@ square: {shape: "square"} rectangle -> square -rectangle.3d: true -square.3d: true +rectangle.style.3d: true +square.style.3d: true `, }, { @@ -1109,17 +1109,17 @@ scorer.t -> itemOutcome.t3: setFeedback(missingConcepts)`, script: `shape: sequence_diagram scorer: { - stroke: red - stroke-width: 5 + style.stroke: red + style.stroke-width: 5 } scorer.abc: { - fill: yellow - stroke-width: 7 + style.fill: yellow + style.stroke-width: 7 } scorer -> itemResponse.a: { - stroke-width: 10 + style.stroke-width: 10 } itemResponse.a -> item.a.b item.a.b -> essayRubric.a.b.c @@ -1877,6 +1877,62 @@ a.sp1 -> a.sp2: redirect a.sp2 -> b: bar `, }, + { + name: "complex-layers", + script: ` +desc: Multi-layer diagram of a home. + +window: { + style.double-border: true +} +roof +garage + +layers: { + window: { + blinds + glass + } + roof: { + shingles + starlink + utility hookup + } + garage: { + tools + vehicles + } + repair: { + desc: How to repair a home. + + steps: { + 1: { + find contractors: { + craigslist + facebook + } + } + 2: { + find contractors -> solicit quotes + } + 3: { + obtain quotes -> negotiate + } + 4: { + negotiate -> book the best bid + } + } + } +} + +scenarios: { + storm: { + water + rain + thunder + } +}`, + }, } runa(t, tcs) diff --git a/e2etests/testdata/measured/empty-shape/dagre/board.exp.json b/e2etests/testdata/measured/empty-shape/dagre/board.exp.json index 0fef54c60..4b263d6c7 100644 --- a/e2etests/testdata/measured/empty-shape/dagre/board.exp.json +++ b/e2etests/testdata/measured/empty-shape/dagre/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "a", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 0 diff --git a/e2etests/testdata/measured/empty-shape/dagre/sketch.exp.svg b/e2etests/testdata/measured/empty-shape/dagre/sketch.exp.svg index 814a9061e..914a423a0 100644 --- a/e2etests/testdata/measured/empty-shape/dagre/sketch.exp.svg +++ b/e2etests/testdata/measured/empty-shape/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="304" height="304" viewBox="-102 -102 304 304"> \ No newline at end of file diff --git a/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json index c0da2a729..f51cf2770 100644 --- a/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json +++ b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "a", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 0 @@ -43,91 +43,9 @@ "zIndex": 0, "level": 1 }, - { - "id": "a.c", - "type": "", - "pos": { - "x": 40, - "y": 359 - }, - "width": 478, - "height": 235, - "opacity": 1, - "strokeDash": 0, - "strokeWidth": 2, - "borderRadius": 0, - "fill": "#EDF0FD", - "stroke": "white", - "shadow": false, - "3d": false, - "multiple": false, - "double-border": false, - "tooltip": "", - "link": "", - "icon": null, - "iconPosition": "", - "blend": false, - "fields": null, - "methods": null, - "columns": null, - "label": "c", - "fontSize": 24, - "fontFamily": "DEFAULT", - "language": "", - "color": "#0A0F25", - "italic": false, - "bold": false, - "underline": false, - "labelWidth": 15, - "labelHeight": 36, - "labelPosition": "INSIDE_TOP_CENTER", - "zIndex": 0, - "level": 2 - }, - { - "id": "a.c.d", - "type": "", - "pos": { - "x": 236, - "y": 413 - }, - "width": 114, - "height": 126, - "opacity": 1, - "strokeDash": 0, - "strokeWidth": 2, - "borderRadius": 0, - "fill": "#F7F8FE", - "stroke": "#0D32B2", - "shadow": false, - "3d": false, - "multiple": false, - "double-border": false, - "tooltip": "", - "link": "", - "icon": null, - "iconPosition": "", - "blend": false, - "fields": null, - "methods": null, - "columns": null, - "label": "d", - "fontSize": 16, - "fontFamily": "DEFAULT", - "language": "", - "color": "#0A0F25", - "italic": false, - "bold": true, - "underline": false, - "labelWidth": 14, - "labelHeight": 26, - "labelPosition": "INSIDE_MIDDLE_CENTER", - "zIndex": 0, - "level": 3 - }, { "id": "a.b", - "type": "", + "type": "rectangle", "pos": { "x": 64, "y": 55 @@ -166,9 +84,50 @@ "zIndex": 0, "level": 2 }, + { + "id": "a.c", + "type": "rectangle", + "pos": { + "x": 40, + "y": 359 + }, + "width": 478, + "height": 235, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "white", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 24, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 15, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 2 + }, { "id": "a.1", - "type": "", + "type": "rectangle", "pos": { "x": 237, "y": 55 @@ -209,7 +168,7 @@ }, { "id": "a.2", - "type": "", + "type": "rectangle", "pos": { "x": 409, "y": 55 @@ -247,6 +206,47 @@ "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 2 + }, + { + "id": "a.c.d", + "type": "rectangle", + "pos": { + "x": 236, + "y": 413 + }, + "width": 114, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 14, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 3 } ], "connections": [ diff --git a/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg index fc9fe180d..8771a2eb3 100644 --- a/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="775" height="852" viewBox="-102 -102 775 852"> \ No newline at end of file diff --git a/e2etests/testdata/regression/elk_img_empty_label_panic/elk/board.exp.json b/e2etests/testdata/regression/elk_img_empty_label_panic/elk/board.exp.json index bdd6a327a..bc5fc3378 100644 --- a/e2etests/testdata/regression/elk_img_empty_label_panic/elk/board.exp.json +++ b/e2etests/testdata/regression/elk_img_empty_label_panic/elk/board.exp.json @@ -55,7 +55,7 @@ }, { "id": "ico", - "type": "", + "type": "rectangle", "pos": { "x": 160, "y": 26 diff --git a/e2etests/testdata/regression/elk_img_empty_label_panic/elk/sketch.exp.svg b/e2etests/testdata/regression/elk_img_empty_label_panic/elk/sketch.exp.svg index b7128f39d..18f97f6a3 100644 --- a/e2etests/testdata/regression/elk_img_empty_label_panic/elk/sketch.exp.svg +++ b/e2etests/testdata/regression/elk_img_empty_label_panic/elk/sketch.exp.svg @@ -39,7 +39,7 @@ width="452" height="332" viewBox="-90 -90 452 332"> \ No newline at end of file diff --git a/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json b/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json index 33dc2e5da..84173aed6 100644 --- a/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json +++ b/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "x", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 0 @@ -45,7 +45,7 @@ }, { "id": "x.a", - "type": "", + "type": "rectangle", "pos": { "x": 50, "y": 50 @@ -86,7 +86,7 @@ }, { "id": "x.b", - "type": "", + "type": "rectangle", "pos": { "x": 263, "y": 50 diff --git a/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg b/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg index c66e878a7..1c982726d 100644 --- a/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="630" height="430" viewBox="-102 -102 630 430">x

linux: because a PC is a terrible thing to waste

-
a You don't have to know how the computer works,just how to work the computer. +a You don't have to know how the computer works,just how to work the computer. x

linux: because a PC is a terrible thing to waste

-
a You don't have to know how the computer works,just how to work the computer. +a You don't have to know how the computer works,just how to work the computer. Office chatterAliceBobbyawkward small talk uhm, hioh, hellowhat did you have for lunch?that's personalok + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/regression/sequence_diagram_ambiguous_edge_group/elk/board.exp.json b/e2etests/testdata/regression/sequence_diagram_ambiguous_edge_group/elk/board.exp.json new file mode 100644 index 000000000..42dba2171 --- /dev/null +++ b/e2etests/testdata/regression/sequence_diagram_ambiguous_edge_group/elk/board.exp.json @@ -0,0 +1,606 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "Office chatter", + "type": "sequence_diagram", + "pos": { + "x": 12, + "y": 12 + }, + "width": 741, + "height": 1166, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Office chatter", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 164, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "Office chatter.alice", + "type": "rectangle", + "pos": { + "x": 36, + "y": 122 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Alice", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 38, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.bob", + "type": "rectangle", + "pos": { + "x": 286, + "y": 122 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Bobby", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.awkward small talk", + "type": "rectangle", + "pos": { + "x": 494, + "y": 122 + }, + "width": 235, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "awkward small talk", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 135, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.awkward small talk.awkward small talk", + "type": "rectangle", + "pos": { + "x": 605, + "y": 362 + }, + "width": 12, + "height": 158, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 135, + "labelHeight": 26, + "zIndex": 2, + "level": 3 + }, + { + "id": "Office chatter.awkward small talk.awkward small talk.ok", + "type": "page", + "pos": { + "x": 550, + "y": 378 + }, + "width": 122, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "ok", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 22, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 5, + "level": 4 + }, + { + "id": "Office chatter.awkward small talk.icebreaker attempt", + "type": "rectangle", + "pos": { + "x": 605, + "y": -9223372036854775808 + }, + "width": 12, + "height": 80, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#DEE1EB", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": true, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 134, + "labelHeight": 26, + "zIndex": 2, + "level": 3 + }, + { + "id": "Office chatter.awkward small talk.unfortunate outcome", + "type": "rectangle", + "pos": { + "x": 605, + "y": -9223372036854775808 + }, + "width": 12, + "height": 80, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#DEE1EB", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": true, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 148, + "labelHeight": 26, + "zIndex": 2, + "level": 3 + } + ], + "connections": [ + { + "id": "Office chatter.(alice -> bob)[1]", + "src": "Office chatter.alice", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.bob", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "uhm, hi", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 50, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 634 + }, + { + "x": 361, + "y": 634 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "Office chatter.(bob -> alice)[1]", + "src": "Office chatter.bob", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.alice", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "oh, hello", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 56, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 764 + }, + { + "x": 111, + "y": 764 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "Office chatter.(alice -> bob)[0]", + "src": "Office chatter.alice", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.bob", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "what did you have for lunch?", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 187, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 894 + }, + { + "x": 361, + "y": 894 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "Office chatter.(bob -> alice)[0]", + "src": "Office chatter.bob", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.alice", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "that's personal", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 99, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 1024 + }, + { + "x": 111, + "y": 1024 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(Office chatter.alice -- )[0]", + "src": "Office chatter.alice", + "srcArrow": "none", + "srcLabel": "", + "dst": "alice-lifeline-end-3851299086", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 248 + }, + { + "x": 111, + "y": 1154 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(Office chatter.bob -- )[0]", + "src": "Office chatter.bob", + "srcArrow": "none", + "srcLabel": "", + "dst": "bob-lifeline-end-3036726343", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 248 + }, + { + "x": 361, + "y": 1154 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(Office chatter.awkward small talk -- )[0]", + "src": "Office chatter.awkward small talk", + "srcArrow": "none", + "srcLabel": "", + "dst": "awkward small talk-lifeline-end-861194358", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 611.5, + "y": 248 + }, + { + "x": 611.5, + "y": 1154 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + } + ] +} diff --git a/e2etests/testdata/regression/sequence_diagram_ambiguous_edge_group/elk/sketch.exp.svg b/e2etests/testdata/regression/sequence_diagram_ambiguous_edge_group/elk/sketch.exp.svg new file mode 100644 index 000000000..4f4934866 --- /dev/null +++ b/e2etests/testdata/regression/sequence_diagram_ambiguous_edge_group/elk/sketch.exp.svg @@ -0,0 +1,62 @@ + +Office chatterAliceBobbyawkward small talk uhm, hioh, hellowhat did you have for lunch?that's personalok + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json index fc36b5f75..29cf0cdbf 100644 --- a/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json +++ b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json @@ -43,6 +43,88 @@ "zIndex": 0, "level": 1 }, + { + "id": "foo.a", + "type": "rectangle", + "pos": { + "x": 24, + "y": 110 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foo.b", + "type": "rectangle", + "pos": { + "x": 274, + "y": 110 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, { "id": "foobar", "type": "sequence_diagram", @@ -84,91 +166,9 @@ "zIndex": 0, "level": 1 }, - { - "id": "foo.a", - "type": "", - "pos": { - "x": 24, - "y": 110 - }, - "width": 150, - "height": 126, - "opacity": 1, - "strokeDash": 0, - "strokeWidth": 2, - "borderRadius": 0, - "fill": "#EDF0FD", - "stroke": "#0D32B2", - "shadow": false, - "3d": false, - "multiple": false, - "double-border": false, - "tooltip": "", - "link": "", - "icon": null, - "iconPosition": "", - "blend": false, - "fields": null, - "methods": null, - "columns": null, - "label": "a", - "fontSize": 16, - "fontFamily": "DEFAULT", - "language": "", - "color": "#0A0F25", - "italic": false, - "bold": false, - "underline": false, - "labelWidth": 12, - "labelHeight": 26, - "labelPosition": "INSIDE_MIDDLE_CENTER", - "zIndex": 0, - "level": 2 - }, - { - "id": "foo.b", - "type": "", - "pos": { - "x": 274, - "y": 110 - }, - "width": 150, - "height": 126, - "opacity": 1, - "strokeDash": 0, - "strokeWidth": 2, - "borderRadius": 0, - "fill": "#EDF0FD", - "stroke": "#0D32B2", - "shadow": false, - "3d": false, - "multiple": false, - "double-border": false, - "tooltip": "", - "link": "", - "icon": null, - "iconPosition": "", - "blend": false, - "fields": null, - "methods": null, - "columns": null, - "label": "b", - "fontSize": 16, - "fontFamily": "DEFAULT", - "language": "", - "color": "#0A0F25", - "italic": false, - "bold": false, - "underline": false, - "labelWidth": 13, - "labelHeight": 26, - "labelPosition": "INSIDE_MIDDLE_CENTER", - "zIndex": 0, - "level": 2 - }, { "id": "foobar.c", - "type": "", + "type": "rectangle", "pos": { "x": 24, "y": 730 @@ -209,7 +209,7 @@ }, { "id": "foobar.d", - "type": "", + "type": "rectangle", "pos": { "x": 274, "y": 730 diff --git a/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg index 418af865c..6fe13d70d 100644 --- a/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="648" height="1340" viewBox="-100 -100 648 1340">aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 +aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 diff --git a/e2etests/testdata/stable/chaos2/elk/board.exp.json b/e2etests/testdata/stable/chaos2/elk/board.exp.json index 2ea0a10ba..1f8a425f2 100644 --- a/e2etests/testdata/stable/chaos2/elk/board.exp.json +++ b/e2etests/testdata/stable/chaos2/elk/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "aa", - "type": "", + "type": "rectangle", "pos": { "x": 12, "y": 12 @@ -45,7 +45,7 @@ }, { "id": "aa.bb", - "type": "", + "type": "rectangle", "pos": { "x": 87, "y": 815 @@ -86,7 +86,7 @@ }, { "id": "aa.bb.cc", - "type": "", + "type": "rectangle", "pos": { "x": 423, "y": 1271 @@ -208,7 +208,7 @@ }, { "id": "aa.bb.cc.dd.ff", - "type": "", + "type": "rectangle", "pos": { "x": 609, "y": 1421 @@ -289,7 +289,7 @@ }, { "id": "aa.bb.cc.hh", - "type": "", + "type": "rectangle", "pos": { "x": 522, "y": 2090 @@ -453,7 +453,7 @@ }, { "id": "aa.ll", - "type": "", + "type": "rectangle", "pos": { "x": 612, "y": 363 @@ -575,7 +575,7 @@ }, { "id": "aa.oo", - "type": "", + "type": "rectangle", "pos": { "x": 689, "y": 87 diff --git a/e2etests/testdata/stable/chaos2/elk/sketch.exp.svg b/e2etests/testdata/stable/chaos2/elk/sketch.exp.svg index d38284143..0c674f303 100644 --- a/e2etests/testdata/stable/chaos2/elk/sketch.exp.svg +++ b/e2etests/testdata/stable/chaos2/elk/sketch.exp.svg @@ -796,7 +796,7 @@ width="1275" height="2738" viewBox="-90 -90 1275 2738">aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 +aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 diff --git a/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json b/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json index 297f00d56..c70ecaf0d 100644 --- a/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json +++ b/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "a", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 0 @@ -45,7 +45,7 @@ }, { "id": "a.b", - "type": "", + "type": "rectangle", "pos": { "x": 40, "y": 50 @@ -86,7 +86,7 @@ }, { "id": "a.b.c", - "type": "", + "type": "rectangle", "pos": { "x": 80, "y": 100 @@ -127,7 +127,7 @@ }, { "id": "a.b.c.d", - "type": "", + "type": "rectangle", "pos": { "x": 130, "y": 150 diff --git a/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg b/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg index f23abb2ed..b61acfe6e 100644 --- a/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="698" height="630" viewBox="-102 -102 698 630">windowroofgarage + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/complex-layers/elk/board.exp.json b/e2etests/testdata/stable/complex-layers/elk/board.exp.json new file mode 100644 index 000000000..3f0ff54e5 --- /dev/null +++ b/e2etests/testdata/stable/complex-layers/elk/board.exp.json @@ -0,0 +1,1220 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "window", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 163, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": true, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "window", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 63, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "roof", + "type": "rectangle", + "pos": { + "x": 195, + "y": 12 + }, + "width": 135, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "roof", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 35, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "garage", + "type": "rectangle", + "pos": { + "x": 350, + "y": 12 + }, + "width": 154, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "garage", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 54, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [], + "layers": [ + { + "name": "window", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "blinds", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 148, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "blinds", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 48, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "glass", + "type": "rectangle", + "pos": { + "x": 180, + "y": 12 + }, + "width": 141, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "glass", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 41, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] + }, + { + "name": "roof", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "shingles", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 164, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "shingles", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 64, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "starlink", + "type": "rectangle", + "pos": { + "x": 196, + "y": 12 + }, + "width": 161, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "starlink", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 61, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "utility hookup", + "type": "rectangle", + "pos": { + "x": 377, + "y": 12 + }, + "width": 206, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "utility hookup", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 106, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] + }, + { + "name": "garage", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "tools", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 141, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "tools", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 41, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "vehicles", + "type": "rectangle", + "pos": { + "x": 173, + "y": 12 + }, + "width": 163, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "vehicles", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 63, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] + }, + { + "name": "repair", + "fontFamily": "SourceSansPro", + "shapes": [], + "connections": [], + "steps": [ + { + "name": "1", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "find contractors", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 511, + "height": 276, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "find contractors", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 192, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "find contractors.craigslist", + "type": "rectangle", + "pos": { + "x": 87, + "y": 87 + }, + "width": 170, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "craigslist", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 70, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "find contractors.facebook", + "type": "rectangle", + "pos": { + "x": 277, + "y": 87 + }, + "width": 171, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "facebook", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 71, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [] + }, + { + "name": "2", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "find contractors", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 219, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "find contractors", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 119, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "solicit quotes", + "type": "rectangle", + "pos": { + "x": 21, + "y": 238 + }, + "width": 200, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "solicit quotes", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 100, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(find contractors -> solicit quotes)[0]", + "src": "find contractors", + "srcArrow": "none", + "srcLabel": "", + "dst": "solicit quotes", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 121.5, + "y": 138 + }, + { + "x": 121.5, + "y": 238 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] + }, + { + "name": "3", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "obtain quotes", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 203, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "obtain quotes", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 103, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "negotiate", + "type": "rectangle", + "pos": { + "x": 27, + "y": 238 + }, + "width": 172, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "negotiate", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 72, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(obtain quotes -> negotiate)[0]", + "src": "obtain quotes", + "srcArrow": "none", + "srcLabel": "", + "dst": "negotiate", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 113.5, + "y": 138 + }, + { + "x": 113.5, + "y": 238 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] + }, + { + "name": "4", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "negotiate", + "type": "rectangle", + "pos": { + "x": 39, + "y": 12 + }, + "width": 172, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "negotiate", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 72, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "book the best bid", + "type": "rectangle", + "pos": { + "x": 12, + "y": 238 + }, + "width": 227, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "book the best bid", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 127, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(negotiate -> book the best bid)[0]", + "src": "negotiate", + "srcArrow": "none", + "srcLabel": "", + "dst": "book the best bid", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 125.5, + "y": 138 + }, + { + "x": 125.5, + "y": 238 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] + } + ] + } + ], + "scenarios": [ + { + "name": "storm", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "window", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 163, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": true, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "window", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 63, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "roof", + "type": "rectangle", + "pos": { + "x": 195, + "y": 12 + }, + "width": 135, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "roof", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 35, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "garage", + "type": "rectangle", + "pos": { + "x": 350, + "y": 12 + }, + "width": 154, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "garage", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 54, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "water", + "type": "rectangle", + "pos": { + "x": 524, + "y": 12 + }, + "width": 148, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "water", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 48, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "rain", + "type": "rectangle", + "pos": { + "x": 692, + "y": 12 + }, + "width": 133, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "rain", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 33, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "thunder", + "type": "rectangle", + "pos": { + "x": 845, + "y": 12 + }, + "width": 163, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "thunder", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 63, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] + } + ] +} diff --git a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg new file mode 100644 index 000000000..bfaa24c21 --- /dev/null +++ b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg @@ -0,0 +1,52 @@ + +windowroofgarage + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/connected_container/dagre/board.exp.json b/e2etests/testdata/stable/connected_container/dagre/board.exp.json index 89dbc5cc6..67c7f6832 100644 --- a/e2etests/testdata/stable/connected_container/dagre/board.exp.json +++ b/e2etests/testdata/stable/connected_container/dagre/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "a", - "type": "", + "type": "rectangle", "pos": { "x": 41, "y": 0 @@ -45,7 +45,7 @@ }, { "id": "a.b", - "type": "", + "type": "rectangle", "pos": { "x": 91, "y": 50 @@ -86,7 +86,7 @@ }, { "id": "c", - "type": "", + "type": "rectangle", "pos": { "x": 40, "y": 326 @@ -127,7 +127,7 @@ }, { "id": "c.d", - "type": "", + "type": "rectangle", "pos": { "x": 90, "y": 376 @@ -168,7 +168,7 @@ }, { "id": "f", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 652 @@ -209,7 +209,7 @@ }, { "id": "f.h", - "type": "", + "type": "rectangle", "pos": { "x": 40, "y": 702 @@ -250,7 +250,7 @@ }, { "id": "f.h.g", - "type": "", + "type": "rectangle", "pos": { "x": 90, "y": 752 diff --git a/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg b/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg index 5592ff4af..1cfadb515 100644 --- a/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="498" height="1182" viewBox="-102 -102 498 1182">xyThe top of the mountain

Cats, no less liquid than their shadows, offer no angles to the wind.

If we can't fix it, it ain't broke.

Dieters live life in the fasting lane.

-
JoeDonaldi am top lefti am top righti am bottom lefti am bottom right +JoeDonaldi am top lefti am top righti am bottom lefti am bottom right xyThe top of the mountain

Cats, no less liquid than their shadows, offer no angles to the wind.

If we can't fix it, it ain't broke.

Dieters live life in the fasting lane.

-
JoeDonaldi am top lefti am top righti am bottom lefti am bottom right +JoeDonaldi am top lefti am top righti am bottom lefti am bottom right poll the peopleresultsunfavorablefavorablewill of the people

A winning strategy

-
+ poll the peopleresultsunfavorablefavorablewill of the people

A winning strategy

-
+ mixed togethersugarsolution we get +mixed togethersugarsolution we get mixed togethersugarsolution we get +mixed togethersugarsolution we get

Markdown: Syntax

-
ab +ab

Markdown: Syntax

-
ab +ab markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
+ markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
+ markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
+ markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
+

code

-
ab +ab

code

-
ab +ab Office chatterAliceBobbyawkward small talkicebreaker attemptunfortunate outcome uhm, hioh, hellowhat did you have for lunch?that's personal + + + + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/todo/sequence_diagram_edge_group_span_field/elk/board.exp.json b/e2etests/testdata/todo/sequence_diagram_edge_group_span_field/elk/board.exp.json new file mode 100644 index 000000000..7aa6f8bc9 --- /dev/null +++ b/e2etests/testdata/todo/sequence_diagram_edge_group_span_field/elk/board.exp.json @@ -0,0 +1,568 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "Office chatter", + "type": "sequence_diagram", + "pos": { + "x": 12, + "y": 12 + }, + "width": 448, + "height": 910, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Office chatter", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 164, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "Office chatter.alice", + "type": "rectangle", + "pos": { + "x": 36, + "y": 122 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Alice", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 38, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.bob", + "type": "rectangle", + "pos": { + "x": 286, + "y": 122 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Bobby", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.alice.a", + "type": "rectangle", + "pos": { + "x": 105, + "y": 752 + }, + "width": 12, + "height": 80, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "zIndex": 2, + "level": 3 + }, + { + "id": "Office chatter.awkward small talk", + "type": "rectangle", + "pos": { + "x": 37, + "y": 338 + }, + "width": 398, + "height": 494, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#DEE1EB", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": true, + "fields": null, + "methods": null, + "columns": null, + "label": "awkward small talk", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 135, + "labelHeight": 26, + "labelPosition": "INSIDE_TOP_LEFT", + "zIndex": 3, + "level": 2 + }, + { + "id": "Office chatter.awkward small talk.icebreaker attempt", + "type": "rectangle", + "pos": { + "x": 61, + "y": 598 + }, + "width": 350, + "height": 80, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#DEE1EB", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": true, + "fields": null, + "methods": null, + "columns": null, + "label": "icebreaker attempt", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 134, + "labelHeight": 26, + "labelPosition": "INSIDE_TOP_LEFT", + "zIndex": 3, + "level": 3 + }, + { + "id": "Office chatter.awkward small talk.unfortunate outcome", + "type": "rectangle", + "pos": { + "x": 67, + "y": 728 + }, + "width": 338, + "height": 80, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#DEE1EB", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": true, + "fields": null, + "methods": null, + "columns": null, + "label": "unfortunate outcome", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 148, + "labelHeight": 26, + "labelPosition": "INSIDE_TOP_LEFT", + "zIndex": 3, + "level": 3 + }, + { + "id": "Office chatter.bob.a", + "type": "rectangle", + "pos": { + "x": 355, + "y": 752 + }, + "width": 12, + "height": 80, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "zIndex": 2, + "level": 3 + } + ], + "connections": [ + { + "id": "Office chatter.(alice -> bob)[1]", + "src": "Office chatter.alice", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.bob", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "uhm, hi", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 50, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 378 + }, + { + "x": 361, + "y": 378 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "Office chatter.(bob -> alice)[0]", + "src": "Office chatter.bob", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.alice", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "oh, hello", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 56, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 508 + }, + { + "x": 111, + "y": 508 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "Office chatter.(alice -> bob)[0]", + "src": "Office chatter.alice", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.bob", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "what did you have for lunch?", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 187, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 638 + }, + { + "x": 361, + "y": 638 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "Office chatter.(bob.a -> alice.a)[0]", + "src": "Office chatter.bob.a", + "srcArrow": "none", + "srcLabel": "", + "dst": "Office chatter.alice.a", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "that's personal", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 99, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 355, + "y": 768 + }, + { + "x": 117, + "y": 768 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(Office chatter.alice -- )[0]", + "src": "Office chatter.alice", + "srcArrow": "none", + "srcLabel": "", + "dst": "alice-lifeline-end-3851299086", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 248 + }, + { + "x": 111, + "y": 898 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(Office chatter.bob -- )[0]", + "src": "Office chatter.bob", + "srcArrow": "none", + "srcLabel": "", + "dst": "bob-lifeline-end-3036726343", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 248 + }, + { + "x": 361, + "y": 898 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + } + ] +} diff --git a/e2etests/testdata/todo/sequence_diagram_edge_group_span_field/elk/sketch.exp.svg b/e2etests/testdata/todo/sequence_diagram_edge_group_span_field/elk/sketch.exp.svg new file mode 100644 index 000000000..9118af6fa --- /dev/null +++ b/e2etests/testdata/todo/sequence_diagram_edge_group_span_field/elk/sketch.exp.svg @@ -0,0 +1,65 @@ + +Office chatterAliceBobbyawkward small talkicebreaker attemptunfortunate outcome uhm, hioh, hellowhat did you have for lunch?that's personal + + + + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/todo/shape_set_width_height/dagre/board.exp.json b/e2etests/testdata/todo/shape_set_width_height/dagre/board.exp.json index 5f57bbfdc..3a6ae4b9a 100644 --- a/e2etests/testdata/todo/shape_set_width_height/dagre/board.exp.json +++ b/e2etests/testdata/todo/shape_set_width_height/dagre/board.exp.json @@ -4,7 +4,7 @@ "shapes": [ { "id": "containers", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 0 @@ -453,6 +453,81 @@ "zIndex": 0, "level": 1 }, + { + "id": "class", + "type": "class", + "pos": { + "x": 1018, + "y": 756 + }, + "width": 800, + "height": 400, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#0A0F25", + "stroke": "#FFFFFF", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": [ + { + "name": "num", + "type": "int", + "visibility": "private" + }, + { + "name": "timeout", + "type": "int", + "visibility": "private" + }, + { + "name": "pid", + "type": "", + "visibility": "private" + } + ], + "methods": [ + { + "name": "getStatus()", + "return": "Enum", + "visibility": "public" + }, + { + "name": "getJobs()", + "return": "Job[]", + "visibility": "public" + }, + { + "name": "setTimeout(seconds int)", + "return": "void", + "visibility": "public" + } + ], + "columns": null, + "label": "class", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 75, + "labelHeight": 36, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + }, { "id": "users", "type": "sql_table", @@ -638,89 +713,14 @@ "neutralAccentColor": "#676C7E" }, { - "id": "class", - "type": "class", + "id": "container", + "type": "rectangle", "pos": { - "x": 1018, - "y": 756 + "x": 1991, + "y": 165 }, - "width": 800, - "height": 400, - "opacity": 1, - "strokeDash": 0, - "strokeWidth": 2, - "borderRadius": 0, - "fill": "#0A0F25", - "stroke": "#FFFFFF", - "shadow": false, - "3d": false, - "multiple": false, - "double-border": false, - "tooltip": "", - "link": "", - "icon": null, - "iconPosition": "", - "blend": false, - "fields": [ - { - "name": "num", - "type": "int", - "visibility": "private" - }, - { - "name": "timeout", - "type": "int", - "visibility": "private" - }, - { - "name": "pid", - "type": "", - "visibility": "private" - } - ], - "methods": [ - { - "name": "getStatus()", - "return": "Enum", - "visibility": "public" - }, - { - "name": "getJobs()", - "return": "Job[]", - "visibility": "public" - }, - { - "name": "setTimeout(seconds int)", - "return": "void", - "visibility": "public" - } - ], - "columns": null, - "label": "class", - "fontSize": 20, - "fontFamily": "DEFAULT", - "language": "", - "color": "#0A0F25", - "italic": false, - "bold": false, - "underline": false, - "labelWidth": 75, - "labelHeight": 36, - "zIndex": 0, - "level": 1, - "primaryAccentColor": "#0D32B2", - "secondaryAccentColor": "#4A6FF3", - "neutralAccentColor": "#676C7E" - }, - { - "id": "text", - "type": "", - "pos": { - "x": 1878, - "y": 556 - }, - "width": 400, - "height": 800, + "width": 174, + "height": 126, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -739,7 +739,7 @@ "fields": null, "methods": null, "columns": null, - "label": "markdown text expanded to 800x400", + "label": "container", "fontSize": 16, "fontFamily": "DEFAULT", "language": "", @@ -747,12 +747,52 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 266, + "labelWidth": 74, "labelHeight": 26, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 }, + { + "id": "text", + "type": "text", + "pos": { + "x": 1878, + "y": 556 + }, + "width": 400, + "height": 800, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "markdown text expanded to 800x400", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "markdown", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 247, + "labelHeight": 24, + "zIndex": 0, + "level": 1 + }, { "id": "code", "type": "code", @@ -832,47 +872,6 @@ "labelHeight": 70, "zIndex": 0, "level": 1 - }, - { - "id": "container", - "type": "", - "pos": { - "x": 1991, - "y": 165 - }, - "width": 174, - "height": 126, - "opacity": 1, - "strokeDash": 0, - "strokeWidth": 2, - "borderRadius": 0, - "fill": "#F7F8FE", - "stroke": "#0D32B2", - "shadow": false, - "3d": false, - "multiple": false, - "double-border": false, - "tooltip": "", - "link": "", - "icon": null, - "iconPosition": "", - "blend": false, - "fields": null, - "methods": null, - "columns": null, - "label": "container", - "fontSize": 16, - "fontFamily": "DEFAULT", - "language": "", - "color": "#0A0F25", - "italic": false, - "bold": true, - "underline": false, - "labelWidth": 74, - "labelHeight": 26, - "labelPosition": "INSIDE_MIDDLE_CENTER", - "zIndex": 0, - "level": 1 } ], "connections": [ diff --git a/e2etests/testdata/todo/shape_set_width_height/dagre/sketch.exp.svg b/e2etests/testdata/todo/shape_set_width_height/dagre/sketch.exp.svg index 4f33f6271..04448ac60 100644 --- a/e2etests/testdata/todo/shape_set_width_height/dagre/sketch.exp.svg +++ b/e2etests/testdata/todo/shape_set_width_height/dagre/sketch.exp.svg @@ -39,17 +39,764 @@ width="2482" height="2672" viewBox="-102 -102 2482 2672">containerscloudtall cylinderclass- num int- timeout @@ -61,11 +808,22 @@ width="2482" height="2672" viewBox="-102 -102 2482 2672">containerscloudtall cylinderclass- num int- timeout @@ -61,11 +808,22 @@ width="2622" height="2644" viewBox="-90 -90 2622 2644">