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/README.md b/README.md index 2818ec5ce..5541306b0 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ let us know and we'll be happy to include it here! - **Python D2 diagram builder**: [https://github.com/MrBlenny/py-d2](https://github.com/MrBlenny/py-d2) - **Clojure D2 transpiler**: [https://github.com/judepayne/dictim](https://github.com/judepayne/dictim) - **JavaScript D2 diagram builder**: [https://github.com/Kreshnik/d2lang-js](https://github.com/Kreshnik/d2lang-js) -- **Maven plugin**: [https://github.com/andrinmeier/unofficial-d2lang-confluence-plugin](https://github.com/andrinmeier/unofficial-d2lang-confluence-plugin) +- **Maven plugin**: [https://github.com/andrinmeier/unofficial-d2lang-maven-plugin](https://github.com/andrinmeier/unofficial-d2lang-maven-plugin) - **Confluence plugin**: [https://github.com/andrinmeier/unofficial-d2lang-confluence-plugin](https://github.com/andrinmeier/unofficial-d2lang-confluence-plugin) - **CIL (C#, Visual Basic, F#, C++ CLR) to D2**: [https://github.com/HugoVG/AppDiagram](https://github.com/HugoVG/AppDiagram) 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/Dockerfile b/ci/release/Dockerfile index 623a0f490..3d7f843bc 100644 --- a/ci/release/Dockerfile +++ b/ci/release/Dockerfile @@ -7,7 +7,9 @@ RUN apt-get update && apt-get install -y ca-certificates curl dumb-init sudo RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash -s - && \ apt-get install -y nodejs -RUN npx playwright install-deps +# https://github.com/microsoft/playwright/issues/18319 +# Hopefully soon. +RUN if [ "$TARGETARCH" = amd64 ]; then npx playwright install-deps; fi RUN adduser --gecos '' --disabled-password debian \ && echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd @@ -26,7 +28,7 @@ RUN mkdir -p /usr/local/lib/d2 \ && rm -Rf /tmp/d2-*-linux-"$TARGETARCH".tar.gz USER debian:debian -RUN d2 init-playwright +RUN if [ "$TARGETARCH" = amd64 ]; then d2 init-playwright; fi WORKDIR /home/debian/src EXPOSE 8080 diff --git a/ci/release/aws/ssh.sh b/ci/release/aws/ssh.sh index 387bdf38c..cab458d22 100755 --- a/ci/release/aws/ssh.sh +++ b/ci/release/aws/ssh.sh @@ -33,11 +33,11 @@ main() { done shift "$FLAGSHIFT" - REMOTE_HOST=$CI_HOST_D2_LINUX_AMD64 && runjob linux-amd64 ssh "$REMOTE_HOST" "$@" - REMOTE_HOST=$CI_HOST_D2_LINUX_ARM64 && runjob linux-arm64 ssh "$REMOTE_HOST" "$@" - REMOTE_HOST=$CI_HOST_D2_MACOS_AMD64 && runjob macos-amd64 ssh "$REMOTE_HOST" "$@" - REMOTE_HOST=$CI_HOST_D2_MACOS_ARM64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@" - REMOTE_HOST=$CI_HOST_D2_WINDOWS_AMD64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@" + REMOTE_HOST=$CI_D2_LINUX_AMD64 && runjob linux-amd64 ssh "$REMOTE_HOST" "$@" + REMOTE_HOST=$CI_D2_LINUX_ARM64 && runjob linux-arm64 ssh "$REMOTE_HOST" "$@" + REMOTE_HOST=$CI_D2_MACOS_AMD64 && runjob macos-amd64 ssh "$REMOTE_HOST" "$@" + REMOTE_HOST=$CI_D2_MACOS_ARM64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@" + REMOTE_HOST=$CI_D2_WINDOWS_AMD64 && runjob windows-amd64 ssh "$REMOTE_HOST" "$@" } main "$@" diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 72587383b..bfadf2bd1 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -2,14 +2,33 @@ - `double-border` keyword implemented. [#565](https://github.com/terrastruct/d2/pull/565) - The [Dockerfile](./docs/INSTALL.md#docker) now supports rendering PNGs [#594](https://github.com/terrastruct/d2/issues/594) + - There was a minor breaking change as part of this where the default working directory of the Dockerfile is now `/home/debian/src` instead of `/root/src` to allow UID remapping with [`fixuid`](https://github.com/boxboat/fixuid). - `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) +- Reduces default padding of shapes. [#702](https://github.com/terrastruct/d2/pull/702) +- Ensures labels fit inside shapes with shape-specific inner bounding boxes. [#702](https://github.com/terrastruct/d2/pull/702) +- Improves package shape dimensions with short height. [#702](https://github.com/terrastruct/d2/pull/702) +- Keeps person shape from becoming too distorted. [#702](https://github.com/terrastruct/d2/pull/702) +- Ensures shapes with icons have enough padding for their labels. [#702](https://github.com/terrastruct/d2/pull/702) #### Bugfixes ⛑️ - Fixes groups overlapping in sequence diagrams when they end in a self loop. [#728](https://github.com/terrastruct/d2/pull/728) +- Fixes dimensions of unlabeled squares or circles with only a set width or height. [#702](https://github.com/terrastruct/d2/pull/702) +- Fixes scaling of actor shapes in sequence diagrams. [#702](https://github.com/terrastruct/d2/pull/702) +- Images can now be set to sizes smaller than 128x128. [#702](https://github.com/terrastruct/d2/pull/702) +- Fixes class height when there are no rows. [#756](https://github.com/terrastruct/d2/pull/756) 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..5a57f0881 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) } @@ -141,6 +143,7 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { } shape.Label = text.Text shape.LabelWidth = text.Dimensions.Width + shape.LabelHeight = text.Dimensions.Height if obj.LabelPosition != nil { shape.LabelPosition = *obj.LabelPosition 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..0b95146e9 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "net/url" + "sort" "strconv" "strings" @@ -18,31 +19,34 @@ import ( "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/shape" "oss.terrastruct.com/d2/lib/textmeasure" ) const INNER_LABEL_PADDING int = 5 -const DEFAULT_SHAPE_PADDING = 100. +const DEFAULT_SHAPE_SIZE = 100. +const MIN_SHAPE_SIZE = 5 -// 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 +86,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 +109,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 +121,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 +503,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 +529,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 +566,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 +647,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] == "_" { @@ -730,9 +777,14 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul return dims, nil } -func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions) (*d2target.TextDimensions, error) { +func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions, withLabelPadding bool) (*d2target.TextDimensions, error) { dims := d2target.TextDimensions{} + if withLabelPadding { + labelDims.Width += INNER_LABEL_PADDING + labelDims.Height += INNER_LABEL_PADDING + } + switch strings.ToLower(obj.Attributes.Shape.Value) { default: return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil @@ -748,22 +800,22 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R if fdims == nil { return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text()) } - lineWidth := fdims.Width - if maxWidth < lineWidth { - maxWidth = lineWidth - } + maxWidth = go2.Max(maxWidth, fdims.Width) } for _, m := range obj.Class.Methods { mdims := GetTextDimensions(mtexts, ruler, m.Text(), go2.Pointer(d2fonts.SourceCodePro)) if mdims == nil { return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text()) } - lineWidth := mdims.Width - if maxWidth < lineWidth { - maxWidth = lineWidth - } + maxWidth = go2.Max(maxWidth, mdims.Width) } - dims.Width = maxWidth + // ┌─PrefixWidth ┌─CenterPadding + // ┌─┬─┬───────┬──────┬───┬──┐ + // │ + getJobs() Job[] │ + // └─┴─┴───────┴──────┴───┴──┘ + // └─PrefixPadding └──TypePadding + // ├───────┤ + ├───┤ = maxWidth + dims.Width = d2target.PrefixPadding + d2target.PrefixWidth + maxWidth + d2target.CenterPadding + d2target.TypePadding // All rows should be the same height var anyRowText *d2target.MText @@ -773,11 +825,10 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R anyRowText = obj.Class.Methods[0].Text() } if anyRowText != nil { - // 10px of padding top and bottom so text doesn't look squished - rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + 20 + rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding dims.Height = rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2) } else { - dims.Height = go2.Max(12, labelDims.Height) + dims.Height = 2*go2.Max(12, labelDims.Height) + d2target.VerticalPadding } case d2target.ShapeSQLTable: @@ -796,9 +847,7 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R } c.Name.LabelWidth = nameDims.Width c.Name.LabelHeight = nameDims.Height - if maxNameWidth < nameDims.Width { - maxNameWidth = nameDims.Width - } + maxNameWidth = go2.Max(maxNameWidth, nameDims.Width) typeDims := GetTextDimensions(mtexts, ruler, ctexts[1], fontFamily) if typeDims == nil { @@ -809,6 +858,7 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R if maxTypeWidth < typeDims.Width { maxTypeWidth = typeDims.Width } + maxTypeWidth = go2.Max(maxTypeWidth, typeDims.Width) if c.Constraint != "" { // covers UNQ constraint with padding @@ -826,21 +876,6 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R return &dims, nil } -func (obj *Object) GetPadding() (x, y float64) { - switch strings.ToLower(obj.Attributes.Shape.Value) { - case d2target.ShapeImage, - d2target.ShapeSQLTable, - d2target.ShapeText, - d2target.ShapeCode: - return 0., 0. - case d2target.ShapeClass: - // TODO fix class row width measurements (see SQL table) - return 100., 0. - default: - return DEFAULT_SHAPE_PADDING, DEFAULT_SHAPE_PADDING - } -} - type Edge struct { Index int `json:"index"` @@ -866,7 +901,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 +973,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 +981,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 +999,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 @@ -1108,29 +1171,41 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler desiredHeight, _ = strconv.Atoi(obj.Attributes.Height.Value) } + dslShape := strings.ToLower(obj.Attributes.Shape.Value) + if obj.Attributes.Label.Value == "" && - obj.Attributes.Shape.Value != d2target.ShapeImage && - obj.Attributes.Shape.Value != d2target.ShapeSQLTable && - obj.Attributes.Shape.Value != d2target.ShapeClass { - obj.Width = DEFAULT_SHAPE_PADDING - obj.Height = DEFAULT_SHAPE_PADDING - if desiredWidth != 0 { - obj.Width = float64(desiredWidth) - } - if desiredHeight != 0 { - obj.Height = float64(desiredHeight) + dslShape != d2target.ShapeImage && + dslShape != d2target.ShapeSQLTable && + dslShape != d2target.ShapeClass { + + if dslShape == d2target.ShapeCircle || dslShape == d2target.ShapeSquare { + sideLength := DEFAULT_SHAPE_SIZE + if desiredWidth != 0 || desiredHeight != 0 { + sideLength = float64(go2.Max(desiredWidth, desiredHeight)) + } + obj.Width = sideLength + obj.Height = sideLength + } else { + obj.Width = DEFAULT_SHAPE_SIZE + obj.Height = DEFAULT_SHAPE_SIZE + if desiredWidth != 0 { + obj.Width = float64(desiredWidth) + } + if desiredHeight != 0 { + obj.Height = float64(desiredHeight) + } } + continue } - shapeType := strings.ToLower(obj.Attributes.Shape.Value) - labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily) if err != nil { return err } + obj.LabelDimensions = *labelDims - switch shapeType { + switch dslShape { case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode: // no labels default: @@ -1140,39 +1215,65 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler } } - if shapeType != d2target.ShapeText && obj.Attributes.Label.Value != "" { - labelDims.Width += INNER_LABEL_PADDING - labelDims.Height += INNER_LABEL_PADDING - } - obj.LabelDimensions = *labelDims - - defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims) + // if there is a desired width or height, fit to content box without inner label padding for smallest minimum size + withInnerLabelPadding := desiredWidth == 0 && desiredHeight == 0 && + dslShape != d2target.ShapeText && obj.Attributes.Label.Value != "" + defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims, withInnerLabelPadding) if err != nil { return err } - obj.Width = float64(go2.Max(defaultDims.Width, desiredWidth)) - obj.Height = float64(go2.Max(defaultDims.Height, desiredHeight)) - - paddingX, paddingY := obj.GetPadding() - - switch shapeType { - case d2target.ShapeSquare, d2target.ShapeCircle: - if desiredWidth != 0 || desiredHeight != 0 { - paddingX = 0. - paddingY = 0. - } - - sideLength := math.Max(obj.Width+paddingX, obj.Height+paddingY) - obj.Width = sideLength - obj.Height = sideLength - - default: + if dslShape == d2target.ShapeImage { if desiredWidth == 0 { - obj.Width += float64(paddingX) + desiredWidth = defaultDims.Width } if desiredHeight == 0 { - obj.Height += float64(paddingY) + desiredHeight = defaultDims.Height + } + obj.Width = float64(go2.Max(MIN_SHAPE_SIZE, desiredWidth)) + obj.Height = float64(go2.Max(MIN_SHAPE_SIZE, desiredHeight)) + // images don't need further processing + continue + } + + contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(defaultDims.Width), float64(defaultDims.Height)) + shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape] + s := shape.NewShape(shapeType, contentBox) + + paddingX, paddingY := s.GetDefaultPadding() + if desiredWidth != 0 || desiredHeight != 0 { + paddingX = 0. + paddingY = 0. + } else { + // give shapes with icons extra padding to fit their label + if obj.Attributes.Icon != nil { + labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING) + // Evenly pad enough to fit label above icon + paddingX += labelHeight + paddingY += labelHeight + } + switch shapeType { + case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.IMAGE_TYPE: + default: + if obj.Attributes.Link != "" { + paddingX += 32 + } + if obj.Attributes.Tooltip != "" { + paddingX += 32 + } + } + } + + fitWidth, fitHeight := s.GetDimensionsToFit(contentBox.Width, contentBox.Height, paddingX, paddingY) + obj.Width = math.Max(float64(desiredWidth), fitWidth) + obj.Height = math.Max(float64(desiredHeight), fitHeight) + if s.AspectRatio1() { + sideLength := math.Max(obj.Width, obj.Height) + obj.Width = sideLength + obj.Height = sideLength + } else if desiredHeight == 0 || desiredWidth == 0 { + if s.GetType() == shape.PERSON_TYPE { + obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.PERSON_AR_LIMIT) } } } @@ -1252,19 +1353,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 +1430,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..8a61e9e8b 100644 --- a/d2layouts/d2sequence/sequence_diagram.go +++ b/d2layouts/d2sequence/sequence_diagram.go @@ -105,6 +105,12 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se sd.objectRank[actor] = rank if actor.Width < MIN_ACTOR_WIDTH { + dslShape := strings.ToLower(actor.Attributes.Shape.Value) + switch dslShape { + case d2target.ShapePerson, d2target.ShapeSquare, d2target.ShapeCircle: + // scale shape up to min width uniformly + actor.Height *= MIN_ACTOR_WIDTH / actor.Width + } actor.Width = MIN_ACTOR_WIDTH } sd.maxActorHeight = math.Max(sd.maxActorHeight, actor.Height) @@ -225,7 +231,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 +330,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/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg index f0a8aabf2..6e4e30c89 100644 --- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg +++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="1597" height="835" viewBox="-102 -102 1597 835">1Like starbucks or something -2I'm not sure what this is +}]]>1Like starbucks or something +2I'm not sure what this is 1https://d2lang.com -2Gee, I feel kind of LIGHT in the head now,knowing I can't make my satellite dish PAYMENTS! -3https://terrastruct.com +}]]>1https://d2lang.com +2Gee, I feel kind of LIGHT in the head now,knowing I can't make my satellite dish PAYMENTS! +3https://terrastruct.com 1Total abstinence is easier than perfect moderation -2Gee, I feel kind of LIGHT in the head now,knowing I can't make my satellite dish PAYMENTS! +}]]>1Total abstinence is easier than perfect moderation +2Gee, I feel kind of LIGHT in the head now,knowing I can't make my satellite dish PAYMENTS! `, mdCSS) } if sketchRunner != nil { - fmt.Fprintf(buf, d2sketch.DefineFillPattern()) + fmt.Fprint(buf, d2sketch.DefineFillPattern()) } // only define shadow filter if a shape uses it diff --git a/d2target/class.go b/d2target/class.go index f62a3ea43..3cc39921e 100644 --- a/d2target/class.go +++ b/d2target/class.go @@ -9,6 +9,9 @@ import ( const ( PrefixPadding = 10 PrefixWidth = 20 + CenterPadding = 50 + // 10px of padding top and bottom so text doesn't look squished + VerticalPadding = 20 ) type Class struct { diff --git a/d2target/d2target.go b/d2target/d2target.go index 255bd4852..b5325d07e 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -37,6 +37,10 @@ type Diagram struct { Shapes []Shape `json:"shapes"` Connections []Connection `json:"connections"` + + Layers []*Diagram `json:"layers,omitempty"` + Scenarios []*Diagram `json:"scenarios,omitempty"` + Steps []*Diagram `json:"steps,omitempty"` } func (diagram Diagram) HashID() (string, error) { @@ -547,19 +551,19 @@ func (s *Shape) GetIconSize(box *geo.Box) int { if iconPosition == label.InsideMiddleCenter { size = halfMinDimension } else { - size = go2.IntMin( + size = go2.Min( minDimension, - go2.IntMax(DEFAULT_ICON_SIZE, halfMinDimension), + go2.Max(DEFAULT_ICON_SIZE, halfMinDimension), ) } - size = go2.IntMin(size, MAX_ICON_SIZE) + size = go2.Min(size, MAX_ICON_SIZE) if !iconPosition.IsOutside() { - size = go2.IntMin(size, - go2.IntMin( - go2.IntMax(int(box.Width)-2*label.PADDING, 0), - go2.IntMax(int(box.Height)-2*label.PADDING, 0), + size = go2.Min(size, + go2.Min( + go2.Max(int(box.Width)-2*label.PADDING, 0), + go2.Max(int(box.Height)-2*label.PADDING, 0), ), ) } diff --git a/e2etests/README.md b/e2etests/README.md index f6f1e8d3c..c25e7ec49 100644 --- a/e2etests/README.md +++ b/e2etests/README.md @@ -17,6 +17,6 @@ If a change results in test diffs, you can run this script to generate a visual report with the old vs new renders. ``` -go run ./e2etests/report/main.go +go run ./e2etests/report/main.go -delta open ./e2etests/out/e2e_report.html ``` diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go index 35a7c6c6d..b1ac13cc8 100644 --- a/e2etests/e2e_test.go +++ b/e2etests/e2e_test.go @@ -12,7 +12,7 @@ import ( "cdr.dev/slog" - tassert "github.com/stretchr/testify/assert" + trequire "github.com/stretchr/testify/require" "oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/diff" @@ -101,18 +101,18 @@ func serde(t *testing.T, tc testCase, ruler *textmeasure.Ruler) { g, err := d2compiler.Compile("", strings.NewReader(tc.script), &d2compiler.CompileOptions{ UTF16: false, }) - tassert.Nil(t, err) + trequire.Nil(t, err) if len(g.Objects) > 0 { err = g.SetDimensions(nil, ruler, nil) - tassert.Nil(t, err) + trequire.Nil(t, err) d2near.WithoutConstantNears(ctx, g) d2sequence.WithoutSequenceDiagrams(ctx, g) } b, err := d2graph.SerializeGraph(g) - tassert.Nil(t, err) + trequire.Nil(t, err) var newG d2graph.Graph err = d2graph.DeserializeGraph(b, &newG) - tassert.Nil(t, err) + trequire.Nil(t, err) } func run(t *testing.T, tc testCase) { @@ -124,9 +124,7 @@ func run(t *testing.T, tc testCase) { var err error if tc.mtexts == nil { ruler, err = textmeasure.NewRuler() - if !tassert.Nil(t, err) { - return - } + trequire.Nil(t, err) serde(t, tc, ruler) } @@ -150,9 +148,7 @@ func run(t *testing.T, tc testCase) { ThemeID: 0, Layout: layout, }) - if !tassert.Nil(t, err) { - return - } + trequire.Nil(t, err) if tc.assertions != nil { t.Run("assertions", func(t *testing.T) { @@ -171,26 +167,19 @@ func run(t *testing.T, tc testCase) { assert.Success(t, err) err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600) assert.Success(t, err) - // if running from e2ereport.sh, we want to keep .got.svg on a failure - forReport := os.Getenv("E2E_REPORT") != "" - if !forReport { - defer os.Remove(pathGotSVG) - } // Check that it's valid SVG var xmlParsed interface{} err = xml.Unmarshal(svgBytes, &xmlParsed) assert.Success(t, err) + var err2 error err = diff.TestdataJSON(filepath.Join(dataPath, "board"), diagram) - assert.Success(t, err) if os.Getenv("SKIP_SVG_CHECK") == "" { - err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes) - assert.Success(t, err) - } - if forReport { - os.Remove(pathGotSVG) + err2 = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes) } + assert.Success(t, err) + assert.Success(t, err2) } } diff --git a/e2etests/regression_test.go b/e2etests/regression_test.go index 02d82f9b5..4e70e1f5b 100644 --- a/e2etests/regression_test.go +++ b/e2etests/regression_test.go @@ -1,7 +1,10 @@ package e2etests import ( + "math" "testing" + + "oss.terrastruct.com/d2/d2target" ) func testRegression(t *testing.T) { @@ -446,6 +449,30 @@ b -> c `, }, + { + name: "empty_class_height", + script: ` +class1: class with rows { + shape: class + -num: int + -timeout: int +} + +class2: class without rows { + shape: class +} +`, + assertions: func(t *testing.T, g *d2target.Diagram) { + if len(g.Shapes) != 2 { + t.Fatal("expected 2 shapes") + } + c1Height := float64(g.Shapes[0].Height) + c2Height := float64(g.Shapes[1].Height) + if math.Round(c1Height/2.) != c2Height { + t.Fatal("expected rowless class to be 1/2 height of class with 2 rows") + } + }, + }, } runa(t, tcs) diff --git a/e2etests/report/main.go b/e2etests/report/main.go index c471991ca..f71c7f1cd 100644 --- a/e2etests/report/main.go +++ b/e2etests/report/main.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "io/ioutil" + stdlog "log" "os" "os/exec" "path/filepath" @@ -38,6 +39,7 @@ func main() { flag.BoolVar(&deltaFlag, "delta", false, "Generate the report only for cases that changed.") flag.StringVar(&testSetFlag, "test-set", "", "Only run set of tests matching this string. e.g. regressions") flag.StringVar(&testCaseFlag, "test-case", "", "Only run tests matching this string. e.g. all_shapes") + skipTests := flag.Bool("skip-tests", false, "Skip running tests first") flag.BoolVar(&vFlag, "v", false, "verbose") flag.Parse() @@ -52,18 +54,20 @@ func main() { testDir = "./e2etests" } - ctx := log.Stderr(context.Background()) - ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - cmd := exec.CommandContext(ctx, "go", "test", testDir, "-run", testMatchString, vString) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "FORCE_COLOR=1") - cmd.Env = append(cmd.Env, "DEBUG=1") - cmd.Env = append(cmd.Env, "TEST_MODE=on") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - log.Debug(ctx, cmd.String()) - _ = cmd.Run() + if !*skipTests { + ctx := log.Stderr(context.Background()) + ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) + defer cancel() + cmd := exec.CommandContext(ctx, "go", "test", testDir, "-run", testMatchString, vString) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "FORCE_COLOR=1") + cmd.Env = append(cmd.Env, "DEBUG=1") + cmd.Env = append(cmd.Env, "TEST_MODE=on") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Debug(ctx, cmd.String()) + _ = cmd.Run() + } var tests []TestItem err := filepath.Walk(filepath.Join(testDir, "testdata"), func(path string, info os.FileInfo, err error) error { @@ -144,6 +148,13 @@ func main() { } path := os.Getenv("REPORT_OUTPUT") + if path == "" { + path = filepath.Join(testDir, "./out/e2e_report.html") + } + err = os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + stdlog.Fatal(err) + } f, err := os.Create(path) if err != nil { panic(fmt.Errorf("error creating file `%s`. %v", path, err)) diff --git a/e2etests/report/template.html b/e2etests/report/template.html index 537a035ea..28ed677ed 100644 --- a/e2etests/report/template.html +++ b/e2etests/report/template.html @@ -1,16 +1,26 @@ E2E report +

Table of Contents

+
{{range .Tests}} -
-

{{.Name}}

+
+

{{.Name}}

{{ if .ExpSVG }} -

Expected

- -

Got

+

Expected

{{ end }} - +

Got

+
+ {{ if .ExpSVG }} + + {{ end }} + +
{{end}}
@@ -20,17 +30,24 @@ flex-wrap: wrap; } .case { - align-items: center; - justify-content: center; + position: relative; padding: 20px; + width: 100%; } - .case svg { - width: 400px; - height: 400px; + .case-img-wrapper { + display: flex; + align-items: center; } - .case pre { - font-size: 10pt; - width: 400px; + .case img { + width: 600px; + } + .case-got { + position: absolute; + left: 600px; + } + .case h2 { + margin: 0; + display: inline; } diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index 0fa299c0d..a489f6398 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,97 @@ a.sp1 -> a.sp2: redirect a.sp2 -> b: bar `, }, + { + name: "people", + script: ` +a.shape: person +b.shape: person +c.shape: person +d.shape: person +e.shape: person +f.shape: person +g.shape: person + +a: - +b: -- +c: ---- +d: -------- +e: ---------------- +f: -------------------------------- +g: ---------------------------------------------------------------- + +1.shape: person +2.shape: person +3.shape: person +4.shape: person +5.shape: person + +1.width: 16 +2.width: 64 +3.width: 128 +4.width: 512 + +# entering both width and height overrides aspect ratio limit +5.height: 256 +5.width: 32 +`, + }, + { + 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-class/dagre/board.exp.json b/e2etests/testdata/measured/empty-class/dagre/board.exp.json index 6a7752e8b..43926e6d3 100644 --- a/e2etests/testdata/measured/empty-class/dagre/board.exp.json +++ b/e2etests/testdata/measured/empty-class/dagre/board.exp.json @@ -10,7 +10,7 @@ "y": 0 }, "width": 112, - "height": 12, + "height": 44, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, diff --git a/e2etests/testdata/measured/empty-class/dagre/sketch.exp.svg b/e2etests/testdata/measured/empty-class/dagre/sketch.exp.svg index a87ea964c..6927edd4b 100644 --- a/e2etests/testdata/measured/empty-class/dagre/sketch.exp.svg +++ b/e2etests/testdata/measured/empty-class/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="316" height="216" viewBox="-102 -102 316 216"> \ No newline at end of file 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/code_leading_trailing_newlines/dagre/board.exp.json b/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/board.exp.json index 540dacbd6..79fe2a3c7 100644 --- a/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/board.exp.json +++ b/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/board.exp.json @@ -37,8 +37,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 239, - "labelHeight": 150, + "labelWidth": 234, + "labelHeight": 145, "zIndex": 0, "level": 1 }, @@ -77,8 +77,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 160, - "labelHeight": 118, + "labelWidth": 155, + "labelHeight": 113, "zIndex": 0, "level": 1 }, @@ -117,8 +117,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 160, - "labelHeight": 118, + "labelWidth": 155, + "labelHeight": 113, "zIndex": 0, "level": 1 } diff --git a/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/sketch.exp.svg b/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/sketch.exp.svg index a42fc16c7..ee57c551c 100644 --- a/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/code_leading_trailing_newlines/dagre/sketch.exp.svg @@ -56,7 +56,7 @@ width="883" height="354" viewBox="-102 -102 883 354"> \ 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..411f09e45 100644 --- a/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json +++ b/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json @@ -4,13 +4,13 @@ "shapes": [ { "id": "x", - "type": "", + "type": "rectangle", "pos": { "x": 0, "y": 0 }, - "width": 426, - "height": 226, + "width": 306, + "height": 166, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -37,21 +37,21 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 18, - "labelHeight": 41, + "labelWidth": 13, + "labelHeight": 36, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 1 }, { "id": "x.a", - "type": "", + "type": "rectangle", "pos": { "x": 50, "y": 50 }, - "width": 113, - "height": 126, + "width": 53, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -78,21 +78,21 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 13, - "labelHeight": 26, + "labelWidth": 8, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 2 }, { "id": "x.b", - "type": "", + "type": "rectangle", "pos": { - "x": 263, + "x": 203, "y": 50 }, - "width": 113, - "height": 126, + "width": 53, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -119,8 +119,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 13, - "labelHeight": 26, + "labelWidth": 8, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 2 @@ -153,56 +153,56 @@ "labelPercentage": 0, "route": [ { - "x": 163, - "y": 73.37662337662337 + "x": 103, + "y": 68.38440111420613 }, { - "x": 189.66666666666669, - "y": 54.675324675324674 + "x": 129.66666666666669, + "y": 53.67688022284123 }, { - "x": 198, + "x": 138, "y": 50 }, { - "x": 200.5, + "x": 140.5, "y": 50 }, { - "x": 203, + "x": 143, "y": 50 }, { - "x": 206.33333333333331, - "y": 62.6 + "x": 146.33333333333331, + "y": 56.6 }, { - "x": 208.83333333333331, - "y": 81.5 + "x": 148.83333333333331, + "y": 66.5 }, { - "x": 211.33333333333334, - "y": 100.4 + "x": 151.33333333333334, + "y": 76.4 }, { - "x": 211.33333333333334, - "y": 125.6 + "x": 151.33333333333334, + "y": 89.6 }, { - "x": 208.83333333333331, - "y": 144.5 + "x": 148.83333333333331, + "y": 99.5 }, { - "x": 206.33333333333331, - "y": 163.4 + "x": 146.33333333333331, + "y": 109.4 }, { - "x": 189.66666666666669, - "y": 171.32467532467533 + "x": 129.66666666666669, + "y": 112.32311977715878 }, { - "x": 163, - "y": 152.62337662337663 + "x": 103, + "y": 97.61559888579387 } ], "isCurve": true, 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..873e458b3 100644 --- a/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="630" height="430" viewBox="-102 -102 630 430">

Oldest message

-

Offset

-

Last message

-

Next message will be
+

Oldest message

+

Offset

+

Last message

+

Next message will be
inserted here

-
M0M1M2M3M4M5M6 - +
M0M1M2M3M4M5M6 +

Oldest message

-

Offset

-

Last message

-

Next message will be
+

Oldest message

+

Offset

+

Last message

+

Next message will be
inserted here

-
M0M1M2M3M4M5M6 - +
M0M1M2M3M4M5M6 + class with rows- +num +int- +timeout +intclass without rows + + + \ No newline at end of file diff --git a/e2etests/testdata/regression/empty_class_height/elk/board.exp.json b/e2etests/testdata/regression/empty_class_height/elk/board.exp.json new file mode 100644 index 000000000..e2cad4326 --- /dev/null +++ b/e2etests/testdata/regression/empty_class_height/elk/board.exp.json @@ -0,0 +1,104 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "class1", + "type": "class", + "pos": { + "x": 12, + "y": 12 + }, + "width": 319, + "height": 184, + "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" + } + ], + "methods": null, + "columns": null, + "label": "class with rows", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 214, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + }, + { + "id": "class2", + "type": "class", + "pos": { + "x": 351, + "y": 58 + }, + "width": 362, + "height": 92, + "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": null, + "methods": null, + "columns": null, + "label": "class without rows", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 257, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + } + ], + "connections": [] +} diff --git a/e2etests/testdata/regression/empty_class_height/elk/sketch.exp.svg b/e2etests/testdata/regression/empty_class_height/elk/sketch.exp.svg new file mode 100644 index 000000000..58188b652 --- /dev/null +++ b/e2etests/testdata/regression/empty_class_height/elk/sketch.exp.svg @@ -0,0 +1,56 @@ + +class with rows- +num +int- +timeout +intclass without rows + + + \ No newline at end of file diff --git a/e2etests/testdata/regression/empty_sequence/dagre/board.exp.json b/e2etests/testdata/regression/empty_sequence/dagre/board.exp.json index 38bbafd16..99a1f6a5b 100644 --- a/e2etests/testdata/regression/empty_sequence/dagre/board.exp.json +++ b/e2etests/testdata/regression/empty_sequence/dagre/board.exp.json @@ -9,8 +9,8 @@ "x": 13, "y": 0 }, - "width": 140, - "height": 126, + "width": 80, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 0, @@ -37,8 +37,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 40, - "labelHeight": 26, + "labelWidth": 35, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 @@ -48,10 +48,10 @@ "type": "sequence_diagram", "pos": { "x": 0, - "y": 226 + "y": 166 }, - "width": 166, - "height": 126, + "width": 106, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 0, @@ -78,8 +78,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 66, - "labelHeight": 26, + "labelWidth": 61, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 @@ -112,20 +112,20 @@ "labelPercentage": 0, "route": [ { - "x": 83, + "x": 53, + "y": 66 + }, + { + "x": 53, + "y": 106 + }, + { + "x": 53, "y": 126 }, { - "x": 83, + "x": 53, "y": 166 - }, - { - "x": 83, - "y": 186 - }, - { - "x": 83, - "y": 226 } ], "isCurve": true, diff --git a/e2etests/testdata/regression/empty_sequence/dagre/sketch.exp.svg b/e2etests/testdata/regression/empty_sequence/dagre/sketch.exp.svg index 7e846f812..3a511b01d 100644 --- a/e2etests/testdata/regression/empty_sequence/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/empty_sequence/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="366" height="552" viewBox="-100 -100 366 552">

