+
+- [Playground link to source code](https://play.d2lang.com/?script=nJRPbvMgEMX3nGIU6Vt-zp82bcoBeg-Mpw4yBhdD0qjK3SvAJMENatWFF8zDjx9vRlwyo7Al4bjKt4FCLRnvCJk6TuGTABz3wiKM744ZjBWAkCqFxSKsgieF9WYVlsEw7QSIzsFlqsQbU-DaqDepj2hq6XLxf_KMlmcSv1pq3iXr2Tl5BhTcMKDhbEzG14HKaSJfw0zHT0zNEGMgV4QzIZ54XYWIaBZOUDZF5aGoPBaVbVF5KirPRWVXVF7uKqRhbYsGULVCTUlPfdmtfGOmn0JXyKwh00ChaJHcJpoFfduTywzkE7BLuTN1gkbzDg1w3Q_MiloiGKes6P8GJ_2DcWH5BWCahXuEAP7pobC3dhjpculXY2XRGDZa47ituO6XDR7-bV7jNarx0KaLcRHRfoZutWwy3p4Zrb_T3pTvB3oUyr9qVXakFMp95KWecT3b1bkajUKLs_pXAAAA__8%3D&sketch=1&)
+
+Bunch of other features, improvements, and bug fixes too. Make sure to check out the updated docs for how to use these new features!
+
+#### Features 🚀
+
+- Classes are implemented. See [docs](https://d2lang.com/tour/classes). [#772](https://github.com/terrastruct/d2/pull/772)
+- Grid diagrams are implemented. See [docs](https://d2lang.com/tour/grid-diagrams). [#1122](https://github.com/terrastruct/d2/pull/1122)
+- Container with constant key near attribute now can have descendant objects and connections (thank you @donglixiaoche) [#1071](https://github.com/terrastruct/d2/pull/1071)
+- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
+
+#### Improvements 🧹
+
+- Labels on parallel `dagre` connections include a gap between them [#1134](https://github.com/terrastruct/d2/pull/1134)
+- Sequence diagram lifelines inherit the actor's `stroke` and `stroke-dash` [#1140](https://github.com/terrastruct/d2/pull/1140)
+- Add `text-transform` styling option (thank you @alexstoick for these 2) [#1118](https://github.com/terrastruct/d2/pull/1118)
+
+#### Bugfixes ⛑️
+
+- Fix inheritence in scenarios/steps [#1090](https://github.com/terrastruct/d2/pull/1090)
+- Fix a bug in 32bit builds [#1115](https://github.com/terrastruct/d2/issues/1115)
+- Fix a bug in ID parsing [#322](https://github.com/terrastruct/d2/issues/322)
+- Fix a bug in watch mode parsing SVG [#1119](https://github.com/terrastruct/d2/issues/1119)
+- Namespace transitions so that multiple animated D2 diagrams can exist on the same page [#1123](https://github.com/terrastruct/d2/issues/1123)
+- Fix a bug in vertical alignment of appendix lines [#1104](https://github.com/terrastruct/d2/issues/1104)
+- Fix precision difference for sketch mode running on different architectures [#921](https://github.com/terrastruct/d2/issues/921)
+- Fix an issue where markdown fonts weren't being applied correctly [#1132](https://github.com/terrastruct/d2/issues/1132)
+
+#### Breaking changes
+
+- `class` and `classes` are now reserved keywords.
+- `text-transform` is now a reserved keyword.
diff --git a/d2cli/main.go b/d2cli/main.go
index 1504d402b..9b7339f8a 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -395,7 +395,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
}
var out []byte
if len(boards) > 0 {
- out := boards[0]
+ out = boards[0]
if animateInterval > 0 {
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
if err != nil {
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 99c0de355..817f4169f 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -500,6 +500,8 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "double-border":
attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
+ case "text-transform":
+ attrs.Style.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
}
}
diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go
index bb26d78f8..c484dd4e0 100644
--- a/d2compiler/compile_test.go
+++ b/d2compiler/compile_test.go
@@ -2257,6 +2257,17 @@ layers: {
}`,
expErr: `d2/testdata/d2compiler/TestCompile/link-board-underscore-not-found.d2:7:9: invalid underscore usage`,
},
+ {
+ name: "text-transform",
+ text: `direction: right
+x -> y: hi {
+ style: {
+ text-transform: capitalize
+ }
+}
+x.style.text-transform: uppercase
+y.style.text-transform: lowercase`,
+ },
{
name: "near_near_const",
text: `
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index e6e9de6b4..8b5e27696 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -10,6 +10,9 @@ import (
"strconv"
"strings"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
@@ -141,6 +144,25 @@ type Attributes struct {
Classes []string `json:"classes,omitempty"`
}
+// ApplyTextTransform will alter the `Label.Value` of the current object based
+// on the specification of the `text-transform` styling option. This function
+// has side-effects!
+func (a *Attributes) ApplyTextTransform() {
+ if a.Style.NoneTextTransform() {
+ return
+ }
+
+ if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "uppercase" {
+ a.Label.Value = strings.ToUpper(a.Label.Value)
+ }
+ if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "lowercase" {
+ a.Label.Value = strings.ToLower(a.Label.Value)
+ }
+ if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "capitalize" {
+ a.Label.Value = cases.Title(language.Und).String(a.Label.Value)
+ }
+}
+
// TODO references at the root scope should have their Scope set to root graph AST
type Reference struct {
Key *d2ast.KeyPath `json:"key"`
@@ -161,25 +183,33 @@ func (r Reference) InEdge() bool {
}
type Style struct {
- Opacity *Scalar `json:"opacity,omitempty"`
- Stroke *Scalar `json:"stroke,omitempty"`
- Fill *Scalar `json:"fill,omitempty"`
- FillPattern *Scalar `json:"fillPattern,omitempty"`
- StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
- StrokeDash *Scalar `json:"strokeDash,omitempty"`
- BorderRadius *Scalar `json:"borderRadius,omitempty"`
- Shadow *Scalar `json:"shadow,omitempty"`
- ThreeDee *Scalar `json:"3d,omitempty"`
- Multiple *Scalar `json:"multiple,omitempty"`
- Font *Scalar `json:"font,omitempty"`
- FontSize *Scalar `json:"fontSize,omitempty"`
- FontColor *Scalar `json:"fontColor,omitempty"`
- Animated *Scalar `json:"animated,omitempty"`
- Bold *Scalar `json:"bold,omitempty"`
- Italic *Scalar `json:"italic,omitempty"`
- Underline *Scalar `json:"underline,omitempty"`
- Filled *Scalar `json:"filled,omitempty"`
- DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
+ Opacity *Scalar `json:"opacity,omitempty"`
+ Stroke *Scalar `json:"stroke,omitempty"`
+ Fill *Scalar `json:"fill,omitempty"`
+ FillPattern *Scalar `json:"fillPattern,omitempty"`
+ StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
+ StrokeDash *Scalar `json:"strokeDash,omitempty"`
+ BorderRadius *Scalar `json:"borderRadius,omitempty"`
+ Shadow *Scalar `json:"shadow,omitempty"`
+ ThreeDee *Scalar `json:"3d,omitempty"`
+ Multiple *Scalar `json:"multiple,omitempty"`
+ Font *Scalar `json:"font,omitempty"`
+ FontSize *Scalar `json:"fontSize,omitempty"`
+ FontColor *Scalar `json:"fontColor,omitempty"`
+ Animated *Scalar `json:"animated,omitempty"`
+ Bold *Scalar `json:"bold,omitempty"`
+ Italic *Scalar `json:"italic,omitempty"`
+ Underline *Scalar `json:"underline,omitempty"`
+ Filled *Scalar `json:"filled,omitempty"`
+ DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
+ TextTransform *Scalar `json:"textTransform,omitempty"`
+}
+
+// NoneTextTransform will return a boolean if the text should not have any
+// transformation applied. This should overwrite theme specific transformations
+// like `CapsLock` from the `terminal` theme.
+func (s Style) NoneTextTransform() bool {
+ return s.TextTransform != nil && s.TextTransform.Value == "none"
}
func (s *Style) Apply(key, value string) error {
@@ -350,6 +380,14 @@ func (s *Style) Apply(key, value string) error {
return errors.New(`expected "double-border" to be true or false`)
}
s.DoubleBorder.Value = value
+ case "text-transform":
+ if s.TextTransform == nil {
+ break
+ }
+ if !go2.Contains(textTransforms, strings.ToLower(value)) {
+ return fmt.Errorf(`expected "text-transform" to be one of (%s)`, strings.Join(textTransforms, ", "))
+ }
+ s.TextTransform.Value = value
default:
return fmt.Errorf("unknown style key: %s", key)
}
@@ -1329,10 +1367,11 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) {
- if obj.Attributes.Language != "latex" {
+ if obj.Attributes.Language != "latex" && !obj.Attributes.Style.NoneTextTransform() {
obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.Value)
}
}
+ obj.Attributes.ApplyTextTransform()
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil {
@@ -1451,9 +1490,11 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
- if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
+ if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !edge.Attributes.Style.NoneTextTransform() {
edge.Attributes.Label.Value = strings.ToUpper(edge.Attributes.Label.Value)
}
+ edge.Attributes.ApplyTextTransform()
+
usedFont := fontFamily
if edge.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[edge.Attributes.Style.Font.Value]
@@ -1479,9 +1520,10 @@ func (g *Graph) Texts() []*d2target.MText {
for _, obj := range g.Objects {
if obj.Attributes.Label.Value != "" {
+ obj.Attributes.ApplyTextTransform()
text := obj.Text()
if capsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) {
- if obj.Attributes.Language != "latex" {
+ if obj.Attributes.Language != "latex" && !obj.Attributes.Style.NoneTextTransform() {
text.Text = strings.ToUpper(text.Text)
}
}
@@ -1512,8 +1554,9 @@ func (g *Graph) Texts() []*d2target.MText {
}
for _, edge := range g.Edges {
if edge.Attributes.Label.Value != "" {
+ edge.Attributes.ApplyTextTransform()
text := edge.Text()
- if capsLock {
+ if capsLock && !edge.Attributes.Style.NoneTextTransform() {
text.Text = strings.ToUpper(text.Text)
}
texts = appendTextDedup(texts, text)
@@ -1582,12 +1625,13 @@ var StyleKeywords = map[string]struct{}{
"border-radius": {},
// Only for text
- "font": {},
- "font-size": {},
- "font-color": {},
- "bold": {},
- "italic": {},
- "underline": {},
+ "font": {},
+ "font-size": {},
+ "font-color": {},
+ "bold": {},
+ "italic": {},
+ "underline": {},
+ "text-transform": {},
// Only for shapes
"shadow": {},
@@ -1625,6 +1669,8 @@ var FillPatterns = []string{
"paper",
}
+var textTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
+
// BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{
"layers": {},
diff --git a/d2layouts/d2near/layout.go b/d2layouts/d2near/layout.go
index 07cfef4d1..bb2d3b786 100644
--- a/d2layouts/d2near/layout.go
+++ b/d2layouts/d2near/layout.go
@@ -59,6 +59,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "-center") {
// The z-index for constant nears does not matter, as it will not collide
g.Objects = append(g.Objects, tempGraph.Objects...)
+ if obj.Parent.Children == nil {
+ obj.Parent.Children = make(map[string]*d2graph.Object)
+ }
obj.Parent.Children[strings.ToLower(obj.ID)] = obj
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
g.Edges = append(g.Edges, tempGraph.Edges...)
diff --git a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
index 6b58edd00..2b7ee4fd4 100644
--- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
+++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
@@ -1,4 +1,4 @@
-