+
+- [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 3504ae8e0..0b3765baf 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -364,12 +364,17 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
if user, err := user.Current(); err == nil {
username = user.Username
}
- description := "Presentation auto-generated by D2 - https://d2lang.com/"
+ description := "Presentation generated with D2 - https://d2lang.com/"
rootName := getFileName(outputPath)
+ // version must be only numbers to avoid issues with PowerPoint
p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers())
+<<<<<<< HEAD
boardIdToIndex := buildBoardIdToIndex(diagram, nil, nil)
err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex)
+=======
+ svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil)
+>>>>>>> gh-821-ppt
if err != nil {
return nil, false, err
}
@@ -379,7 +384,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
}
dur := time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
- return nil, true, nil
+ return svg, true, nil
default:
compileDur := time.Since(start)
if animateInterval <= 0 {
@@ -397,7 +402,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 {
@@ -758,7 +763,11 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
return svg, nil
}
+<<<<<<< HEAD
func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string, boardIdToIndex map[string]int) error {
+=======
+func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) ([]byte, error) {
+>>>>>>> gh-821-ppt
var currBoardPath []string
// Root board doesn't have a name, so we use the output filename
if diagram.Name == "" {
@@ -767,31 +776,33 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
currBoardPath = append(boardPath, diagram.Name)
}
+ var svg []byte
if !diagram.IsFolderOnly {
// gofpdf will print the png img with a slight filter
// make the bg fill within the png transparent so that the pdf bg fill is the only bg color present
diagram.Root.Fill = "transparent"
- svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
+ var err error
+ svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
SetDimensions: true,
})
if err != nil {
- return err
+ return nil, err
}
svg, err = plugin.PostProcess(ctx, svg)
if err != nil {
- return err
+ return nil, err
}
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil {
- return bundleErr
+ return nil, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
@@ -800,12 +811,12 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
pngScale := 2.
pngImg, err := png.ConvertSVG(ms, page, svg)
if err != nil {
- return err
+ return nil, err
}
slide, err := presentation.AddSlide(pngImg, diagram, currBoardPath)
if err != nil {
- return err
+ return nil, err
}
viewboxSlice := appendix.FindViewboxSlice(svg)
@@ -848,26 +859,37 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
}
for _, dl := range diagram.Layers {
+<<<<<<< HEAD
err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
+=======
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
+>>>>>>> gh-821-ppt
if err != nil {
- return err
+ return nil, err
}
}
for _, dl := range diagram.Scenarios {
+<<<<<<< HEAD
err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
+=======
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
+>>>>>>> gh-821-ppt
if err != nil {
- return err
+ return nil, err
}
}
for _, dl := range diagram.Steps {
+<<<<<<< HEAD
err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
+=======
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
+>>>>>>> gh-821-ppt
if err != nil {
- return err
+ return nil, err
}
}
- // TODO: return SVG?
- return nil
+ return svg, nil
}
// newExt must include leading .
@@ -882,9 +904,7 @@ func renameExt(fp string, newExt string) string {
func getFileName(path string) string {
ext := filepath.Ext(path)
- trimmedPath := strings.TrimSuffix(path, ext)
- splitPath := strings.Split(trimmedPath, "/")
- return splitPath[len(splitPath)-1]
+ return strings.TrimSuffix(filepath.Base(path), ext)
}
// TODO: remove after removing slog
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 @@
-