hey

+

hey

  • they
      @@ -804,8 +804,8 @@ width="317" height="771" viewBox="-102 -102 317 771">

      hey

      +

      hey

      • they
          @@ -804,8 +804,8 @@ width="317" height="771" viewBox="-90 -90 317 771">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. - - +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. + + 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. - - +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. + + aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 - - - - - - - - - +aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 + + + + + + + + + aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 - - - - - - - - - +aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 + + + + + + + + + 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..193998b7b --- /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": 103, + "height": 66, + "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": 58, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "roof", + "type": "rectangle", + "pos": { + "x": 135, + "y": 12 + }, + "width": 75, + "height": 66, + "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": 30, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "garage", + "type": "rectangle", + "pos": { + "x": 230, + "y": 12 + }, + "width": 94, + "height": 66, + "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": 49, + "labelHeight": 21, + "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": 88, + "height": 66, + "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": 43, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "glass", + "type": "rectangle", + "pos": { + "x": 120, + "y": 12 + }, + "width": 81, + "height": 66, + "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": 36, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] + }, + { + "name": "roof", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "shingles", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 104, + "height": 66, + "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": 59, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "starlink", + "type": "rectangle", + "pos": { + "x": 136, + "y": 12 + }, + "width": 101, + "height": 66, + "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": 56, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "utility hookup", + "type": "rectangle", + "pos": { + "x": 257, + "y": 12 + }, + "width": 146, + "height": 66, + "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": 101, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] + }, + { + "name": "garage", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "tools", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 81, + "height": 66, + "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": 36, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "vehicles", + "type": "rectangle", + "pos": { + "x": 113, + "y": 12 + }, + "width": 103, + "height": 66, + "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": 58, + "labelHeight": 21, + "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": 391, + "height": 216, + "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": 187, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "find contractors.craigslist", + "type": "rectangle", + "pos": { + "x": 87, + "y": 87 + }, + "width": 110, + "height": 66, + "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": 65, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "find contractors.facebook", + "type": "rectangle", + "pos": { + "x": 217, + "y": 87 + }, + "width": 111, + "height": 66, + "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": 66, + "labelHeight": 21, + "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": 159, + "height": 66, + "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": 114, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "solicit quotes", + "type": "rectangle", + "pos": { + "x": 21, + "y": 178 + }, + "width": 140, + "height": 66, + "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": 95, + "labelHeight": 21, + "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": 91.5, + "y": 78 + }, + { + "x": 91.5, + "y": 178 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] + }, + { + "name": "3", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "obtain quotes", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 143, + "height": 66, + "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": 98, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "negotiate", + "type": "rectangle", + "pos": { + "x": 27, + "y": 178 + }, + "width": 112, + "height": 66, + "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": 67, + "labelHeight": 21, + "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": 83.5, + "y": 78 + }, + { + "x": 83.5, + "y": 178 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] + }, + { + "name": "4", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "negotiate", + "type": "rectangle", + "pos": { + "x": 39, + "y": 12 + }, + "width": 112, + "height": 66, + "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": 67, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "book the best bid", + "type": "rectangle", + "pos": { + "x": 12, + "y": 178 + }, + "width": 167, + "height": 66, + "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": 122, + "labelHeight": 21, + "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": 95.5, + "y": 78 + }, + { + "x": 95.5, + "y": 178 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] + } + ] + } + ], + "scenarios": [ + { + "name": "storm", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "window", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 103, + "height": 66, + "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": 58, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "roof", + "type": "rectangle", + "pos": { + "x": 135, + "y": 12 + }, + "width": 75, + "height": 66, + "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": 30, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "garage", + "type": "rectangle", + "pos": { + "x": 230, + "y": 12 + }, + "width": 94, + "height": 66, + "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": 49, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "water", + "type": "rectangle", + "pos": { + "x": 344, + "y": 12 + }, + "width": 88, + "height": 66, + "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": 43, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "rain", + "type": "rectangle", + "pos": { + "x": 452, + "y": 12 + }, + "width": 73, + "height": 66, + "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": 28, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "thunder", + "type": "rectangle", + "pos": { + "x": 545, + "y": 12 + }, + "width": 103, + "height": 66, + "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": 58, + "labelHeight": 21, + "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..c046cad60 --- /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..bb6843a80 100644 --- a/e2etests/testdata/stable/connected_container/dagre/board.exp.json +++ b/e2etests/testdata/stable/connected_container/dagre/board.exp.json @@ -4,13 +4,13 @@ "shapes": [ { "id": "a", - "type": "", + "type": "rectangle", "pos": { - "x": 41, + "x": 40, "y": 0 }, - "width": 213, - "height": 226, + "width": 156, + "height": 166, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -37,21 +37,21 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 17, - "labelHeight": 41, + "labelWidth": 12, + "labelHeight": 36, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 1 }, { "id": "a.b", - "type": "", + "type": "rectangle", "pos": { - "x": 91, + "x": 94, "y": 50 }, - "width": 113, - "height": 126, + "width": 53, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -78,21 +78,21 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 13, - "labelHeight": 26, + "labelWidth": 8, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 2 }, { "id": "c", - "type": "", + "type": "rectangle", "pos": { "x": 40, - "y": 326 + "y": 266 }, - "width": 214, - "height": 226, + "width": 157, + "height": 166, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -119,21 +119,21 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 17, - "labelHeight": 41, + "labelWidth": 12, + "labelHeight": 36, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 1 }, { "id": "c.d", - "type": "", + "type": "rectangle", "pos": { - "x": 90, - "y": 376 + "x": 93, + "y": 316 }, - "width": 114, - "height": 126, + "width": 54, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -160,21 +160,21 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 14, - "labelHeight": 26, + "labelWidth": 9, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 2 }, { "id": "f", - "type": "", + "type": "rectangle", "pos": { "x": 0, - "y": 652 + "y": 532 }, - "width": 294, - "height": 326, + "width": 237, + "height": 266, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -201,21 +201,21 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 14, - "labelHeight": 41, + "labelWidth": 9, + "labelHeight": 36, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 1 }, { "id": "f.h", - "type": "", + "type": "rectangle", "pos": { "x": 40, - "y": 702 + "y": 582 }, - "width": 214, - "height": 226, + "width": 157, + "height": 166, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -242,21 +242,21 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 16, - "labelHeight": 36, + "labelWidth": 11, + "labelHeight": 31, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 2 }, { "id": "f.h.g", - "type": "", + "type": "rectangle", "pos": { - "x": 90, - "y": 752 + "x": 93, + "y": 632 }, - "width": 114, - "height": 126, + "width": 54, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -283,8 +283,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 14, - "labelHeight": 26, + "labelWidth": 9, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 3 @@ -317,32 +317,32 @@ "labelPercentage": 0, "route": [ { - "x": 147, + "x": 120, + "y": 116 + }, + { + "x": 120, + "y": 156 + }, + { + "x": 120, "y": 176 }, { - "x": 147, - "y": 216 + "x": 120, + "y": 191 }, { - "x": 147, - "y": 236 + "x": 120, + "y": 206 }, { - "x": 147, - "y": 251 + "x": 120, + "y": 276 }, { - "x": 147, - "y": 266 - }, - { - "x": 147, - "y": 336 - }, - { - "x": 147, - "y": 376 + "x": 120, + "y": 316 } ], "isCurve": true, @@ -377,44 +377,44 @@ "labelPercentage": 0, "route": [ { - "x": 147, - "y": 502 + "x": 120, + "y": 382 }, { - "x": 147, - "y": 542 + "x": 120, + "y": 422 }, { - "x": 147, - "y": 562 + "x": 120, + "y": 442 }, { - "x": 147, - "y": 577 + "x": 120, + "y": 457 }, { - "x": 147, + "x": 120, + "y": 472 + }, + { + "x": 120, + "y": 492 + }, + { + "x": 120, + "y": 507 + }, + { + "x": 120, + "y": 522 + }, + { + "x": 120, "y": 592 }, { - "x": 147, - "y": 612 - }, - { - "x": 147, - "y": 627 - }, - { - "x": 147, - "y": 642 - }, - { - "x": 147, - "y": 712 - }, - { - "x": 147, - "y": 752 + "x": 120, + "y": 632 } ], "isCurve": true, diff --git a/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg b/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg index 5592ff4af..720dd613e 100644 --- a/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/connected_container/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -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.

          +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.

          +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

          +
          + poll the peopleresultsunfavorablefavorablewill of the people

          A winning strategy

          -
          - +poll the peopleresultsunfavorablefavorablewill of the people

          A winning strategy

          +
          +

          Markdown: Syntax

          +

          Markdown: Syntax

          • Overview
              @@ -1053,8 +1053,8 @@ title for the link, surrounded in quotes. For example:

              Code

              Unlike a pre-formatted code block, a code span indicates code within a normal paragraph. For example:

              -
          ab - +
          ab +

          Markdown: Syntax

          +

          Markdown: Syntax

          • Overview
              @@ -1053,8 +1053,8 @@ title for the link, surrounded in quotes. For example:

              Code

              Unlike a pre-formatted code block, a code span indicates code within a normal paragraph. For example:

              -
          ab - +
          ab +

          Note: This document is itself written using Markdown; you +

          Note: This document is itself written using Markdown; you can see the source for it by adding '.text' to the URL.


          Overview

          -
          ab - +
          ab +

          Note: This document is itself written using Markdown; you +

          Note: This document is itself written using Markdown; you can see the source for it by adding '.text' to the URL.


          Overview

          -
          ab - +
          ab + mixed togethersugarsolution we get - - +mixed togethersugarsolution we get + + mixed togethersugarsolution we get - - +mixed togethersugarsolution we get + +
            +
            • Overview
              • Philosophy
              • @@ -808,8 +808,8 @@ width="583" height="756" viewBox="-102 -102 583 756">
                  +
                  • Overview
                    • Philosophy
                    • @@ -808,8 +808,8 @@ width="583" height="756" viewBox="-90 -90 583 756">
                        +
                        • Overview ok this is all measured
                          • Philosophy
                          • @@ -804,8 +804,8 @@ width="449" height="732" viewBox="-102 -102 449 732">
                              +
                              • Overview ok this is all measured
                                • Philosophy
                                • @@ -804,8 +804,8 @@ width="449" height="732" viewBox="-90 -90 449 732">
                                    +
                                    • Overview
                                      • Philosophy
                                      • @@ -829,8 +829,8 @@ width="551" height="1168" viewBox="-102 -102 551 1168">
                                          +
                                          • Overview
                                            • Philosophy
                                            • @@ -829,8 +829,8 @@ width="551" height="1168" viewBox="-90 -90 551 1168">

                                              List items may consist of multiple paragraphs. Each subsequent +

                                              List items may consist of multiple paragraphs. Each subsequent paragraph in a list item must be indented by either 4 spaces or one tab:

                                                @@ -827,8 +827,8 @@ sit amet, consectetuer adipiscing elit.

                                                Another item in the same list.

                                            -
                                          ab - +
                                        ab +

                                        List items may consist of multiple paragraphs. Each subsequent +

                                        List items may consist of multiple paragraphs. Each subsequent paragraph in a list item must be indented by either 4 spaces or one tab:

                                          @@ -827,8 +827,8 @@ sit amet, consectetuer adipiscing elit.

                                          Another item in the same list.

                                      -
                                    ab - +
                                  ab +

                                  Markdown: Syntax

                                  -
                                  ab - +

                                  Markdown: Syntax

                                  +
                                  ab +

                                  Markdown: Syntax

                                  -
                                  ab - +

                                  Markdown: Syntax

                                  +
                                  ab +

                                  Every frustum longs to be a cone

                                  +

                                  Every frustum longs to be a cone

                                  • A continuing flow of paper is sufficient to continue the flow of paper
                                  • Please remain calm, it's no use both of us being hysterical at the same time
                                  • Visits always give pleasure: if not on arrival, then on the departure

                                  Festivity Level 1: Your guests are chatting amiably with each other.

                                  -
                                  xy - +
                                  xy +

                                  Every frustum longs to be a cone

                                  +

                                  Every frustum longs to be a cone

                                  • A continuing flow of paper is sufficient to continue the flow of paper
                                  • Please remain calm, it's no use both of us being hysterical at the same time
                                  • Visits always give pleasure: if not on arrival, then on the departure

                                  Festivity Level 1: Your guests are chatting amiably with each other.

                                  -
                                  xy - +
                                  xy + 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.

                        -
                        +
                      {
                      +
                      {
                       	fenced: "block",
                       	of: "json",
                       }
                       
                      -
                      ab - +
                      ab +
                      {
                      +
                      {
                       	fenced: "block",
                       	of: "json",
                       }
                       
                      -
                      ab - +
                      ab +

                      a line of text and an

                      +

                      a line of text and an

                      {
                       	indented: "block",
                       	of: "json",
                       }
                       
                      -
                      ab - +
                      ab +

                      a line of text and an

                      +

                      a line of text and an

                      {
                       	indented: "block",
                       	of: "json",
                       }
                       
                      -
                      ab - +
                      ab +

                      code

                      -
                      ab - +

                      code

                      +
                      ab +

                      code

                      -
                      ab - +

                      code

                      +
                      ab +

                      A paragraph is simply one or more consecutive lines of text, separated +

                      A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. (A blank line is any line that looks like a blank line -- a line containing nothing but spaces or tabs is considered blank.) Normal paragraphs should not be indented with spaces or tabs.

                      -
                      ab - +
                      ab +

                      A paragraph is simply one or more consecutive lines of text, separated +

                      A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. (A blank line is any line that looks like a blank line -- a line containing nothing but spaces or tabs is considered blank.) Normal paragraphs should not be indented with spaces or tabs.

                      -
                      ab - +
                      ab + -------------------------------------------------------------------------------------------------------------------------------12345 + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/people/elk/board.exp.json b/e2etests/testdata/stable/people/elk/board.exp.json new file mode 100644 index 000000000..86e8ac798 --- /dev/null +++ b/e2etests/testdata/stable/people/elk/board.exp.json @@ -0,0 +1,499 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "a", + "type": "person", + "pos": { + "x": 12, + "y": 269 + }, + "width": 49, + "height": 66, + "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": true, + "underline": false, + "labelWidth": 5, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b", + "type": "person", + "pos": { + "x": 81, + "y": 269 + }, + "width": 64, + "height": 66, + "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": true, + "underline": false, + "labelWidth": 11, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c", + "type": "person", + "pos": { + "x": 165, + "y": 269 + }, + "width": 89, + "height": 66, + "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": true, + "underline": false, + "labelWidth": 21, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "d", + "type": "person", + "pos": { + "x": 274, + "y": 254 + }, + "width": 142, + "height": 95, + "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": true, + "underline": false, + "labelWidth": 43, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "e", + "type": "person", + "pos": { + "x": 436, + "y": 220 + }, + "width": 245, + "height": 163, + "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": true, + "underline": false, + "labelWidth": 85, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "f", + "type": "person", + "pos": { + "x": 701, + "y": 151 + }, + "width": 453, + "height": 302, + "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": true, + "underline": false, + "labelWidth": 170, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "g", + "type": "person", + "pos": { + "x": 1174, + "y": 12 + }, + "width": 870, + "height": 580, + "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": true, + "underline": false, + "labelWidth": 340, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "1", + "type": "person", + "pos": { + "x": 2064, + "y": 291 + }, + "width": 18, + "height": 21, + "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": "1", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 7, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "2", + "type": "person", + "pos": { + "x": 2102, + "y": 280 + }, + "width": 64, + "height": 43, + "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": "2", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "3", + "type": "person", + "pos": { + "x": 2186, + "y": 259 + }, + "width": 128, + "height": 85, + "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": "3", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "4", + "type": "person", + "pos": { + "x": 2334, + "y": 131 + }, + "width": 512, + "height": 341, + "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": "4", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "5", + "type": "person", + "pos": { + "x": 2866, + "y": 174 + }, + "width": 32, + "height": 256, + "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": "5", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [] +} diff --git a/e2etests/testdata/stable/people/elk/sketch.exp.svg b/e2etests/testdata/stable/people/elk/sketch.exp.svg new file mode 100644 index 000000000..4d0a12f5f --- /dev/null +++ b/e2etests/testdata/stable/people/elk/sketch.exp.svg @@ -0,0 +1,52 @@ + +-------------------------------------------------------------------------------------------------------------------------------12345 + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/pre/dagre/board.exp.json b/e2etests/testdata/stable/pre/dagre/board.exp.json index c0ebae2a7..589035e7e 100644 --- a/e2etests/testdata/stable/pre/dagre/board.exp.json +++ b/e2etests/testdata/stable/pre/dagre/board.exp.json @@ -7,7 +7,7 @@ "type": "text", "pos": { "x": 0, - "y": 226 + "y": 166 }, "width": 602, "height": 170, @@ -44,13 +44,13 @@ }, { "id": "a", - "type": "", + "type": "rectangle", "pos": { - "x": 245, + "x": 275, "y": 0 }, - "width": 113, - "height": 126, + "width": 53, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -77,21 +77,21 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 13, - "labelHeight": 26, + "labelWidth": 8, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 }, { "id": "b", - "type": "", + "type": "rectangle", "pos": { - "x": 245, - "y": 496 + "x": 275, + "y": 436 }, - "width": 113, - "height": 126, + "width": 53, + "height": 66, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -118,8 +118,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 13, - "labelHeight": 26, + "labelWidth": 8, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 @@ -151,6 +151,14 @@ "labelPosition": "", "labelPercentage": 0, "route": [ + { + "x": 301, + "y": 66 + }, + { + "x": 301, + "y": 106 + }, { "x": 301, "y": 126 @@ -158,14 +166,6 @@ { "x": 301, "y": 166 - }, - { - "x": 301, - "y": 186 - }, - { - "x": 301, - "y": 226 } ], "isCurve": true, @@ -199,6 +199,14 @@ "labelPosition": "", "labelPercentage": 0, "route": [ + { + "x": 301, + "y": 336 + }, + { + "x": 301, + "y": 376 + }, { "x": 301, "y": 396 @@ -206,14 +214,6 @@ { "x": 301, "y": 436 - }, - { - "x": 301, - "y": 456 - }, - { - "x": 301, - "y": 496 } ], "isCurve": true, diff --git a/e2etests/testdata/stable/pre/dagre/sketch.exp.svg b/e2etests/testdata/stable/pre/dagre/sketch.exp.svg index 594198f60..41b1f6cef 100644 --- a/e2etests/testdata/stable/pre/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/pre/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="806" height="826" viewBox="-102 -102 806 826">

                      Here is an example of AppleScript:

                      +

                      Here is an example of AppleScript:

                      tell application "Foo"
                           beep
                       end tell
                       

                      A code block continues until it reaches a line that is not indented (or the end of the article).

                      -
                      ab - +
                      ab +

                      Here is an example of AppleScript:

                      +

                      Here is an example of AppleScript:

                      tell application "Foo"
                           beep
                       end tell
                       

                      A code block continues until it reaches a line that is not indented (or the end of the article).

                      -
                      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..bad736b99 --- /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": 850, + "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": 159, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "Office chatter.alice", + "type": "rectangle", + "pos": { + "x": 36, + "y": 122 + }, + "width": 150, + "height": 66, + "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": 33, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.bob", + "type": "rectangle", + "pos": { + "x": 286, + "y": 122 + }, + "width": 150, + "height": 66, + "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": 43, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "Office chatter.alice.a", + "type": "rectangle", + "pos": { + "x": 105, + "y": 692 + }, + "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": 7, + "labelHeight": 21, + "zIndex": 2, + "level": 3 + }, + { + "id": "Office chatter.awkward small talk", + "type": "rectangle", + "pos": { + "x": 37, + "y": 278 + }, + "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": 130, + "labelHeight": 21, + "labelPosition": "INSIDE_TOP_LEFT", + "zIndex": 3, + "level": 2 + }, + { + "id": "Office chatter.awkward small talk.icebreaker attempt", + "type": "rectangle", + "pos": { + "x": 61, + "y": 538 + }, + "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": 129, + "labelHeight": 21, + "labelPosition": "INSIDE_TOP_LEFT", + "zIndex": 3, + "level": 3 + }, + { + "id": "Office chatter.awkward small talk.unfortunate outcome", + "type": "rectangle", + "pos": { + "x": 67, + "y": 668 + }, + "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": 143, + "labelHeight": 21, + "labelPosition": "INSIDE_TOP_LEFT", + "zIndex": 3, + "level": 3 + }, + { + "id": "Office chatter.bob.a", + "type": "rectangle", + "pos": { + "x": 355, + "y": 692 + }, + "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": 7, + "labelHeight": 21, + "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": 318 + }, + { + "x": 361, + "y": 318 + } + ], + "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": 448 + }, + { + "x": 111, + "y": 448 + } + ], + "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": 578 + }, + { + "x": 361, + "y": 578 + } + ], + "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": 708 + }, + { + "x": 117, + "y": 708 + } + ], + "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": 188 + }, + { + "x": 111, + "y": 838 + } + ], + "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": 188 + }, + { + "x": 361, + "y": 838 + } + ], + "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..44b32c262 --- /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..3582b4b80 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 @@ -37,8 +37,8 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 128, - "labelHeight": 41, + "labelWidth": 123, + "labelHeight": 36, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 1 @@ -78,8 +78,8 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 161, - "labelHeight": 36, + "labelWidth": 156, + "labelHeight": 31, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 2 @@ -119,8 +119,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 68, - "labelHeight": 26, + "labelWidth": 63, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 3 @@ -160,8 +160,8 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 197, - "labelHeight": 36, + "labelWidth": 192, + "labelHeight": 31, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 2 @@ -201,8 +201,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 44, - "labelHeight": 26, + "labelWidth": 39, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 3 @@ -242,8 +242,8 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 149, - "labelHeight": 36, + "labelWidth": 144, + "labelHeight": 31, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 2 @@ -283,8 +283,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 65, - "labelHeight": 26, + "labelWidth": 60, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 3 @@ -324,8 +324,8 @@ "italic": false, "bold": false, "underline": false, - "labelWidth": 193, - "labelHeight": 36, + "labelWidth": 188, + "labelHeight": 31, "labelPosition": "INSIDE_TOP_CENTER", "zIndex": 0, "level": 2 @@ -365,8 +365,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 36, - "labelHeight": 26, + "labelWidth": 31, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 3 @@ -406,8 +406,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 45, - "labelHeight": 26, + "labelWidth": 40, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 @@ -447,12 +447,87 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 91, - "labelHeight": 26, + "labelWidth": 86, + "labelHeight": 21, "labelPosition": "INSIDE_MIDDLE_CENTER", "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": 70, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + }, { "id": "users", "type": "sql_table", @@ -629,8 +704,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 61, - "labelHeight": 36, + "labelWidth": 56, + "labelHeight": 31, "zIndex": 0, "level": 1, "primaryAccentColor": "#0D32B2", @@ -638,89 +713,14 @@ "neutralAccentColor": "#676C7E" }, { - "id": "class", - "type": "class", + "id": "container", + "type": "rectangle", "pos": { - "x": 1018, - "y": 756 + "x": 2021, + "y": 195 }, - "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": 114, + "height": 66, "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, - "labelHeight": 26, + "labelWidth": 69, + "labelHeight": 21, "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", @@ -788,8 +828,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 196, - "labelHeight": 70, + "labelWidth": 191, + "labelHeight": 65, "zIndex": 0, "level": 1 }, @@ -797,11 +837,11 @@ "id": "small code", "type": "code", "pos": { - "x": 1980, - "y": 2233 + "x": 1983, + "y": 2236 }, - "width": 196, - "height": 70, + "width": 191, + "height": 65, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -828,49 +868,8 @@ "italic": false, "bold": true, "underline": false, - "labelWidth": 196, - "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", + "labelWidth": 191, + "labelHeight": 65, "zIndex": 0, "level": 1 } @@ -903,11 +902,11 @@ "route": [ { "x": 1418, - "y": 356 + "y": 355 }, { "x": 1418, - "y": 396 + "y": 395.8 }, { "x": 1418, @@ -1059,11 +1058,11 @@ "route": [ { "x": 2078, - "y": 291 + "y": 261 }, { "x": 2078, - "y": 383 + "y": 377 }, { "x": 2078, @@ -1175,11 +1174,11 @@ }, { "x": 2078, - "y": 2061 + "y": 2061.5 }, { "x": 2078, - "y": 2233 + "y": 2235.5 } ], "isCurve": true, 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..d806caff2 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">