diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 6dbdee6f0..17f4ec3db 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,8 +1,13 @@
#### Features ๐
+- `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)
+
#### Improvements ๐งน
#### Bugfixes โ๏ธ
+
+- Fixes groups overlapping in sequence diagrams when they end in a self loop. [#728](https://github.com/terrastruct/d2/pull/728)
diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1
index b1bcb709c..2d4876a23 100644
--- a/ci/release/template/man/d2.1
+++ b/ci/release/template/man/d2.1
@@ -13,7 +13,7 @@
.Nm d2
.Ar layout Op Ar name
.Nm d2
-.Ar fmt Ar file.d2
+.Ar fmt Ar file.d2 ...
.Sh DESCRIPTION
.Nm
compiles and renders
@@ -83,10 +83,8 @@ Print version information and exit.
Lists available layout engine options with short help.
.It Ar layout Op Ar name
Display long help for a particular layout engine, including its configuration options.
-.It Ar fmt Ar file.d2
-Format
-.Ar file.d2
-.Ns .
+.It Ar fmt Ar file.d2 ...
+Format all passed files.
.El
.Sh SEE ALSO
.Xr d2plugin-tala 1
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 9214f23ad..bdc5acb8b 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -343,6 +343,8 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
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()}
}
}
@@ -567,6 +569,11 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
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`)
diff --git a/d2exporter/export.go b/d2exporter/export.go
index 86b6fc559..06628af70 100644
--- a/d2exporter/export.go
+++ b/d2exporter/export.go
@@ -95,6 +95,9 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
if obj.Attributes.Style.Font != nil {
shape.FontFamily = obj.Attributes.Style.Font.Value
}
+ if obj.Attributes.Style.DoubleBorder != nil {
+ shape.DoubleBorder, _ = strconv.ParseBool(obj.Attributes.Style.DoubleBorder.Value)
+ }
}
func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index ced42fd4f..d1e5a714b 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -150,6 +150,7 @@ type Style struct {
Italic *Scalar `json:"italic,omitempty"`
Underline *Scalar `json:"underline,omitempty"`
Filled *Scalar `json:"filled,omitempty"`
+ DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
}
func (s *Style) Apply(key, value string) error {
@@ -303,6 +304,15 @@ func (s *Style) Apply(key, value string) error {
return errors.New(`expected "filled" to be true or false`)
}
s.Filled.Value = value
+ case "double-border":
+ if s.DoubleBorder == nil {
+ break
+ }
+ _, err := strconv.ParseBool(value)
+ if err != nil {
+ return errors.New(`expected "double-border" to be true or false`)
+ }
+ s.DoubleBorder.Value = value
default:
return fmt.Errorf("unknown style key: %s", key)
}
@@ -1341,8 +1351,9 @@ var StyleKeywords = map[string]struct{}{
"underline": {},
// Only for shapes
- "shadow": {},
- "multiple": {},
+ "shadow": {},
+ "multiple": {},
+ "double-border": {},
// Only for squares
"3d": {},
diff --git a/d2graph/seqdiagram.go b/d2graph/seqdiagram.go
index 08a2f6ce3..b9cddac8d 100644
--- a/d2graph/seqdiagram.go
+++ b/d2graph/seqdiagram.go
@@ -97,3 +97,12 @@ func (e *Edge) ContainedBy(obj *Object) bool {
}
return false
}
+
+func (e *Edge) GetGroup() *Object {
+ for _, ref := range e.References {
+ if ref.ScopeObj.IsSequenceDiagramGroup() {
+ return ref.ScopeObj
+ }
+ }
+ return nil
+}
diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go
index 43859aa35..8ef3e91a2 100644
--- a/d2layouts/d2sequence/sequence_diagram.go
+++ b/d2layouts/d2sequence/sequence_diagram.go
@@ -445,6 +445,8 @@ func (sd *sequenceDiagram) placeSpans() {
// routeMessages routes horizontal edges (messages) from Src to Dst lifeline (actor/span center)
// in another step, routes are adjusted to spans borders when necessary
func (sd *sequenceDiagram) routeMessages() error {
+ var prevIsLoop bool
+ var prevGroup *d2graph.Object
messageOffset := sd.maxActorHeight + sd.yStep
for _, message := range sd.messages {
message.ZIndex = MESSAGE_Z_INDEX
@@ -454,6 +456,14 @@ func (sd *sequenceDiagram) routeMessages() error {
noteOffset += note.Height + sd.yStep
}
}
+
+ // we need extra space if the previous message is a loop in a different group
+ group := message.GetGroup()
+ if prevIsLoop && prevGroup != group {
+ messageOffset += MIN_MESSAGE_DISTANCE
+ }
+ prevGroup = group
+
startY := messageOffset + noteOffset
var startX, endX float64
@@ -490,11 +500,13 @@ func (sd *sequenceDiagram) routeMessages() error {
geo.NewPoint(midX, endY),
geo.NewPoint(endX, endY),
}
+ prevIsLoop = true
} else {
message.Route = []*geo.Point{
geo.NewPoint(startX, startY),
geo.NewPoint(endX, startY),
}
+ prevIsLoop = false
}
messageOffset += sd.yStep
diff --git a/d2oracle/edit.go b/d2oracle/edit.go
index 5d928d86a..1d3103a28 100644
--- a/d2oracle/edit.go
+++ b/d2oracle/edit.go
@@ -284,6 +284,11 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
attrs.Style.Multiple.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
+ case "double-border":
+ if attrs.Style.DoubleBorder != nil {
+ attrs.Style.DoubleBorder.MapKey.SetScalar(mk.Value.ScalarBox())
+ return nil
+ }
case "font":
if attrs.Style.Font != nil {
attrs.Style.Font.MapKey.SetScalar(mk.Value.ScalarBox())
diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go
index 16697c0b1..e95a6fe5a 100644
--- a/d2renderers/d2sketch/sketch.go
+++ b/d2renderers/d2sketch/sketch.go
@@ -91,6 +91,47 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
return output, nil
}
+func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
+ jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
+ fill: "%s",
+ stroke: "%s",
+ strokeWidth: %d,
+ %s
+ });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
+ pathsBigRect, err := computeRoughPathData(r, jsBigRect)
+ if err != nil {
+ return "", err
+ }
+ jsSmallRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
+ fill: "%s",
+ stroke: "%s",
+ strokeWidth: %d,
+ %s
+ });`, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
+ pathsSmallRect, err := computeRoughPathData(r, jsSmallRect)
+ if err != nil {
+ return "", err
+ }
+ output := ""
+ for _, p := range pathsBigRect {
+ output += fmt.Sprintf(
+ ``,
+ shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
+ )
+ }
+ for _, p := range pathsSmallRect {
+ output += fmt.Sprintf(
+ ``,
+ shape.Pos.X+d2target.INNER_BORDER_OFFSET, shape.Pos.Y+d2target.INNER_BORDER_OFFSET, p, shape.CSSStyle(),
+ )
+ }
+ output += fmt.Sprintf(
+ ``,
+ shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
+ )
+ return output, nil
+}
+
func Oval(r *Runner, shape d2target.Shape) (string, error) {
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
fill: "%s",
@@ -116,6 +157,47 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
return output, nil
}
+func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
+ jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
+ fill: "%s",
+ stroke: "%s",
+ strokeWidth: %d,
+ %s
+ });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
+ jsSmallCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
+ fill: "%s",
+ stroke: "%s",
+ strokeWidth: %d,
+ %s
+ });`, shape.Width/2, shape.Height/2, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
+ pathsBigCircle, err := computeRoughPathData(r, jsBigCircle)
+ if err != nil {
+ return "", err
+ }
+ pathsSmallCircle, err := computeRoughPathData(r, jsSmallCircle)
+ if err != nil {
+ return "", err
+ }
+ output := ""
+ for _, p := range pathsBigCircle {
+ output += fmt.Sprintf(
+ ``,
+ shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
+ )
+ }
+ for _, p := range pathsSmallCircle {
+ output += fmt.Sprintf(
+ ``,
+ shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
+ )
+ }
+ output += fmt.Sprintf(
+ ``,
+ shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2, shape.Width/2, shape.Height/2,
+ )
+ return output, nil
+}
+
// TODO need to personalize this per shape like we do in Terrastruct app
func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
output := ""
diff --git a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
index 07980441b..f0a8aabf2 100644
--- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
+++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
@@ -51,7 +51,7 @@ 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
\ 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 2ad6ac348..23a00b942 100644
--- a/e2etests/testdata/measured/empty-shape/dagre/board.exp.json
+++ b/e2etests/testdata/measured/empty-shape/dagre/board.exp.json
@@ -21,6 +21,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
diff --git a/e2etests/testdata/measured/empty-shape/dagre/sketch.exp.svg b/e2etests/testdata/measured/empty-shape/dagre/sketch.exp.svg
index e989e9c0d..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/measured/empty-sql_table/dagre/board.exp.json b/e2etests/testdata/measured/empty-sql_table/dagre/board.exp.json
index 7989809d1..4c42d5b5c 100644
--- a/e2etests/testdata/measured/empty-sql_table/dagre/board.exp.json
+++ b/e2etests/testdata/measured/empty-sql_table/dagre/board.exp.json
@@ -21,6 +21,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
diff --git a/e2etests/testdata/measured/empty-sql_table/dagre/sketch.exp.svg b/e2etests/testdata/measured/empty-sql_table/dagre/sketch.exp.svg
index 69d917f3c..53a789b51 100644
--- a/e2etests/testdata/measured/empty-sql_table/dagre/sketch.exp.svg
+++ b/e2etests/testdata/measured/empty-sql_table/dagre/sketch.exp.svg
@@ -39,7 +39,7 @@ width="254" height="216" viewBox="-102 -102 254 216">
\ 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 415b1eeb0..69078a8cf 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
@@ -21,6 +21,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
@@ -60,6 +61,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
@@ -99,6 +101,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
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 12b278bba..908f054f3 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
@@ -39,7 +39,7 @@ width="680" height="242" viewBox="-102 -102 680 242">
\ 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 754b0c6f6..8c7a45efa 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
@@ -21,6 +21,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": {
@@ -71,6 +72,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": {
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 b66955ecf..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 b5d6460d7..31d13d98b 100644
--- a/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json
+++ b/e2etests/testdata/regression/elk_loop_panic/dagre/board.exp.json
@@ -21,6 +21,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
@@ -61,6 +62,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
@@ -101,6 +103,7 @@
"shadow": false,
"3d": false,
"multiple": false,
+ "double-border": false,
"tooltip": "",
"link": "",
"icon": null,
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 4ca1c4d4d..1c982726d 100644
--- a/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg
+++ b/e2etests/testdata/regression/elk_loop_panic/dagre/sketch.exp.svg
@@ -39,7 +39,7 @@ width="630" height="430" viewBox="-102 -102 630 430">
md
-
ab
+ab
md
-
ab
+abx
y
-
aYou don't have to know how the computer works,just how to work the computer.
+aYou don't have to know how the computer works,just how to work the computer.x
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 rightxyThe top of the mountain
bottom
-
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 rightpoll the peopleresultsunfavorablefavorablewill of the people
title
-
+poll the peopleresultsunfavorablefavorablewill of the people
title
-
+
md
-
ab
+ab
md
-
ab
+ab
md
-
ab
+ab
md
-
ab
+abmixed togethersugarsolutionwe get
+mixed togethersugarsolutionwe getmixed togethersugarsolutionwe get
+mixed togethersugarsolutionwe get