merge with master

This commit is contained in:
Antoine Poivey 2023-04-20 16:48:52 +02:00
commit 760dbfe5ae
No known key found for this signature in database
GPG key ID: 6AA1C83421F1A287
1225 changed files with 141251 additions and 51340 deletions

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version-file: ./go.mod go-version-file: ./go.mod
cache: true cache: true

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version-file: ./go.mod go-version-file: ./go.mod
cache: true cache: true

View file

@ -9,7 +9,7 @@ jobs:
d2-project: d2-project:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/add-to-project@v0.4.0 - uses: actions/add-to-project@v0.5.0
with: with:
project-url: https://github.com/orgs/terrastruct/projects/34 project-url: https://github.com/orgs/terrastruct/projects/34
github-token: ${{ secrets._GITHUB_TOKEN }} github-token: ${{ secrets._GITHUB_TOKEN }}

View file

@ -209,7 +209,7 @@ let us know and we'll be happy to include it here!
### Community plugins ### Community plugins
- **Tree-sitter grammar**: [https://github.com/pleshevskiy/tree-sitter-d2](https://github.com/pleshevskiy/tree-sitter-d2) - **Tree-sitter grammar**: [https://git.pleshevski.ru/pleshevskiy/tree-sitter-d2](https://git.pleshevski.ru/pleshevskiy/tree-sitter-d2)
- **Emacs major mode**: [https://github.com/andorsk/d2-mode](https://github.com/andorsk/d2-mode) - **Emacs major mode**: [https://github.com/andorsk/d2-mode](https://github.com/andorsk/d2-mode)
- **Goldmark extension**: [https://github.com/FurqanSoftware/goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2) - **Goldmark extension**: [https://github.com/FurqanSoftware/goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2)
- **Telegram bot**: [https://github.com/meinside/telegram-d2-bot](https://github.com/meinside/telegram-d2-bot) - **Telegram bot**: [https://github.com/meinside/telegram-d2-bot](https://github.com/meinside/telegram-d2-bot)
@ -226,6 +226,7 @@ let us know and we'll be happy to include it here!
- **CIL (C#, Visual Basic, F#, C++ CLR) to D2**: [https://github.com/HugoVG/AppDiagram](https://github.com/HugoVG/AppDiagram) - **CIL (C#, Visual Basic, F#, C++ CLR) to D2**: [https://github.com/HugoVG/AppDiagram](https://github.com/HugoVG/AppDiagram)
- **D2 Snippets (for text editors)**: [https://github.com/Paracelsus-Rose/D2-Language-Code-Snippets](https://github.com/Paracelsus-Rose/D2-Language-Code-Snippets) - **D2 Snippets (for text editors)**: [https://github.com/Paracelsus-Rose/D2-Language-Code-Snippets](https://github.com/Paracelsus-Rose/D2-Language-Code-Snippets)
- **Mongo to D2**: [https://github.com/novuhq/mongo-to-D2](https://github.com/novuhq/mongo-to-D2) - **Mongo to D2**: [https://github.com/novuhq/mongo-to-D2](https://github.com/novuhq/mongo-to-D2)
- **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter)
### Misc ### Misc

View file

@ -1,16 +1,10 @@
#### Features 🚀 #### Features 🚀
- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
#### Improvements 🧹 #### Improvements 🧹
- Labels on parallel `dagre` connections include a gap between them [#1134](https://github.com/terrastruct/d2/pull/1134) - ELK self loops get distributed around the object instead of stacking [#1232](https://github.com/terrastruct/d2/pull/1232)
#### Bugfixes ⛑️ #### Bugfixes ⛑️
- Fix a bug in 32bit builds [#1115](https://github.com/terrastruct/d2/issues/1115) - Fixes an issue with markdown labels that are empty when rendered [#1223](https://github.com/terrastruct/d2/issues/1223)
- Fix a bug in ID parsing [#322](https://github.com/terrastruct/d2/issues/322) - ELK self loops always have enough space for long labels [#1232](https://github.com/terrastruct/d2/pull/1232)
- 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)

View file

@ -20,7 +20,7 @@ PRs when it's a visual change.
- Sequence diagrams are now supported, experimentally. See [docs](https://d2lang.com/tour/sequence-diagrams). [#99](https://github.com/terrastruct/d2/issues/99) - Sequence diagrams are now supported, experimentally. See [docs](https://d2lang.com/tour/sequence-diagrams). [#99](https://github.com/terrastruct/d2/issues/99)
- Formatting of d2 scripts is supported on the CLI with the `fmt` subcommand. See `man d2` or `d2 --help`. [#292](https://github.com/terrastruct/d2/pull/292) - Formatting of d2 scripts is supported on the CLI with the `fmt` subcommand. See `man d2` or `d2 --help`. [#292](https://github.com/terrastruct/d2/pull/292)
- Latex is now supported. See [docs](https://d2lang.com/tour/text) for how to use. [#229](https://github.com/terrastruct/d2/pull/229) - LaTeX is now supported. See [docs](https://d2lang.com/tour/text) for how to use. [#229](https://github.com/terrastruct/d2/pull/229)
- `direction` keyword is now supported to specify `up`, `down`, `right`, `left` layouts. See [docs](https://d2lang.com/tour/layouts) for more. - `direction` keyword is now supported to specify `up`, `down`, `right`, `left` layouts. See [docs](https://d2lang.com/tour/layouts) for more.
[#251](https://github.com/terrastruct/d2/pull/251) [#251](https://github.com/terrastruct/d2/pull/251)
- Self-referencing connections are now valid. E.g. `x -> x`. Render will vary based on layout engine. [#273](https://github.com/terrastruct/d2/pull/273) - Self-referencing connections are now valid. E.g. `x -> x`. Render will vary based on layout engine. [#273](https://github.com/terrastruct/d2/pull/273)

View file

@ -9,7 +9,7 @@
#### Bugfixes ⛑️ #### Bugfixes ⛑️
- Appendix seperator line no longer added to PNG export when appendix doesn't exist. [#582](https://github.com/terrastruct/d2/pull/582) - Appendix separator line no longer added to PNG export when appendix doesn't exist. [#582](https://github.com/terrastruct/d2/pull/582)
- Watch mode only fits to screen on initial load. [#601](https://github.com/terrastruct/d2/pull/601) - Watch mode only fits to screen on initial load. [#601](https://github.com/terrastruct/d2/pull/601)
- Dimensions (`width`/`height`) were incorrectly giving compiler errors when applied on a shape with style. [#614](https://github.com/terrastruct/d2/pull/614) - Dimensions (`width`/`height`) were incorrectly giving compiler errors when applied on a shape with style. [#614](https://github.com/terrastruct/d2/pull/614)
- `near` would collide with labels if they were on the diagram boundaries in the same position. [#617](https://github.com/terrastruct/d2/pull/617) - `near` would collide with labels if they were on the diagram boundaries in the same position. [#617](https://github.com/terrastruct/d2/pull/617)

View file

@ -31,4 +31,4 @@ D2 0.3 is here!
- Prevents an object's `near` from targeting another object with `near` set to a constant [#1100](https://github.com/terrastruct/d2/pull/1100) - Prevents an object's `near` from targeting another object with `near` set to a constant [#1100](https://github.com/terrastruct/d2/pull/1100)
- Fixes inaccurate bold edge label padding [#1108](https://github.com/terrastruct/d2/pull/1108) - Fixes inaccurate bold edge label padding [#1108](https://github.com/terrastruct/d2/pull/1108)
- Prevents Latex blocks from being uppercased in special themes [#1111](https://github.com/terrastruct/d2/pull/1111) - Prevents LaTeX blocks from being uppercased in special themes [#1111](https://github.com/terrastruct/d2/pull/1111)

View file

@ -0,0 +1,41 @@
Major updates in 0.4.0:
- `classes` and `class` are finally here. No more repeating styles!
- Introducing Grid diagrams. A very popular form of diagramming software architecture.
To showcase both of these, here's a demo with a link to the source code below:
<img width="671" alt="Screen Shot 2023-04-08 at 7 20 04 PM" src="https://user-images.githubusercontent.com/3120367/230750853-5925f8a1-98bc-4f51-b6f7-26ada4534a58.png">
- [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.

View file

@ -0,0 +1,35 @@
Multi-board D2 compositions now get 2 more useful formats to export to: PowerPoint and GIFs.
## Powerpoint example
Make sure you click present, and click around the links and navigations!
- Download: [wcc.pptx](https://github.com/terrastruct/d2/files/11256733/wcc.pptx)
- Google Slides: [https://docs.google.com/presentation/d/18rRh4izu3k_43On8PXtVYdqRxmoQJd4y/view](https://docs.google.com/presentation/d/18rRh4izu3k_43On8PXtVYdqRxmoQJd4y/view)
- Source code: [https://github.com/terrastruct/d2/blob/master/docs/examples/wcc/wcc.d2](https://github.com/terrastruct/d2/blob/master/docs/examples/wcc/wcc.d2)
## GIF example
This is the same diagram as one we shared when we announced animated SVGs, but in GIF form.
![animated](https://user-images.githubusercontent.com/3120367/232637553-dd35e076-dfb4-4910-958d-d57ec382f792.gif)
#### Features 🚀
- Export diagrams to `.pptx` (PowerPoint) [#1139](https://github.com/terrastruct/d2/pull/1139)
- Export diagrams to `.gif` [#1200](https://github.com/terrastruct/d2/pull/1200)
- Customize gap size in grid diagrams with `grid-gap`, `vertical-gap`, or `horizontal-gap` (see [docs](https://d2lang.com/tour/grid-diagrams/#gap-size)) [#1178](https://github.com/terrastruct/d2/issues/1178)
- New dark theme "Dark Terrastruct Flagship" with the theme ID of `201` [#1150](https://github.com/terrastruct/d2/issues/1150)
#### Improvements 🧹
- `font-size` works with Markdown text [#1191](https://github.com/terrastruct/d2/issues/1191)
- SVG outputs for internal links use relative paths instead of absolute [#1197](https://github.com/terrastruct/d2/pull/1197)
- Improves arrowhead label positioning [#1207](https://github.com/terrastruct/d2/pull/1207)
#### Bugfixes ⛑️
- Fixes grid layouts not applying on objects with a constant near [#1173](https://github.com/terrastruct/d2/issues/1173)
- Fixes Markdown header rendering in Firefox and Safari [#1177](https://github.com/terrastruct/d2/issues/1177)
- Fixes a grid layout panic when there are fewer objects than rows/columns [#1189](https://github.com/terrastruct/d2/issues/1189)
- Fixes an issue where PNG exports that were too large were crashing [#1093](https://github.com/terrastruct/d2/issues/1093)

42
d2cli/export.go Normal file
View file

@ -0,0 +1,42 @@
package d2cli
import (
"path/filepath"
)
type exportExtension string
const GIF exportExtension = ".gif"
const PNG exportExtension = ".png"
const PPTX exportExtension = ".pptx"
const PDF exportExtension = ".pdf"
const SVG exportExtension = ".svg"
var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}
func getExportExtension(outputPath string) exportExtension {
ext := filepath.Ext(outputPath)
for _, kext := range SUPPORTED_EXTENSIONS {
if kext == exportExtension(ext) {
return exportExtension(ext)
}
}
// default is svg
return exportExtension(SVG)
}
func (ex exportExtension) supportsAnimation() bool {
return ex == SVG || ex == GIF
}
func (ex exportExtension) requiresAnimationInterval() bool {
return ex == GIF
}
func (ex exportExtension) requiresPNGRenderer() bool {
return ex == PNG || ex == PDF || ex == PPTX || ex == GIF
}
func (ex exportExtension) supportsDarkTheme() bool {
return ex == SVG
}

88
d2cli/export_test.go Normal file
View file

@ -0,0 +1,88 @@
package d2cli
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOutputFormat(t *testing.T) {
type testCase struct {
outputPath string
extension exportExtension
supportsDarkTheme bool
supportsAnimation bool
requiresAnimationInterval bool
requiresPngRender bool
}
testCases := []testCase{
{
outputPath: "/out.svg",
extension: SVG,
supportsDarkTheme: true,
supportsAnimation: true,
requiresAnimationInterval: false,
requiresPngRender: false,
},
{
// assumes SVG by default
outputPath: "/out",
extension: SVG,
supportsDarkTheme: true,
supportsAnimation: true,
requiresAnimationInterval: false,
requiresPngRender: false,
},
{
outputPath: "-",
extension: SVG,
supportsDarkTheme: true,
supportsAnimation: true,
requiresAnimationInterval: false,
requiresPngRender: false,
},
{
outputPath: "/out.png",
extension: PNG,
supportsDarkTheme: false,
supportsAnimation: false,
requiresAnimationInterval: false,
requiresPngRender: true,
},
{
outputPath: "/out.pptx",
extension: PPTX,
supportsDarkTheme: false,
supportsAnimation: false,
requiresAnimationInterval: false,
requiresPngRender: true,
},
{
outputPath: "/out.pdf",
extension: PDF,
supportsDarkTheme: false,
supportsAnimation: false,
requiresAnimationInterval: false,
requiresPngRender: true,
},
{
outputPath: "/out.gif",
extension: GIF,
supportsDarkTheme: false,
supportsAnimation: true,
requiresAnimationInterval: true,
requiresPngRender: true,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.outputPath, func(t *testing.T) {
extension := getExportExtension(tc.outputPath)
assert.Equal(t, tc.extension, extension)
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
assert.Equal(t, tc.requiresPngRender, extension.requiresPNGRenderer())
})
}
}

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -20,6 +21,7 @@ import (
"oss.terrastruct.com/util-go/xmain" "oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2plugin" "oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2animate" "oss.terrastruct.com/d2/d2renderers/d2animate"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
@ -32,10 +34,11 @@ import (
"oss.terrastruct.com/d2/lib/imgbundler" "oss.terrastruct.com/d2/lib/imgbundler"
ctxlog "oss.terrastruct.com/d2/lib/log" ctxlog "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/pdf" "oss.terrastruct.com/d2/lib/pdf"
pdflib "oss.terrastruct.com/d2/lib/pdf"
"oss.terrastruct.com/d2/lib/png" "oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/pptx"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version" "oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xgif"
"cdr.dev/slog" "cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/sloghuman"
@ -98,6 +101,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
fontRegularFlag := ms.Opts.String("D2_FONT_REGULAR", "font-regular", "", "", "path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.") fontRegularFlag := ms.Opts.String("D2_FONT_REGULAR", "font-regular", "", "", "path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.")
fontItalicFlag := ms.Opts.String("D2_FONT_ITALIC", "font-italic", "", "", "path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used.") fontItalicFlag := ms.Opts.String("D2_FONT_ITALIC", "font-italic", "", "", "path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used.")
fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.") fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.")
fontSemiboldFlag := ms.Opts.String("D2_FONT_SEMIBOLD", "font-semibold", "", "", "path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.")
ps, err := d2plugin.ListPlugins(ctx) ps, err := d2plugin.ListPlugins(ctx)
if err != nil { if err != nil {
@ -118,7 +122,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
return nil return nil
} }
fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag) fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag, *fontSemiboldFlag)
if err != nil { if err != nil {
return xmain.UsageErrorf("failed to load specified fonts: %v", err) return xmain.UsageErrorf("failed to load specified fonts: %v", err)
} }
@ -183,13 +187,16 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
inputPath = filepath.Join(inputPath, "index.d2") inputPath = filepath.Join(inputPath, "index.d2")
} }
} }
if filepath.Ext(outputPath) == ".ppt" {
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
}
outputFormat := getExportExtension(outputPath)
if outputPath != "-" { if outputPath != "-" {
outputPath = ms.AbsPath(outputPath) outputPath = ms.AbsPath(outputPath)
if *animateIntervalFlag > 0 { if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG or GIF.\nYou provided: %s", filepath.Ext(outputPath))
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" { } else if *animateIntervalFlag <= 0 && outputFormat.requiresAnimationInterval() {
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath)) return xmain.UsageErrorf("-animate-interval must be greater than 0 for %s outputs.\nYou provided: %d", outputFormat, *animateIntervalFlag)
}
} }
} }
@ -233,12 +240,14 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
} }
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation) ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation)
var pw png.Playwright if !outputFormat.supportsDarkTheme() {
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
if darkThemeFlag != nil { if darkThemeFlag != nil {
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg") ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
darkThemeFlag = nil darkThemeFlag = nil
} }
}
var pw png.Playwright
if outputFormat.requiresPNGRenderer() {
pw, err = png.InitPlaywright() pw, err = png.InitPlaywright()
if err != nil { if err != nil {
return err return err
@ -347,16 +356,66 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
return nil, false, err return nil, false, err
} }
if filepath.Ext(outputPath) == ".pdf" { ext := getExportExtension(outputPath)
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil) switch ext {
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap) case GIF:
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, diagram)
if err != nil {
return nil, false, err
}
out, err := xgif.AnimatePNGs(ms, pngs, int(animateInterval))
if err != nil {
return nil, false, err
}
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return nil, false, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return nil, false, err
}
dur := time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
return svg, true, nil
case PDF:
pageMap := buildBoardIDToIndex(diagram, nil, nil)
path := []pdf.BoardTitle{
{Name: "root", BoardID: "root"},
}
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, path, pageMap)
if err != nil { if err != nil {
return pdf, false, err return pdf, false, err
} }
dur := time.Since(start) dur := time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur) ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
return pdf, true, nil return pdf, true, nil
} else { case PPTX:
var username string
if user, err := user.Current(); err == nil {
username = user.Username
}
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())
boardIdToIndex := buildBoardIDToIndex(diagram, nil, nil)
path := []pptx.BoardTitle{
{Name: "root", BoardID: "root", LinkToSlide: boardIdToIndex["root"] + 1},
}
svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, path, boardIdToIndex)
if err != nil {
return nil, false, err
}
err = p.SaveTo(outputPath)
if err != nil {
return nil, false, err
}
dur := time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
return svg, true, nil
default:
compileDur := time.Since(start) compileDur := time.Since(start)
if animateInterval <= 0 { if animateInterval <= 0 {
// Rename all the "root.layers.x" to the paths that the boards get output to // Rename all the "root.layers.x" to the paths that the boards get output to
@ -364,14 +423,19 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
relink(diagram, linkToOutput) err = relink("root", diagram, linkToOutput)
if err != nil {
return nil, false, err
}
} }
boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram) boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
out := boards[0] var out []byte
if len(boards) > 0 {
out = boards[0]
if animateInterval > 0 { if animateInterval > 0 {
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval)) out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
if err != nil { if err != nil {
@ -387,6 +451,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
} }
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start)) ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
} }
}
return out, true, nil return out, true, nil
} }
} }
@ -462,26 +527,40 @@ func resolveLinks(currDiagramPath, outputPath string, diagram *d2target.Diagram)
return linkToOutput, nil return linkToOutput, nil
} }
func relink(d *d2target.Diagram, linkToOutput map[string]string) { func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string]string) error {
for i, shape := range d.Shapes { for i, shape := range d.Shapes {
if shape.Link != "" { if shape.Link != "" {
for k, v := range linkToOutput { for k, v := range linkToOutput {
if shape.Link == k { if shape.Link == k {
d.Shapes[i].Link = v rel, err := filepath.Rel(filepath.Dir(linkToOutput[currDiagramPath]), v)
if err != nil {
return err
}
d.Shapes[i].Link = rel
break break
} }
} }
} }
} }
for _, board := range d.Layers { for _, board := range d.Layers {
relink(board, linkToOutput) err := relink(strings.Join([]string{currDiagramPath, "layers", board.Name}, "."), board, linkToOutput)
if err != nil {
return err
}
} }
for _, board := range d.Scenarios { for _, board := range d.Scenarios {
relink(board, linkToOutput) err := relink(strings.Join([]string{currDiagramPath, "scenarios", board.Name}, "."), board, linkToOutput)
if err != nil {
return err
}
} }
for _, board := range d.Steps { for _, board := range d.Steps {
relink(board, linkToOutput) err := relink(strings.Join([]string{currDiagramPath, "steps", board.Name}, "."), board, linkToOutput)
if err != nil {
return err
} }
}
return nil
} }
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) { func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
@ -569,7 +648,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
} }
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) { func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := filepath.Ext(outputPath) == ".png" toPNG := getExportExtension(outputPath) == PNG
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{ svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: opts.Pad, Pad: opts.Pad,
Sketch: opts.Sketch, Sketch: opts.Sketch,
@ -638,25 +717,13 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
return svg, nil return svg, nil
} }
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) { func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int) (svg []byte, err error) {
var isRoot bool var isRoot bool
if pdf == nil { if doc == nil {
pdf = pdflib.Init() doc = pdf.Init()
isRoot = true isRoot = true
} }
var currBoardPath []string
// Root board doesn't have a name, so we use the output filename
if diagram.Name == "" {
ext := filepath.Ext(outputPath)
trimmedPath := strings.TrimSuffix(outputPath, ext)
splitPath := strings.Split(trimmedPath, "/")
rootName := splitPath[len(splitPath)-1]
currBoardPath = append(boardPath, rootName)
} else {
currBoardPath = append(boardPath, diagram.Name)
}
if !diagram.IsFolderOnly { if !diagram.IsFolderOnly {
rootFill := diagram.Root.Fill rootFill := diagram.Root.Fill
// gofpdf will print the png img with a slight filter // gofpdf will print the png img with a slight filter
@ -700,33 +767,166 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
if err != nil { if err != nil {
return svg, err return svg, err
} }
err = pdf.AddPDFPage(pngImg, currBoardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap) err = doc.AddPDFPage(pngImg, boardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap)
if err != nil { if err != nil {
return svg, err return svg, err
} }
} }
for _, dl := range diagram.Layers { for _, dl := range diagram.Layers {
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap) path := append(boardPath, pdf.BoardTitle{
Name: dl.Name,
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, "."),
})
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
for _, dl := range diagram.Scenarios { for _, dl := range diagram.Scenarios {
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap) path := append(boardPath, pdf.BoardTitle{
Name: dl.Name,
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, "."),
})
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
for _, dl := range diagram.Steps { for _, dl := range diagram.Steps {
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap) path := append(boardPath, pdf.BoardTitle{
Name: dl.Name,
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, "."),
})
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if isRoot { if isRoot {
err := pdf.Export(outputPath) err := doc.Export(outputPath)
if err != nil {
return nil, err
}
}
return svg, nil
}
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 []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
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"
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 nil, err
}
svg, err = plugin.PostProcess(ctx, svg)
if err != nil {
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 nil, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg)
if err != nil {
return nil, err
}
slide, err := presentation.AddSlide(pngImg, boardPath)
if err != nil {
return nil, err
}
viewboxSlice := appendix.FindViewboxSlice(svg)
viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64)
if err != nil {
return nil, err
}
viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64)
if err != nil {
return nil, err
}
// Draw links
for _, shape := range diagram.Shapes {
if shape.Link == "" {
continue
}
linkX := png.SCALE * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth))
linkY := png.SCALE * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth))
linkWidth := png.SCALE * (float64(shape.Width) + float64(shape.StrokeWidth*2))
linkHeight := png.SCALE * (float64(shape.Height) + float64(shape.StrokeWidth*2))
link := &pptx.Link{
Left: int(linkX),
Top: int(linkY),
Width: int(linkWidth),
Height: int(linkHeight),
Tooltip: shape.Link,
}
slide.AddLink(link)
key, err := d2parser.ParseKey(shape.Link)
if err != nil || key.Path[0].Unbox().ScalarString() != "root" {
// External link
link.ExternalUrl = shape.Link
} else if pageNum, ok := boardIDToIndex[shape.Link]; ok {
// Internal link
link.SlideIndex = pageNum + 1
}
}
}
for _, dl := range diagram.Layers {
boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, ".")
path := append(boardPath, pptx.BoardTitle{
Name: dl.Name,
BoardID: boardID,
LinkToSlide: boardIDToIndex[boardID] + 1,
})
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Scenarios {
boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, ".")
path := append(boardPath, pptx.BoardTitle{
Name: dl.Name,
BoardID: boardID,
LinkToSlide: boardIDToIndex[boardID] + 1,
})
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Steps {
boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, ".")
path := append(boardPath, pptx.BoardTitle{
Name: dl.Name,
BoardID: boardID,
LinkToSlide: boardIDToIndex[boardID] + 1,
})
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -745,6 +945,11 @@ func renameExt(fp string, newExt string) string {
} }
} }
func getFileName(path string) string {
ext := filepath.Ext(path)
return strings.TrimSuffix(filepath.Base(path), ext)
}
// TODO: remove after removing slog // TODO: remove after removing slog
func DiscardSlog(ctx context.Context) context.Context { func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard))) return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
@ -785,14 +990,15 @@ func loadFont(ms *xmain.State, path string) ([]byte, error) {
return ttf, nil return ttf, nil
} }
func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string) (*d2fonts.FontFamily, error) { func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToSemibold string) (*d2fonts.FontFamily, error) {
if pathToRegular == "" && pathToItalic == "" && pathToBold == "" { if pathToRegular == "" && pathToItalic == "" && pathToBold == "" && pathToSemibold == "" {
return nil, nil return nil, nil
} }
var regularTTF []byte var regularTTF []byte
var italicTTF []byte var italicTTF []byte
var boldTTF []byte var boldTTF []byte
var semiboldTTF []byte
var err error var err error
if pathToRegular != "" { if pathToRegular != "" {
@ -813,6 +1019,99 @@ func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string)
return nil, err return nil, err
} }
} }
if pathToSemibold != "" {
semiboldTTF, err = loadFont(ms, pathToSemibold)
if err != nil {
return nil, err
}
}
return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF) return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF, semiboldTTF)
}
const LAYERS = "layers"
const STEPS = "steps"
const SCENARIOS = "scenarios"
// buildBoardIDToIndex returns a map from board path to page int
// To map correctly, it must follow the same traversal of pdf/pptx building
func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int {
newPath := append(path, diagram.Name)
if dictionary == nil {
dictionary = map[string]int{}
newPath[0] = "root"
}
key := strings.Join(newPath, ".")
dictionary[key] = len(dictionary)
for _, dl := range diagram.Layers {
buildBoardIDToIndex(dl, dictionary, append(newPath, LAYERS))
}
for _, dl := range diagram.Scenarios {
buildBoardIDToIndex(dl, dictionary, append(newPath, SCENARIOS))
}
for _, dl := range diagram.Steps {
buildBoardIDToIndex(dl, dictionary, append(newPath, STEPS))
}
return dictionary
}
func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
if !diagram.IsFolderOnly {
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
SetDimensions: true,
})
if err != nil {
return nil, nil, err
}
svg, err = plugin.PostProcess(ctx, svg)
if err != nil {
return nil, 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 nil, nil, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg)
if err != nil {
return nil, nil, err
}
pngs = append(pngs, pngImg)
}
for _, dl := range diagram.Layers {
_, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
if err != nil {
return nil, nil, err
}
pngs = append(pngs, layerPNGs...)
}
for _, dl := range diagram.Scenarios {
_, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
if err != nil {
return nil, nil, err
}
pngs = append(pngs, scenarioPNGs...)
}
for _, dl := range diagram.Steps {
_, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
if err != nil {
return nil, nil, err
}
pngs = append(pngs, stepsPNGs...)
}
return svg, pngs, nil
} }

View file

@ -73,6 +73,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
c.validateKeys(g.Root, ir) c.validateKeys(g.Root, ir)
} }
c.validateNear(g) c.validateNear(g)
c.validateEdges(g)
c.compileBoardsField(g, ir, "layers") c.compileBoardsField(g, ir, "layers")
c.compileBoardsField(g, ir, "scenarios") c.compileBoardsField(g, ir, "scenarios")
@ -115,7 +116,6 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
} }
type compiler struct { type compiler struct {
inEdgeGroup bool
err d2parser.ParseError err d2parser.ParseError
} }
@ -124,6 +124,18 @@ func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
} }
func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) { func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
class := m.GetField("class")
if class != nil {
className := class.Primary()
if className == nil {
c.errorf(class.LastRef().AST(), "class missing value")
} else {
classMap := m.GetClassMap(className.String())
if classMap != nil {
c.compileMap(obj, classMap)
}
}
}
shape := m.GetField("shape") shape := m.GetField("shape")
if shape != nil { if shape != nil {
c.compileField(obj, shape) c.compileField(obj, shape)
@ -138,7 +150,7 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
c.compileField(obj, f) c.compileField(obj, f)
} }
switch obj.Attributes.Shape.Value { switch obj.Shape.Value {
case d2target.ShapeClass: case d2target.ShapeClass:
c.compileClass(obj) c.compileClass(obj)
case d2target.ShapeSQLTable: case d2target.ShapeSQLTable:
@ -158,26 +170,46 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
return return
} }
_, isReserved := d2graph.SimpleReservedKeywords[keyword] _, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved { if f.Name == "classes" {
c.compileReserved(obj.Attributes, f) if f.Map() != nil {
if len(f.Map().Edges) > 0 {
c.errorf(f.Map().Edges[0].LastRef().AST(), "classes cannot contain an edge")
}
for _, classesField := range f.Map().Fields {
if classesField.Map() == nil {
continue
}
for _, cf := range classesField.Map().Fields {
if _, ok := d2graph.ReservedKeywords[cf.Name]; !ok {
c.errorf(cf.LastRef().AST(), "%s is an invalid class field, must be reserved keyword", cf.Name)
}
if cf.Name == "class" {
c.errorf(cf.LastRef().AST(), `"class" cannot appear within "classes"`)
}
}
}
}
return
} else if isReserved {
c.compileReserved(&obj.Attributes, f)
return return
} else if f.Name == "style" { } else if f.Name == "style" {
if f.Map() == nil { if f.Map() == nil {
return return
} }
c.compileStyle(obj.Attributes, f.Map()) c.compileStyle(&obj.Attributes, f.Map())
if obj.Attributes.Style.Animated != nil { if obj.Style.Animated != nil {
c.errorf(obj.Attributes.Style.Animated.MapKey, `key "animated" can only be applied to edges`) c.errorf(obj.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
} }
return return
} }
if obj.Parent != nil { if obj.Parent != nil {
if obj.Parent.Attributes.Shape.Value == d2target.ShapeSQLTable { if obj.Parent.Shape.Value == d2target.ShapeSQLTable {
c.errorf(f.LastRef().AST(), "sql_table columns cannot have children") c.errorf(f.LastRef().AST(), "sql_table columns cannot have children")
return return
} }
if obj.Parent.Attributes.Shape.Value == d2target.ShapeClass { if obj.Parent.Shape.Value == d2target.ShapeClass {
c.errorf(f.LastRef().AST(), "class fields cannot have children") c.errorf(f.LastRef().AST(), "class fields cannot have children")
return return
} }
@ -185,14 +217,14 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
obj = obj.EnsureChild(d2graphIDA([]string{f.Name})) obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
if f.Primary() != nil { if f.Primary() != nil {
c.compileLabel(obj.Attributes, f) c.compileLabel(&obj.Attributes, f)
} }
if f.Map() != nil { if f.Map() != nil {
c.compileMap(obj, f.Map()) c.compileMap(obj, f.Map())
} }
if obj.Attributes.Label.MapKey == nil { if obj.Label.MapKey == nil {
obj.Attributes.Label.MapKey = f.LastPrimaryKey() obj.Label.MapKey = f.LastPrimaryKey()
} }
for _, fr := range f.References { for _, fr := range f.References {
if fr.Primary() { if fr.Primary() {
@ -218,7 +250,7 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
scalar := f.Primary().Value scalar := f.Primary().Value
switch scalar := scalar.(type) { switch scalar := scalar.(type) {
case *d2ast.Null: case *d2ast.Null:
// TODO: Delete instaed. // TODO: Delete instead.
attrs.Label.Value = scalar.ScalarString() attrs.Label.Value = scalar.ScalarString()
case *d2ast.BlockString: case *d2ast.BlockString:
attrs.Language = scalar.Tag attrs.Language = scalar.Tag
@ -305,18 +337,18 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
c.errorf(scalar, "non-integer width %#v: %s", scalar.ScalarString(), err) c.errorf(scalar, "non-integer width %#v: %s", scalar.ScalarString(), err)
return return
} }
attrs.Width = &d2graph.Scalar{} attrs.WidthAttr = &d2graph.Scalar{}
attrs.Width.Value = scalar.ScalarString() attrs.WidthAttr.Value = scalar.ScalarString()
attrs.Width.MapKey = f.LastPrimaryKey() attrs.WidthAttr.MapKey = f.LastPrimaryKey()
case "height": case "height":
_, err := strconv.Atoi(scalar.ScalarString()) _, err := strconv.Atoi(scalar.ScalarString())
if err != nil { if err != nil {
c.errorf(scalar, "non-integer height %#v: %s", scalar.ScalarString(), err) c.errorf(scalar, "non-integer height %#v: %s", scalar.ScalarString(), err)
return return
} }
attrs.Height = &d2graph.Scalar{} attrs.HeightAttr = &d2graph.Scalar{}
attrs.Height.Value = scalar.ScalarString() attrs.HeightAttr.Value = scalar.ScalarString()
attrs.Height.MapKey = f.LastPrimaryKey() attrs.HeightAttr.MapKey = f.LastPrimaryKey()
case "top": case "top":
v, err := strconv.Atoi(scalar.ScalarString()) v, err := strconv.Atoi(scalar.ScalarString())
if err != nil { if err != nil {
@ -362,6 +394,74 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
} }
attrs.Constraint.Value = scalar.ScalarString() attrs.Constraint.Value = scalar.ScalarString()
attrs.Constraint.MapKey = f.LastPrimaryKey() attrs.Constraint.MapKey = f.LastPrimaryKey()
case "grid-rows":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer grid-rows %#v: %s", scalar.ScalarString(), err)
return
}
if v <= 0 {
c.errorf(scalar, "grid-rows must be a positive integer: %#v", scalar.ScalarString())
return
}
attrs.GridRows = &d2graph.Scalar{}
attrs.GridRows.Value = scalar.ScalarString()
attrs.GridRows.MapKey = f.LastPrimaryKey()
case "grid-columns":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer grid-columns %#v: %s", scalar.ScalarString(), err)
return
}
if v <= 0 {
c.errorf(scalar, "grid-columns must be a positive integer: %#v", scalar.ScalarString())
return
}
attrs.GridColumns = &d2graph.Scalar{}
attrs.GridColumns.Value = scalar.ScalarString()
attrs.GridColumns.MapKey = f.LastPrimaryKey()
case "grid-gap":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer grid-gap %#v: %s", scalar.ScalarString(), err)
return
}
if v < 0 {
c.errorf(scalar, "grid-gap must be a non-negative integer: %#v", scalar.ScalarString())
return
}
attrs.GridGap = &d2graph.Scalar{}
attrs.GridGap.Value = scalar.ScalarString()
attrs.GridGap.MapKey = f.LastPrimaryKey()
case "vertical-gap":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer vertical-gap %#v: %s", scalar.ScalarString(), err)
return
}
if v < 0 {
c.errorf(scalar, "vertical-gap must be a non-negative integer: %#v", scalar.ScalarString())
return
}
attrs.VerticalGap = &d2graph.Scalar{}
attrs.VerticalGap.Value = scalar.ScalarString()
attrs.VerticalGap.MapKey = f.LastPrimaryKey()
case "horizontal-gap":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer horizontal-gap %#v: %s", scalar.ScalarString(), err)
return
}
if v < 0 {
c.errorf(scalar, "horizontal-gap must be a non-negative integer: %#v", scalar.ScalarString())
return
}
attrs.HorizontalGap = &d2graph.Scalar{}
attrs.HorizontalGap.Value = scalar.ScalarString()
attrs.HorizontalGap.MapKey = f.LastPrimaryKey()
case "class":
attrs.Classes = append(attrs.Classes, scalar.ScalarString())
case "classes":
} }
if attrs.Link != nil && attrs.Tooltip != nil { if attrs.Link != nil && attrs.Tooltip != nil {
@ -430,15 +530,17 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
case "filled": case "filled":
attrs.Style.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Style.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "width": case "width":
attrs.Width = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.WidthAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "height": case "height":
attrs.Height = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.HeightAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "top": case "top":
attrs.Top = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Top = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "left": case "left":
attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "double-border": case "double-border":
attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "text-transform":
attrs.Style.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
} }
} }
@ -450,20 +552,13 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
} }
if e.Primary() != nil { if e.Primary() != nil {
c.compileLabel(edge.Attributes, e) c.compileLabel(&edge.Attributes, e)
} }
if e.Map() != nil { if e.Map() != nil {
for _, f := range e.Map().Fields { c.compileEdgeMap(edge, e.Map())
_, 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() edge.Label.MapKey = e.LastPrimaryKey()
for _, er := range e.References { for _, er := range e.References {
scopeObjIDA := d2ir.BoardIDA(er.Context.ScopeMap) scopeObjIDA := d2ir.BoardIDA(er.Context.ScopeMap)
scopeObj := edge.Src.Graph.Root.EnsureChildIDVal(scopeObjIDA) scopeObj := edge.Src.Graph.Root.EnsureChildIDVal(scopeObjIDA)
@ -477,6 +572,29 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
} }
} }
func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
class := m.GetField("class")
if class != nil {
className := class.Primary()
if className == nil {
c.errorf(class.LastRef().AST(), "class missing value")
} else {
classMap := m.GetClassMap(className.String())
if classMap != nil {
c.compileEdgeMap(edge, classMap)
}
}
}
for _, f := range m.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)
}
}
func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) { func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
keyword := strings.ToLower(f.Name) keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword] _, isStyleReserved := d2graph.StyleKeywords[keyword]
@ -486,13 +604,13 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
} }
_, isReserved := d2graph.SimpleReservedKeywords[keyword] _, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved { if isReserved {
c.compileReserved(edge.Attributes, f) c.compileReserved(&edge.Attributes, f)
return return
} else if f.Name == "style" { } else if f.Name == "style" {
if f.Map() == nil { if f.Map() == nil {
return return
} }
c.compileStyle(edge.Attributes, f.Map()) c.compileStyle(&edge.Attributes, f.Map())
return return
} }
@ -551,7 +669,7 @@ var FullToShortLanguageAliases map[string]string
func (c *compiler) compileClass(obj *d2graph.Object) { func (c *compiler) compileClass(obj *d2graph.Object) {
obj.Class = &d2target.Class{} obj.Class = &d2target.Class{}
for _, f := range obj.ChildrenArray { for _, f := range obj.ChildrenArray {
visiblity := "public" visibility := "public"
name := f.IDVal name := f.IDVal
// See https://www.uml-diagrams.org/visibility.html // See https://www.uml-diagrams.org/visibility.html
if name != "" { if name != "" {
@ -559,35 +677,35 @@ func (c *compiler) compileClass(obj *d2graph.Object) {
case '+': case '+':
name = name[1:] name = name[1:]
case '-': case '-':
visiblity = "private" visibility = "private"
name = name[1:] name = name[1:]
case '#': case '#':
visiblity = "protected" visibility = "protected"
name = name[1:] name = name[1:]
} }
} }
if !strings.Contains(f.IDVal, "(") { if !strings.Contains(f.IDVal, "(") {
typ := f.Attributes.Label.Value typ := f.Label.Value
if typ == f.IDVal { if typ == f.IDVal {
typ = "" typ = ""
} }
obj.Class.Fields = append(obj.Class.Fields, d2target.ClassField{ obj.Class.Fields = append(obj.Class.Fields, d2target.ClassField{
Name: name, Name: name,
Type: typ, Type: typ,
Visibility: visiblity, Visibility: visibility,
}) })
} else { } else {
// TODO: Not great, AST should easily allow specifying alternate primary field // TODO: Not great, AST should easily allow specifying alternate primary field
// as an explicit label should change the name. // as an explicit label should change the name.
returnType := f.Attributes.Label.Value returnType := f.Label.Value
if returnType == f.IDVal { if returnType == f.IDVal {
returnType = "void" returnType = "void"
} }
obj.Class.Methods = append(obj.Class.Methods, d2target.ClassMethod{ obj.Class.Methods = append(obj.Class.Methods, d2target.ClassMethod{
Name: name, Name: name,
Return: returnType, Return: returnType,
Visibility: visiblity, Visibility: visibility,
}) })
} }
} }
@ -607,7 +725,7 @@ func (c *compiler) compileClass(obj *d2graph.Object) {
func (c *compiler) compileSQLTable(obj *d2graph.Object) { func (c *compiler) compileSQLTable(obj *d2graph.Object) {
obj.SQLTable = &d2target.SQLTable{} obj.SQLTable = &d2target.SQLTable{}
for _, col := range obj.ChildrenArray { for _, col := range obj.ChildrenArray {
typ := col.Attributes.Label.Value typ := col.Label.Value
if typ == col.IDVal { if typ == col.IDVal {
// Not great, AST should easily allow specifying alternate primary field // Not great, AST should easily allow specifying alternate primary field
// as an explicit label should change the name. // as an explicit label should change the name.
@ -617,8 +735,8 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {
Name: d2target.Text{Label: col.IDVal}, Name: d2target.Text{Label: col.IDVal},
Type: d2target.Text{Label: typ}, Type: d2target.Text{Label: typ},
} }
if col.Attributes.Constraint.Value != "" { if col.Constraint.Value != "" {
d2Col.Constraint = col.Attributes.Constraint.Value d2Col.Constraint = col.Constraint.Value
} }
obj.SQLTable.Columns = append(obj.SQLTable.Columns, d2Col) obj.SQLTable.Columns = append(obj.SQLTable.Columns, d2Col)
} }
@ -648,41 +766,48 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
keyword := strings.ToLower(f.Name) keyword := strings.ToLower(f.Name)
_, isReserved := d2graph.ReservedKeywords[keyword] _, isReserved := d2graph.ReservedKeywords[keyword]
if isReserved { if isReserved {
switch obj.Attributes.Shape.Value { switch obj.Shape.Value {
case d2target.ShapeCircle, d2target.ShapeSquare: case d2target.ShapeCircle, d2target.ShapeSquare:
checkEqual := (keyword == "width" && obj.Attributes.Height != nil) || (keyword == "height" && obj.Attributes.Width != nil) checkEqual := (keyword == "width" && obj.HeightAttr != nil) || (keyword == "height" && obj.WidthAttr != nil)
if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value { if checkEqual && obj.WidthAttr.Value != obj.HeightAttr.Value {
c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Attributes.Shape.Value) c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Shape.Value)
} }
} }
switch f.Name { switch f.Name {
case "style": case "style":
if obj.Attributes.Style.ThreeDee != nil { if obj.Style.ThreeDee != nil {
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeHexagon) { if !strings.EqualFold(obj.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeHexagon) {
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`) c.errorf(obj.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`)
} }
} }
if obj.Attributes.Style.DoubleBorder != nil { if obj.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 { if obj.Shape.Value != "" && obj.Shape.Value != d2target.ShapeSquare && obj.Shape.Value != d2target.ShapeRectangle && obj.Shape.Value != d2target.ShapeCircle && obj.Shape.Value != d2target.ShapeOval {
c.errorf(obj.Attributes.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`) c.errorf(obj.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
} }
} }
case "shape": case "shape":
if obj.Attributes.Shape.Value == d2target.ShapeImage && obj.Attributes.Icon == nil { if obj.Shape.Value == d2target.ShapeImage && obj.Icon == nil {
c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`) c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`)
} }
in := d2target.IsShape(obj.Attributes.Shape.Value) in := d2target.IsShape(obj.Shape.Value)
_, arrowheadIn := d2target.Arrowheads[obj.Attributes.Shape.Value] _, arrowheadIn := d2target.Arrowheads[obj.Shape.Value]
if !in && arrowheadIn { if !in && arrowheadIn {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value)) c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Shape.Value))
}
case "grid-rows", "grid-columns", "grid-gap", "vertical-gap", "horizontal-gap":
for _, child := range obj.ChildrenArray {
if child.IsContainer() {
c.errorf(f.LastPrimaryKey(),
fmt.Sprintf(`%#v can only be used on containers with one level of nesting right now. (%#v has nested %#v)`, keyword, child.AbsID(), child.ChildrenArray[0].ID))
}
} }
} }
return return
} }
if obj.Attributes.Shape.Value == d2target.ShapeImage { if obj.Shape.Value == d2target.ShapeImage {
c.errorf(f.LastRef().AST(), "image shapes cannot have children.") c.errorf(f.LastRef().AST(), "image shapes cannot have children.")
return return
} }
@ -695,9 +820,9 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
func (c *compiler) validateNear(g *d2graph.Graph) { func (c *compiler) validateNear(g *d2graph.Graph) {
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.Attributes.NearKey != nil { if obj.NearKey != nil {
nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]] _, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isKey { if isKey {
// Doesn't make sense to set near to an ancestor or descendant // Doesn't make sense to set near to an ancestor or descendant
nearIsAncestor := false nearIsAncestor := false
@ -708,7 +833,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
} }
} }
if nearIsAncestor { if nearIsAncestor {
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an ancestor") c.errorf(obj.NearKey, "near keys cannot be set to an ancestor")
continue continue
} }
nearIsDescendant := false nearIsDescendant := false
@ -719,55 +844,72 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
} }
} }
if nearIsDescendant { if nearIsDescendant {
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an descendant") c.errorf(obj.NearKey, "near keys cannot be set to an descendant")
continue continue
} }
if nearObj.OuterSequenceDiagram() != nil { if nearObj.OuterSequenceDiagram() != nil {
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an object within sequence diagrams") c.errorf(obj.NearKey, "near keys cannot be set to an object within sequence diagrams")
continue continue
} }
if nearObj.Attributes.NearKey != nil { if nearObj.NearKey != nil {
_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.Attributes.NearKey)[0]] _, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
if nearObjNearIsConst { if nearObjNearIsConst {
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an object with a constant near key") c.errorf(obj.NearKey, "near keys cannot be set to an object with a constant near key")
continue continue
} }
} }
} else if isConst { } else if isConst {
is := false
for _, e := range g.Edges {
if e.Src == obj || e.Dst == obj {
is = true
break
}
}
if is {
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on connected shapes")
continue
}
if obj.Parent != g.Root { if obj.Parent != g.Root {
c.errorf(obj.Attributes.NearKey, "constant near keys can only be set on root level shapes") c.errorf(obj.NearKey, "constant near keys can only be set on root level shapes")
continue
}
if len(obj.ChildrenArray) > 0 {
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on shapes with children")
continue continue
} }
} else { } else {
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, ", ")) c.errorf(obj.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
continue continue
} }
} }
} }
for _, edge := range g.Edges {
srcNearContainer := edge.Src.OuterNearContainer()
dstNearContainer := edge.Dst.OuterNearContainer()
var isSrcNearConst, isDstNearConst bool
if srcNearContainer != nil {
_, isSrcNearConst = d2graph.NearConstants[d2graph.Key(srcNearContainer.NearKey)[0]]
}
if dstNearContainer != nil {
_, isDstNearConst = d2graph.NearConstants[d2graph.Key(dstNearContainer.NearKey)[0]]
}
if (isSrcNearConst || isDstNearConst) && srcNearContainer != dstNearContainer {
c.errorf(edge.References[0].Edge, "cannot connect objects from within a container, that has near constant set, to objects outside that container")
}
}
}
func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges {
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
continue
}
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
continue
}
}
} }
func (c *compiler) validateBoardLinks(g *d2graph.Graph) { func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.Attributes.Link == nil { if obj.Link == nil {
continue continue
} }
linkKey, err := d2parser.ParseKey(obj.Attributes.Link.Value) linkKey, err := d2parser.ParseKey(obj.Link.Value)
if err != nil { if err != nil {
continue continue
} }
@ -777,7 +919,7 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
} }
if !hasBoard(g.RootBoard(), linkKey.IDA()) { if !hasBoard(g.RootBoard(), linkKey.IDA()) {
c.errorf(obj.Attributes.Link.MapKey, "linked board not found") c.errorf(obj.Link.MapKey, "linked board not found")
continue continue
} }
} }

View file

@ -43,8 +43,8 @@ x: {
t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle { if g.Objects[0].Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value to be circle: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected g.Objects[0].Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
} }
}, },
@ -65,8 +65,8 @@ x: {
t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" { if g.Objects[0].Style.Opacity.Value != "0.4" {
t.Fatalf("expected g.Objects[0].Attributes.Style.Opacity.Value to be 0.4: %#v", g.Objects[0].Attributes.Style.Opacity.Value) t.Fatalf("expected g.Objects[0].Style.Opacity.Value to be 0.4: %#v", g.Objects[0].Style.Opacity.Value)
} }
}, },
@ -102,14 +102,14 @@ x: {
if g.Objects[0].ID != "hey" { if g.Objects[0].ID != "hey" {
t.Fatalf("expected g.Objects[0].ID to be 'hey': %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be 'hey': %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeHexagon { if g.Objects[0].Shape.Value != d2target.ShapeHexagon {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value to be hexagon: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected g.Objects[0].Shape.Value to be hexagon: %#v", g.Objects[0].Shape.Value)
} }
if g.Objects[0].Attributes.Width.Value != "200" { if g.Objects[0].WidthAttr.Value != "200" {
t.Fatalf("expected g.Objects[0].Attributes.Width.Value to be 200: %#v", g.Objects[0].Attributes.Width.Value) t.Fatalf("expected g.Objects[0].Width.Value to be 200: %#v", g.Objects[0].WidthAttr.Value)
} }
if g.Objects[0].Attributes.Height.Value != "230" { if g.Objects[0].HeightAttr.Value != "230" {
t.Fatalf("expected g.Objects[0].Attributes.Height.Value to be 230: %#v", g.Objects[0].Attributes.Height.Value) t.Fatalf("expected g.Objects[0].Height.Value to be 230: %#v", g.Objects[0].HeightAttr.Value)
} }
}, },
}, },
@ -121,7 +121,7 @@ x: {
} }
`, `,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "200", g.Objects[0].Attributes.Top.Value) tassert.Equal(t, "200", g.Objects[0].Top.Value)
}, },
}, },
{ {
@ -160,13 +160,13 @@ d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and
if g.Objects[0].ID != "hey" { if g.Objects[0].ID != "hey" {
t.Fatalf("expected ID to be 'hey': %#v", g.Objects[0]) t.Fatalf("expected ID to be 'hey': %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle { if g.Objects[0].Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
} }
if g.Objects[0].Attributes.Width != nil { if g.Objects[0].WidthAttr != nil {
t.Fatalf("expected Attributes.Width to be nil: %#v", g.Objects[0].Attributes.Width) t.Fatalf("expected Attributes.Width to be nil: %#v", g.Objects[0].WidthAttr)
} }
if g.Objects[0].Attributes.Height == nil { if g.Objects[0].HeightAttr == nil {
t.Fatalf("Attributes.Height is nil") t.Fatalf("Attributes.Height is nil")
} }
}, },
@ -237,7 +237,7 @@ containers: {
} }
`, `,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
if g.Objects[0].Attributes.Icon == nil { if g.Objects[0].Icon == nil {
t.Fatal("Attribute icon is nil") t.Fatal("Attribute icon is nil")
} }
}, },
@ -326,7 +326,7 @@ containers: {
if len(g.Objects) != 1 { if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects) t.Fatalf("expected 1 objects: %#v", g.Objects)
} }
if g.Objects[0].Attributes.Style.StrokeWidth.Value != "0" { if g.Objects[0].Style.StrokeWidth.Value != "0" {
t.Fatalf("unexpected") t.Fatalf("unexpected")
} }
}, },
@ -442,8 +442,8 @@ y: "But it's real. And if it's real it can be affected ... we may not be able"
if len(g.Root.ChildrenArray) != 2 { if len(g.Root.ChildrenArray) != 2 {
t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray)) t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
} }
if g.Objects[1].Attributes.Label.Value != "But it's real. And if it's real it can be affected ... we may not be able" { if g.Objects[1].Label.Value != "But it's real. And if it's real it can be affected ... we may not be able" {
t.Fatalf("expected g.Objects[1].Label.Value to be last value: %#v", g.Objects[1].Attributes.Label.Value) t.Fatalf("expected g.Objects[1].Label.Value to be last value: %#v", g.Objects[1].Label.Value)
} }
}, },
}, },
@ -470,8 +470,8 @@ x: {
if len(g.Root.ChildrenArray) != 2 { if len(g.Root.ChildrenArray) != 2 {
t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray)) t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
} }
if g.Objects[0].Attributes.Label.Value != "All we are given is possibilities -- to make ourselves one thing or another." { if g.Objects[0].Label.Value != "All we are given is possibilities -- to make ourselves one thing or another." {
t.Fatalf("expected g.Objects[0].Label.Value to be last value: %#v", g.Objects[0].Attributes.Label.Value) t.Fatalf("expected g.Objects[0].Label.Value to be last value: %#v", g.Objects[0].Label.Value)
} }
}, },
}, },
@ -626,11 +626,11 @@ x: {
if g.Edges[1].Dst.ID != "b" { if g.Edges[1].Dst.ID != "b" {
t.Fatalf("expected g.Edges[1].Dst.ID to be b: %#v", g.Edges[1]) t.Fatalf("expected g.Edges[1].Dst.ID to be b: %#v", g.Edges[1])
} }
if g.Edges[0].Attributes.Label.Value != "Can you imagine how life could be improved if we could do away with" { if g.Edges[0].Label.Value != "Can you imagine how life could be improved if we could do away with" {
t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Attributes.Label) t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label)
} }
if g.Edges[1].Attributes.Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." { if g.Edges[1].Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." {
t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Attributes.Label) t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Label)
} }
}, },
}, },
@ -656,8 +656,8 @@ x: {
if g.Edges[0].Dst.ID != "b" { if g.Edges[0].Dst.ID != "b" {
t.Fatalf("expected g.Edges[0].Dst.ID to be b: %#v", g.Edges[0]) t.Fatalf("expected g.Edges[0].Dst.ID to be b: %#v", g.Edges[0])
} }
if g.Edges[0].Attributes.Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." { if g.Edges[0].Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." {
t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Attributes.Label) t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label)
} }
}, },
}, },
@ -756,11 +756,11 @@ x -> y -> z: "The kids will love our inflatable slides"
t.Fatalf("expected g.Edges[1].Dst.ID to be y: %#v", g.Edges[1]) t.Fatalf("expected g.Edges[1].Dst.ID to be y: %#v", g.Edges[1])
} }
if g.Edges[0].Attributes.Label.Value != "The kids will love our inflatable slides" { if g.Edges[0].Label.Value != "The kids will love our inflatable slides" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label: %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label.Value)
} }
if g.Edges[1].Attributes.Label.Value != "The kids will love our inflatable slides" { if g.Edges[1].Label.Value != "The kids will love our inflatable slides" {
t.Fatalf("unexpected g.Edges[1].Attributes.Label: %#v", g.Edges[1].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Label.Value)
} }
}, },
}, },
@ -797,8 +797,8 @@ x -> y: one
if !g.Edges[0].DstArrow { if !g.Edges[0].DstArrow {
t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow) t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
} }
if g.Edges[0].Attributes.Label.Value != "two" { if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label to be two: %#v", g.Edges[0].Attributes.Label) t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
} }
}, },
}, },
@ -840,8 +840,8 @@ b: {
if !g.Edges[0].DstArrow { if !g.Edges[0].DstArrow {
t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow) t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
} }
if g.Edges[0].Attributes.Label.Value != "two" { if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label to be two: %#v", g.Edges[0].Attributes.Label) t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
} }
}, },
}, },
@ -883,8 +883,8 @@ b.(x -> y)[0]: two
if !g.Edges[0].DstArrow { if !g.Edges[0].DstArrow {
t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow) t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
} }
if g.Edges[0].Attributes.Label.Value != "two" { if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label to be two: %#v", g.Edges[0].Attributes.Label) t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
} }
}, },
}, },
@ -936,8 +936,8 @@ x -> y: {
if g.Edges[0].Dst.ID != "y" { if g.Edges[0].Dst.ID != "y" {
t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0]) t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
} }
if g.Edges[0].Attributes.Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." { if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -950,8 +950,8 @@ x -> y: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Label.Value != "asdf" { if g.Edges[0].Label.Value != "asdf" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -972,7 +972,7 @@ x -> y: {
t.Fatalf("expected 2 objects: %#v", g.Objects) t.Fatalf("expected 2 objects: %#v", g.Objects)
} }
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value) assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value) assert.String(t, "", g.Edges[0].Shape.Value)
// Make sure the DSL didn't change. this is a regression test where it did // Make sure the DSL didn't change. this is a regression test where it did
exp := `x -> y: { exp := `x -> y: {
source-arrowhead: { source-arrowhead: {
@ -1025,9 +1025,9 @@ x -> y: {
assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value) assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
assert.String(t, "QOTD", g.Edges[0].DstArrowhead.Label.Value) assert.String(t, "QOTD", g.Edges[0].DstArrowhead.Label.Value)
assert.String(t, "true", g.Edges[0].DstArrowhead.Style.Filled.Value) assert.String(t, "true", g.Edges[0].DstArrowhead.Style.Filled.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value) assert.String(t, "", g.Edges[0].Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Label.Value) assert.String(t, "", g.Edges[0].Label.Value)
assert.JSON(t, nil, g.Edges[0].Attributes.Style.Filled) assert.JSON(t, nil, g.Edges[0].Style.Filled)
}, },
}, },
{ {
@ -1044,7 +1044,7 @@ x -> y: {
t.Fatalf("expected 2 objects: %#v", g.Objects) t.Fatalf("expected 2 objects: %#v", g.Objects)
} }
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value) assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value) assert.String(t, "", g.Edges[0].Shape.Value)
}, },
}, },
{ {
@ -1061,7 +1061,7 @@ x -> y: {
t.Fatalf("expected 2 objects: %#v", g.Objects) t.Fatalf("expected 2 objects: %#v", g.Objects)
} }
assert.String(t, "triangle", g.Edges[0].SrcArrowhead.Shape.Value) assert.String(t, "triangle", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value) assert.String(t, "", g.Edges[0].Shape.Value)
}, },
}, },
{ {
@ -1087,7 +1087,7 @@ x -> y: {
t.Fatalf("expected 2 objects: %#v", g.Objects) t.Fatalf("expected 2 objects: %#v", g.Objects)
} }
assert.String(t, "yo", g.Edges[0].SrcArrowhead.Label.Value) assert.String(t, "yo", g.Edges[0].SrcArrowhead.Label.Value)
assert.String(t, "", g.Edges[0].Attributes.Label.Value) assert.String(t, "", g.Edges[0].Label.Value)
}, },
}, },
{ {
@ -1106,7 +1106,7 @@ x -> y: {
t.Fatalf("expected 2 objects: %#v", g.Objects) t.Fatalf("expected 2 objects: %#v", g.Objects)
} }
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value) assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value) assert.String(t, "", g.Edges[0].Shape.Value)
}, },
}, },
{ {
@ -1128,7 +1128,7 @@ x -> y: {
} }
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value) assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value) assert.String(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value) assert.String(t, "", g.Edges[0].Shape.Value)
}, },
}, },
{ {
@ -1143,8 +1143,8 @@ x -> y: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Animated.Value != "true" { if g.Edges[0].Style.Animated.Value != "true" {
t.Fatalf("Edges[0].Attributes.Style.Animated.Value: %#v", g.Edges[0].Attributes.Style.Animated.Value) t.Fatalf("Edges[0].Style.Animated.Value: %#v", g.Edges[0].Style.Animated.Value)
} }
}, },
}, },
@ -1201,11 +1201,11 @@ x -> y -> z: {
if len(g.Edges) != 2 { if len(g.Edges) != 2 {
t.Fatalf("expected 2 edge: %#v", g.Edges) t.Fatalf("expected 2 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." { if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
if g.Edges[1].Attributes.Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." { if g.Edges[1].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[1].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[1].Label.Value)
} }
}, },
}, },
@ -1226,8 +1226,8 @@ x -> y
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." { if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1249,8 +1249,8 @@ x -> y: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
}, },
}, },
@ -1270,11 +1270,11 @@ x -> y: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1293,11 +1293,11 @@ x -> y
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1317,11 +1317,11 @@ x -> y
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1342,11 +1342,11 @@ x.(a -> b)[0].style.opacity: 0.4
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1367,11 +1367,11 @@ x: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1396,11 +1396,11 @@ x: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1423,11 +1423,11 @@ x: {
if len(g.Edges) != 1 { if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges) t.Fatalf("expected 1 edge: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value) t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
} }
if g.Edges[0].Attributes.Label.Value != "" { if g.Edges[0].Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1452,8 +1452,8 @@ x -> y: {
if len(g.Objects) != 1 { if len(g.Objects) != 1 {
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
if g.Objects[0].Attributes.Link.Value != "https://google.com" { if g.Objects[0].Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Link.Value) t.Fatal(g.Objects[0].Link.Value)
} }
}, },
}, },
@ -1465,8 +1465,8 @@ x -> y: {
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
if g.Objects[0].Attributes.Tooltip.Value != "https://google.com" { if g.Objects[0].Tooltip.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Tooltip.Value) t.Fatal(g.Objects[0].Tooltip.Value)
} }
}, },
}, },
@ -1482,12 +1482,12 @@ x -> y: {
if len(g.Objects) != 1 { if len(g.Objects) != 1 {
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
if g.Objects[0].Attributes.Link.Value != "https://google.com" { if g.Objects[0].Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Link.Value) t.Fatal(g.Objects[0].Link.Value)
} }
if g.Objects[0].Attributes.Tooltip.Value != "hello world" { if g.Objects[0].Tooltip.Value != "hello world" {
t.Fatal(g.Objects[0].Attributes.Tooltip.Value) t.Fatal(g.Objects[0].Tooltip.Value)
} }
}, },
}, },
@ -1517,8 +1517,8 @@ b: {
if len(g.Objects) != 1 { if len(g.Objects) != 1 {
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
if g.Objects[0].Attributes.Link.Value != "Overview.Untitled board 7.zzzzz" { if g.Objects[0].Link.Value != "Overview.Untitled board 7.zzzzz" {
t.Fatal(g.Objects[0].Attributes.Link.Value) t.Fatal(g.Objects[0].Link.Value)
} }
}, },
}, },
@ -1558,25 +1558,27 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
`, `,
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`, 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",
text: `x: {
near: top-center
y
}
`,
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", name: "near_bad_connected",
text: `x: { text: `
x: {
near: top-center near: top-center
} }
x -> y x -> y
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:2:9: constant near keys cannot be set on connected shapes`, expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:5:5: cannot connect objects from within a container, that has near constant set, to objects outside that container`,
},
{
name: "near_descendant_connect_to_outside",
text: `
x: {
near: top-left
y
}
x.y -> z
`,
expErr: "d2/testdata/d2compiler/TestCompile/near_descendant_connect_to_outside.d2:6:5: cannot connect objects from within a container, that has near constant set, to objects outside that container",
}, },
{ {
name: "nested_near_constant", name: "nested_near_constant",
@ -1601,20 +1603,20 @@ y
if len(g.Objects) != 2 { if len(g.Objects) != 2 {
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
if g.Objects[0].Attributes.NearKey == nil { if g.Objects[0].NearKey == nil {
t.Fatal("missing near key") t.Fatal("missing near key")
} }
if g.Objects[0].Attributes.Icon.Path != "orange" { if g.Objects[0].Icon.Path != "orange" {
t.Fatal(g.Objects[0].Attributes.Icon) t.Fatal(g.Objects[0].Icon)
} }
if g.Objects[0].Attributes.Style.Opacity.Value != "0.5" { if g.Objects[0].Style.Opacity.Value != "0.5" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity) t.Fatal(g.Objects[0].Style.Opacity)
} }
if g.Objects[0].Attributes.Style.Stroke.Value != "red" { if g.Objects[0].Style.Stroke.Value != "red" {
t.Fatal(g.Objects[0].Attributes.Style.Stroke) t.Fatal(g.Objects[0].Style.Stroke)
} }
if g.Objects[0].Attributes.Style.Fill.Value != "green" { if g.Objects[0].Style.Fill.Value != "green" {
t.Fatal(g.Objects[0].Attributes.Style.Fill) t.Fatal(g.Objects[0].Style.Fill)
} }
}, },
}, },
@ -1694,7 +1696,7 @@ y -> x.style
} }
assert.String(t, `"b\nb"`, g.Objects[0].ID) assert.String(t, `"b\nb"`, g.Objects[0].ID)
assert.String(t, `b assert.String(t, `b
b`, g.Objects[0].Attributes.Label.Value) b`, g.Objects[0].Label.Value)
}, },
}, },
{ {
@ -1706,7 +1708,7 @@ b`, g.Objects[0].Attributes.Label.Value)
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
assert.String(t, "b\rb", g.Objects[0].ID) assert.String(t, "b\rb", g.Objects[0].ID)
assert.String(t, "b\rb", g.Objects[0].Attributes.Label.Value) assert.String(t, "b\rb", g.Objects[0].Label.Value)
}, },
}, },
{ {
@ -1728,8 +1730,8 @@ b`, g.Objects[0].Attributes.Label.Value)
if len(g.Objects[0].Class.Methods) != 0 { if len(g.Objects[0].Class.Methods) != 0 {
t.Fatal(len(g.Objects[0].Class.Methods)) t.Fatal(len(g.Objects[0].Class.Methods))
} }
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" { if g.Objects[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity.Value) t.Fatal(g.Objects[0].Style.Opacity.Value)
} }
}, },
}, },
@ -1749,8 +1751,8 @@ b`, g.Objects[0].Attributes.Label.Value)
if len(g.Objects[0].SQLTable.Columns) != 1 { if len(g.Objects[0].SQLTable.Columns) != 1 {
t.Fatal(len(g.Objects[0].SQLTable.Columns)) t.Fatal(len(g.Objects[0].SQLTable.Columns))
} }
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" { if g.Objects[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity.Value) t.Fatal(g.Objects[0].Style.Opacity.Value)
} }
}, },
}, },
@ -1773,8 +1775,8 @@ b`, g.Objects[0].Attributes.Label.Value)
if len(g.Objects[0].SQLTable.Columns) != 1 { if len(g.Objects[0].SQLTable.Columns) != 1 {
t.Fatal(len(g.Objects[0].SQLTable.Columns)) t.Fatal(len(g.Objects[0].SQLTable.Columns))
} }
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" { if g.Objects[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity.Value) t.Fatal(g.Objects[0].Style.Opacity.Value)
} }
}, },
}, },
@ -1794,7 +1796,7 @@ x.y -> a.b: {
} }
`, `,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "true", g.Edges[0].Attributes.Style.Animated.Value) tassert.Equal(t, "true", g.Edges[0].Style.Animated.Value)
}, },
}, },
{ {
@ -1899,7 +1901,7 @@ dst.id <-> src.dst_id
} }
`, `,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "sequence_diagram", g.Objects[0].Attributes.Shape.Value) assert.String(t, "sequence_diagram", g.Objects[0].Shape.Value)
}, },
}, },
{ {
@ -1946,7 +1948,7 @@ b
text: `shape: sequence_diagram text: `shape: sequence_diagram
`, `,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "sequence_diagram", g.Root.Attributes.Shape.Value) assert.String(t, "sequence_diagram", g.Root.Shape.Value)
}, },
}, },
{ {
@ -2026,7 +2028,7 @@ ok: {
text: `direction: right`, text: `direction: right`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "right", g.Root.Attributes.Direction.Value) assert.String(t, "right", g.Root.Direction.Value)
}, },
}, },
{ {
@ -2034,7 +2036,7 @@ ok: {
text: `x`, text: `x`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "", g.Objects[0].Attributes.Direction.Value) assert.String(t, "", g.Objects[0].Direction.Value)
}, },
}, },
{ {
@ -2044,7 +2046,7 @@ ok: {
direction: left direction: left
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "left", g.Objects[0].Attributes.Direction.Value) assert.String(t, "left", g.Objects[0].Direction.Value)
}, },
}, },
{ {
@ -2055,7 +2057,7 @@ ok: {
constraint: BIZ constraint: BIZ
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "bar", g.Objects[0].Attributes.Label.Value) assert.String(t, "bar", g.Objects[0].Label.Value)
}, },
}, },
{ {
@ -2149,7 +2151,7 @@ layers: {
} }
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.x", g.Objects[0].Attributes.Link.Value) tassert.Equal(t, "root.layers.x", g.Objects[0].Link.Value)
}, },
}, },
{ {
@ -2169,8 +2171,8 @@ scenarios: {
} }
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.cat", g.Objects[0].Attributes.Link.Value) tassert.Equal(t, "root.layers.cat", g.Objects[0].Link.Value)
tassert.Equal(t, "root.layers.cat", g.Scenarios[0].Objects[0].Attributes.Link.Value) tassert.Equal(t, "root.layers.cat", g.Scenarios[0].Objects[0].Link.Value)
}, },
}, },
{ {
@ -2203,7 +2205,7 @@ layers: {
} }
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.x.layers.x", g.Objects[0].Attributes.Link.Value) tassert.Equal(t, "root.layers.x.layers.x", g.Objects[0].Link.Value)
}, },
}, },
{ {
@ -2217,7 +2219,7 @@ layers: {
} }
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "root.layers.x", g.Objects[1].Attributes.Link.Value) tassert.Equal(t, "root.layers.x", g.Objects[1].Link.Value)
}, },
}, },
{ {
@ -2235,9 +2237,9 @@ layers: {
} }
}`, }`,
assertions: func(t *testing.T, g *d2graph.Graph) { assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.NotNil(t, g.Layers[0].Layers[0].Objects[0].Attributes.Link.Value) tassert.NotNil(t, g.Layers[0].Layers[0].Objects[0].Link.Value)
tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[0].Attributes.Link.Value) tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[0].Link.Value)
tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[1].Attributes.Link.Value) tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[1].Link.Value)
}, },
}, },
{ {
@ -2263,6 +2265,17 @@ x: {
}`, }`,
expErr: `d2/testdata/d2compiler/TestCompile/border-radius-negative.d2:3:24: expected "border-radius" to be a number greater or equal to 0`, expErr: `d2/testdata/d2compiler/TestCompile/border-radius-negative.d2:3:24: expected "border-radius" to be a number greater or equal to 0`,
}, },
{
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", name: "near_near_const",
text: ` text: `
@ -2276,6 +2289,160 @@ obj {
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`, expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
}, },
{
name: "grid",
text: `hey: {
grid-rows: 200
grid-columns: 230
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "200", g.Objects[0].GridRows.Value)
},
},
{
name: "grid_negative",
text: `hey: {
grid-rows: 200
grid-columns: -200
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:16: grid-columns must be a positive integer: "-200"`,
},
{
name: "grid_gap_negative",
text: `hey: {
horizontal-gap: -200
vertical-gap: -30
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:2:18: horizontal-gap must be a non-negative integer: "-200"
d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must be a non-negative integer: "-30"`,
},
{
name: "grid_edge",
text: `hey: {
grid-rows: 1
a -> b
}
c -> hey.b
hey.a -> c
hey -> c: ok
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are not supported yet`,
},
{
name: "grid_nested",
text: `hey: {
grid-rows: 200
grid-columns: 200
a
b
c
d.invalid descendant
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_nested.d2:2:2: "grid-rows" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")
d2/testdata/d2compiler/TestCompile/grid_nested.d2:3:2: "grid-columns" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")`,
},
{
name: "classes",
text: `classes: {
dragon_ball: {
label: ""
shape: circle
style.fill: orange
}
path: {
label: "then"
style.stroke-width: 4
}
}
nostar: { class: dragon_ball }
1star: "*" { class: dragon_ball; style.fill: red }
2star: { label: "**"; class: dragon_ball }
nostar -> 1star: { class: path }
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 3, len(g.Objects))
tassert.Equal(t, "dragon_ball", g.Objects[0].Classes[0])
tassert.Equal(t, "", g.Objects[0].Label.Value)
// Class field overrides primary
tassert.Equal(t, "", g.Objects[1].Label.Value)
tassert.Equal(t, "**", g.Objects[2].Label.Value)
tassert.Equal(t, "orange", g.Objects[0].Style.Fill.Value)
tassert.Equal(t, "red", g.Objects[1].Style.Fill.Value)
tassert.Equal(t, "4", g.Edges[0].Style.StrokeWidth.Value)
tassert.Equal(t, "then", g.Edges[0].Label.Value)
},
},
{
name: "reordered-classes",
text: `classes: {
x: {
shape: circle
}
}
a.class: x
classes.x.shape: diamond
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 1, len(g.Objects))
tassert.Equal(t, "diamond", g.Objects[0].Shape.Value)
},
},
{
name: "no-class-primary",
text: `x.class
`,
expErr: `d2/testdata/d2compiler/TestCompile/no-class-primary.d2:1:3: class missing value`,
},
{
name: "no-class-inside-classes",
text: `classes: {
x: {
class: y
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/no-class-inside-classes.d2:3:5: "class" cannot appear within "classes"`,
},
{
// This is okay
name: "missing-class",
text: `x.class: yo
`,
},
{
name: "classes-unreserved",
text: `classes: {
mango: {
seed
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/classes-unreserved.d2:3:5: seed is an invalid class field, must be reserved keyword`,
},
{
name: "classes-internal-edge",
text: `classes: {
mango: {
width: 100
}
jango: {
height: 100
}
mango -> jango
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/classes-internal-edge.d2:8:3: classes cannot contain an edge`,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -2412,6 +2579,19 @@ layers: {
assert.False(t, g.Layers[1].Scenarios[1].IsFolderOnly) assert.False(t, g.Layers[1].Scenarios[1].IsFolderOnly)
}, },
}, },
{
name: "scenarios_edge_index",
run: func(t *testing.T) {
assertCompile(t, `a -> x
scenarios: {
1: {
(a -> x)[0].style.opacity: 0.1
}
}
`, "")
},
},
{ {
name: "errs/duplicate_board", name: "errs/duplicate_board",
run: func(t *testing.T) { run: func(t *testing.T) {

View file

@ -42,10 +42,10 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) { func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) {
shape.Stroke = obj.GetStroke(shape.StrokeDash) shape.Stroke = obj.GetStroke(shape.StrokeDash)
shape.Fill = obj.GetFill() shape.Fill = obj.GetFill()
if obj.Attributes.Shape.Value == d2target.ShapeText { if obj.Shape.Value == d2target.ShapeText {
shape.Color = color.N1 shape.Color = color.N1
} }
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass { if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
shape.PrimaryAccentColor = color.B2 shape.PrimaryAccentColor = color.B2
shape.SecondaryAccentColor = color.AA2 shape.SecondaryAccentColor = color.AA2
shape.NeutralAccentColor = color.N2 shape.NeutralAccentColor = color.N2
@ -72,63 +72,64 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Them
} }
func applyStyles(shape *d2target.Shape, obj *d2graph.Object) { func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
if obj.Attributes.Style.Opacity != nil { if obj.Style.Opacity != nil {
shape.Opacity, _ = strconv.ParseFloat(obj.Attributes.Style.Opacity.Value, 64) shape.Opacity, _ = strconv.ParseFloat(obj.Style.Opacity.Value, 64)
} }
if obj.Attributes.Style.StrokeDash != nil { if obj.Style.StrokeDash != nil {
shape.StrokeDash, _ = strconv.ParseFloat(obj.Attributes.Style.StrokeDash.Value, 64) shape.StrokeDash, _ = strconv.ParseFloat(obj.Style.StrokeDash.Value, 64)
} }
if obj.Attributes.Style.Fill != nil { if obj.Style.Fill != nil {
shape.Fill = obj.Attributes.Style.Fill.Value shape.Fill = obj.Style.Fill.Value
} else if obj.Attributes.Shape.Value == d2target.ShapeText { } else if obj.Shape.Value == d2target.ShapeText {
shape.Fill = "transparent" shape.Fill = "transparent"
} }
if obj.Attributes.Style.FillPattern != nil { if obj.Style.FillPattern != nil {
shape.FillPattern = obj.Attributes.Style.FillPattern.Value shape.FillPattern = obj.Style.FillPattern.Value
} }
if obj.Attributes.Style.Stroke != nil { if obj.Style.Stroke != nil {
shape.Stroke = obj.Attributes.Style.Stroke.Value shape.Stroke = obj.Style.Stroke.Value
} }
if obj.Attributes.Style.StrokeWidth != nil { if obj.Style.StrokeWidth != nil {
shape.StrokeWidth, _ = strconv.Atoi(obj.Attributes.Style.StrokeWidth.Value) shape.StrokeWidth, _ = strconv.Atoi(obj.Style.StrokeWidth.Value)
} }
if obj.Attributes.Style.Shadow != nil { if obj.Style.Shadow != nil {
shape.Shadow, _ = strconv.ParseBool(obj.Attributes.Style.Shadow.Value) shape.Shadow, _ = strconv.ParseBool(obj.Style.Shadow.Value)
} }
if obj.Attributes.Style.ThreeDee != nil { if obj.Style.ThreeDee != nil {
shape.ThreeDee, _ = strconv.ParseBool(obj.Attributes.Style.ThreeDee.Value) shape.ThreeDee, _ = strconv.ParseBool(obj.Style.ThreeDee.Value)
} }
if obj.Attributes.Style.Multiple != nil { if obj.Style.Multiple != nil {
shape.Multiple, _ = strconv.ParseBool(obj.Attributes.Style.Multiple.Value) shape.Multiple, _ = strconv.ParseBool(obj.Style.Multiple.Value)
} }
if obj.Attributes.Style.BorderRadius != nil { if obj.Style.BorderRadius != nil {
shape.BorderRadius, _ = strconv.Atoi(obj.Attributes.Style.BorderRadius.Value) shape.BorderRadius, _ = strconv.Atoi(obj.Style.BorderRadius.Value)
} }
if obj.Attributes.Style.FontColor != nil { if obj.Style.FontColor != nil {
shape.Color = obj.Attributes.Style.FontColor.Value shape.Color = obj.Style.FontColor.Value
} }
if obj.Attributes.Style.Italic != nil { if obj.Style.Italic != nil {
shape.Italic, _ = strconv.ParseBool(obj.Attributes.Style.Italic.Value) shape.Italic, _ = strconv.ParseBool(obj.Style.Italic.Value)
} }
if obj.Attributes.Style.Bold != nil { if obj.Style.Bold != nil {
shape.Bold, _ = strconv.ParseBool(obj.Attributes.Style.Bold.Value) shape.Bold, _ = strconv.ParseBool(obj.Style.Bold.Value)
} }
if obj.Attributes.Style.Underline != nil { if obj.Style.Underline != nil {
shape.Underline, _ = strconv.ParseBool(obj.Attributes.Style.Underline.Value) shape.Underline, _ = strconv.ParseBool(obj.Style.Underline.Value)
} }
if obj.Attributes.Style.Font != nil { if obj.Style.Font != nil {
shape.FontFamily = obj.Attributes.Style.Font.Value shape.FontFamily = obj.Style.Font.Value
} }
if obj.Attributes.Style.DoubleBorder != nil { if obj.Style.DoubleBorder != nil {
shape.DoubleBorder, _ = strconv.ParseBool(obj.Attributes.Style.DoubleBorder.Value) shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value)
} }
} }
func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape := d2target.BaseShape() shape := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value) shape.SetType(obj.Shape.Value)
shape.ID = obj.AbsID() shape.ID = obj.AbsID()
shape.Classes = obj.Classes
shape.ZIndex = obj.ZIndex shape.ZIndex = obj.ZIndex
shape.Level = int(obj.Level()) shape.Level = int(obj.Level())
shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y)) shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y))
@ -154,10 +155,10 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape.Color = text.GetColor(shape.Italic) shape.Color = text.GetColor(shape.Italic)
applyStyles(shape, obj) applyStyles(shape, obj)
switch obj.Attributes.Shape.Value { switch obj.Shape.Value {
case d2target.ShapeCode, d2target.ShapeText: case d2target.ShapeCode, d2target.ShapeText:
shape.Language = obj.Attributes.Language shape.Language = obj.Language
shape.Label = obj.Attributes.Label.Value shape.Label = obj.Label.Value
case d2target.ShapeClass: case d2target.ShapeClass:
shape.Class = *obj.Class shape.Class = *obj.Class
// The label is the header for classes and tables, which is set in client to be 4 px larger than the object's set font size // The label is the header for classes and tables, which is set in client to be 4 px larger than the object's set font size
@ -177,13 +178,13 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
} }
} }
if obj.Attributes.Tooltip != nil { if obj.Tooltip != nil {
shape.Tooltip = obj.Attributes.Tooltip.Value shape.Tooltip = obj.Tooltip.Value
} }
if obj.Attributes.Link != nil { if obj.Link != nil {
shape.Link = obj.Attributes.Link.Value shape.Link = obj.Link.Value
} }
shape.Icon = obj.Attributes.Icon shape.Icon = obj.Icon
if obj.IconPosition != nil { if obj.IconPosition != nil {
shape.IconPosition = *obj.IconPosition shape.IconPosition = *obj.IconPosition
} }
@ -194,6 +195,7 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection { func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
connection := d2target.BaseConnection() connection := d2target.BaseConnection()
connection.ID = edge.AbsID() connection.ID = edge.AbsID()
connection.Classes = edge.Classes
connection.ZIndex = edge.ZIndex connection.ZIndex = edge.ZIndex
text := edge.Text() text := edge.Text()
@ -211,7 +213,11 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
} }
if edge.SrcArrowhead != nil { if edge.SrcArrowhead != nil {
if edge.SrcArrowhead.Label.Value != "" { if edge.SrcArrowhead.Label.Value != "" {
connection.SrcLabel = edge.SrcArrowhead.Label.Value connection.SrcLabel = &d2target.Text{
Label: edge.SrcArrowhead.Label.Value,
LabelWidth: edge.SrcArrowhead.LabelDimensions.Width,
LabelHeight: edge.SrcArrowhead.LabelDimensions.Height,
}
} }
} }
if edge.DstArrow { if edge.DstArrow {
@ -228,66 +234,70 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
} }
if edge.DstArrowhead != nil { if edge.DstArrowhead != nil {
if edge.DstArrowhead.Label.Value != "" { if edge.DstArrowhead.Label.Value != "" {
connection.DstLabel = edge.DstArrowhead.Label.Value connection.DstLabel = &d2target.Text{
Label: edge.DstArrowhead.Label.Value,
LabelWidth: edge.DstArrowhead.LabelDimensions.Width,
LabelHeight: edge.DstArrowhead.LabelDimensions.Height,
}
} }
} }
if theme != nil && theme.SpecialRules.NoCornerRadius { if theme != nil && theme.SpecialRules.NoCornerRadius {
connection.BorderRadius = 0 connection.BorderRadius = 0
} }
if edge.Attributes.Style.BorderRadius != nil { if edge.Style.BorderRadius != nil {
connection.BorderRadius, _ = strconv.ParseFloat(edge.Attributes.Style.BorderRadius.Value, 64) connection.BorderRadius, _ = strconv.ParseFloat(edge.Style.BorderRadius.Value, 64)
} }
if edge.Attributes.Style.Opacity != nil { if edge.Style.Opacity != nil {
connection.Opacity, _ = strconv.ParseFloat(edge.Attributes.Style.Opacity.Value, 64) connection.Opacity, _ = strconv.ParseFloat(edge.Style.Opacity.Value, 64)
} }
if edge.Attributes.Style.StrokeDash != nil { if edge.Style.StrokeDash != nil {
connection.StrokeDash, _ = strconv.ParseFloat(edge.Attributes.Style.StrokeDash.Value, 64) connection.StrokeDash, _ = strconv.ParseFloat(edge.Style.StrokeDash.Value, 64)
} }
connection.Stroke = edge.GetStroke(connection.StrokeDash) connection.Stroke = edge.GetStroke(connection.StrokeDash)
if edge.Attributes.Style.Stroke != nil { if edge.Style.Stroke != nil {
connection.Stroke = edge.Attributes.Style.Stroke.Value connection.Stroke = edge.Style.Stroke.Value
} }
if edge.Attributes.Style.StrokeWidth != nil { if edge.Style.StrokeWidth != nil {
connection.StrokeWidth, _ = strconv.Atoi(edge.Attributes.Style.StrokeWidth.Value) connection.StrokeWidth, _ = strconv.Atoi(edge.Style.StrokeWidth.Value)
} }
if edge.Attributes.Style.Fill != nil { if edge.Style.Fill != nil {
connection.Fill = edge.Attributes.Style.Fill.Value connection.Fill = edge.Style.Fill.Value
} }
connection.FontSize = text.FontSize connection.FontSize = text.FontSize
if edge.Attributes.Style.FontSize != nil { if edge.Style.FontSize != nil {
connection.FontSize, _ = strconv.Atoi(edge.Attributes.Style.FontSize.Value) connection.FontSize, _ = strconv.Atoi(edge.Style.FontSize.Value)
} }
if edge.Attributes.Style.Animated != nil { if edge.Style.Animated != nil {
connection.Animated, _ = strconv.ParseBool(edge.Attributes.Style.Animated.Value) connection.Animated, _ = strconv.ParseBool(edge.Style.Animated.Value)
} }
if edge.Attributes.Tooltip != nil { if edge.Tooltip != nil {
connection.Tooltip = edge.Attributes.Tooltip.Value connection.Tooltip = edge.Tooltip.Value
} }
connection.Icon = edge.Attributes.Icon connection.Icon = edge.Icon
if edge.Attributes.Style.Italic != nil { if edge.Style.Italic != nil {
connection.Italic, _ = strconv.ParseBool(edge.Attributes.Style.Italic.Value) connection.Italic, _ = strconv.ParseBool(edge.Style.Italic.Value)
} }
connection.Color = text.GetColor(connection.Italic) connection.Color = text.GetColor(connection.Italic)
if edge.Attributes.Style.FontColor != nil { if edge.Style.FontColor != nil {
connection.Color = edge.Attributes.Style.FontColor.Value connection.Color = edge.Style.FontColor.Value
} }
if edge.Attributes.Style.Bold != nil { if edge.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Attributes.Style.Bold.Value) connection.Bold, _ = strconv.ParseBool(edge.Style.Bold.Value)
} }
if theme != nil && theme.SpecialRules.Mono { if theme != nil && theme.SpecialRules.Mono {
connection.FontFamily = "mono" connection.FontFamily = "mono"
} }
if edge.Attributes.Style.Font != nil { if edge.Style.Font != nil {
connection.FontFamily = edge.Attributes.Style.Font.Value connection.FontFamily = edge.Style.Font.Value
} }
connection.Label = text.Text connection.Label = text.Text
connection.LabelWidth = text.Dimensions.Width connection.LabelWidth = text.Dimensions.Width

View file

@ -16,6 +16,7 @@ import (
"oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter" "oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2sequence" "oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
@ -231,7 +232,7 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil) err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err) assert.JSON(t, nil, err)
err = d2sequence.Layout(ctx, g, d2dagrelayout.DefaultLayout) err = d2sequence.Layout(ctx, g, d2grid.Layout(ctx, g, d2dagrelayout.DefaultLayout))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -1,6 +1,7 @@
package d2graph package d2graph
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"math" "math"
@ -9,6 +10,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2ast"
@ -55,7 +59,6 @@ func NewGraph() *Graph {
Graph: d, Graph: d,
Parent: nil, Parent: nil,
Children: make(map[string]*Object), Children: make(map[string]*Object),
Attributes: &Attributes{},
} }
return d return d
} }
@ -67,6 +70,8 @@ func (g *Graph) RootBoard() *Graph {
return g return g
} }
type LayoutGraph func(context.Context, *Graph) error
// TODO consider having different Scalar types // TODO consider having different Scalar types
// Right now we'll hold any types in Value and just convert, e.g. floats // Right now we'll hold any types in Value and just convert, e.g. floats
type Scalar struct { type Scalar struct {
@ -87,13 +92,10 @@ type Object struct {
ID string `json:"id"` ID string `json:"id"`
IDVal string `json:"id_val"` IDVal string `json:"id_val"`
Map *d2ast.Map `json:"-"` Map *d2ast.Map `json:"-"`
LabelDimensions d2target.TextDimensions `json:"label_dimensions"`
References []Reference `json:"references,omitempty"` References []Reference `json:"references,omitempty"`
*geo.Box `json:"box,omitempty"` *geo.Box `json:"box,omitempty"`
LabelPosition *string `json:"labelPosition,omitempty"` LabelPosition *string `json:"labelPosition,omitempty"`
LabelWidth *int `json:"labelWidth,omitempty"`
LabelHeight *int `json:"labelHeight,omitempty"`
IconPosition *string `json:"iconPosition,omitempty"` IconPosition *string `json:"iconPosition,omitempty"`
Class *d2target.Class `json:"class,omitempty"` Class *d2target.Class `json:"class,omitempty"`
@ -102,20 +104,22 @@ type Object struct {
Children map[string]*Object `json:"-"` Children map[string]*Object `json:"-"`
ChildrenArray []*Object `json:"-"` ChildrenArray []*Object `json:"-"`
Attributes *Attributes `json:"attributes,omitempty"` Attributes `json:"attributes"`
ZIndex int `json:"zIndex"` ZIndex int `json:"zIndex"`
} }
type Attributes struct { type Attributes struct {
Label Scalar `json:"label"` Label Scalar `json:"label"`
LabelDimensions d2target.TextDimensions `json:"labelDimensions"`
Style Style `json:"style"` Style Style `json:"style"`
Icon *url.URL `json:"icon,omitempty"` Icon *url.URL `json:"icon,omitempty"`
Tooltip *Scalar `json:"tooltip,omitempty"` Tooltip *Scalar `json:"tooltip,omitempty"`
Link *Scalar `json:"link,omitempty"` Link *Scalar `json:"link,omitempty"`
Width *Scalar `json:"width,omitempty"` WidthAttr *Scalar `json:"width,omitempty"`
Height *Scalar `json:"height,omitempty"` HeightAttr *Scalar `json:"height,omitempty"`
Top *Scalar `json:"top,omitempty"` Top *Scalar `json:"top,omitempty"`
Left *Scalar `json:"left,omitempty"` Left *Scalar `json:"left,omitempty"`
@ -129,6 +133,35 @@ type Attributes struct {
Direction Scalar `json:"direction"` Direction Scalar `json:"direction"`
Constraint Scalar `json:"constraint"` Constraint Scalar `json:"constraint"`
GridRows *Scalar `json:"gridRows,omitempty"`
GridColumns *Scalar `json:"gridColumns,omitempty"`
GridGap *Scalar `json:"gridGap,omitempty"`
VerticalGap *Scalar `json:"verticalGap,omitempty"`
HorizontalGap *Scalar `json:"horizontalGap,omitempty"`
// These names are attached to the rendered elements in SVG
// so that users can target them however they like outside of D2
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 // TODO references at the root scope should have their Scope set to root graph AST
@ -170,6 +203,14 @@ type Style struct {
Underline *Scalar `json:"underline,omitempty"` Underline *Scalar `json:"underline,omitempty"`
Filled *Scalar `json:"filled,omitempty"` Filled *Scalar `json:"filled,omitempty"`
DoubleBorder *Scalar `json:"doubleBorder,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 { func (s *Style) Apply(key, value string) error {
@ -340,6 +381,14 @@ func (s *Style) Apply(key, value string) error {
return errors.New(`expected "double-border" to be true or false`) return errors.New(`expected "double-border" to be true or false`)
} }
s.DoubleBorder.Value = value 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: default:
return fmt.Errorf("unknown style key: %s", key) return fmt.Errorf("unknown style key: %s", key)
} }
@ -363,7 +412,7 @@ func (l ContainerLevel) LabelSize() int {
func (obj *Object) GetFill() string { func (obj *Object) GetFill() string {
level := int(obj.Level()) level := int(obj.Level())
shape := obj.Attributes.Shape.Value shape := obj.Shape.Value
if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) { if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
return color.N1 return color.N1
@ -442,7 +491,7 @@ func (obj *Object) GetFill() string {
} }
func (obj *Object) GetStroke(dashGapSize interface{}) string { func (obj *Object) GetStroke(dashGapSize interface{}) string {
shape := obj.Attributes.Shape.Value shape := obj.Shape.Value
if strings.EqualFold(shape, d2target.ShapeCode) || if strings.EqualFold(shape, d2target.ShapeCode) ||
strings.EqualFold(shape, d2target.ShapeText) { strings.EqualFold(shape, d2target.ShapeText) {
return color.N1 return color.N1
@ -469,10 +518,10 @@ func (obj *Object) IsContainer() bool {
} }
func (obj *Object) HasOutsideBottomLabel() bool { func (obj *Object) HasOutsideBottomLabel() bool {
if obj == nil || obj.Attributes == nil { if obj == nil {
return false return false
} }
switch obj.Attributes.Shape.Value { switch obj.Shape.Value {
case d2target.ShapeImage, d2target.ShapePerson: case d2target.ShapeImage, d2target.ShapePerson:
return true return true
default: default:
@ -480,6 +529,18 @@ func (obj *Object) HasOutsideBottomLabel() bool {
} }
} }
func (obj *Object) HasLabel() bool {
if obj == nil {
return false
}
switch obj.Shape.Value {
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
return false
default:
return obj.Label.Value != ""
}
}
func (obj *Object) AbsID() string { func (obj *Object) AbsID() string {
if obj.Parent != nil && obj.Parent.ID != "" { if obj.Parent != nil && obj.Parent.ID != "" {
return obj.Parent.AbsID() + "." + obj.ID return obj.Parent.AbsID() + "." + obj.ID
@ -495,12 +556,12 @@ func (obj *Object) AbsIDArray() []string {
} }
func (obj *Object) Text() *d2target.MText { func (obj *Object) Text() *d2target.MText {
isBold := !obj.IsContainer() && obj.Attributes.Shape.Value != "text" isBold := !obj.IsContainer() && obj.Shape.Value != "text"
isItalic := false isItalic := false
if obj.Attributes.Style.Bold != nil && obj.Attributes.Style.Bold.Value == "true" { if obj.Style.Bold != nil && obj.Style.Bold.Value == "true" {
isBold = true isBold = true
} }
if obj.Attributes.Style.Italic != nil && obj.Attributes.Style.Italic.Value == "true" { if obj.Style.Italic != nil && obj.Style.Italic.Value == "true" {
isItalic = true isItalic = true
} }
fontSize := d2fonts.FONT_SIZE_M fontSize := d2fonts.FONT_SIZE_M
@ -510,14 +571,14 @@ func (obj *Object) Text() *d2target.MText {
} }
if obj.OuterSequenceDiagram() == nil { if obj.OuterSequenceDiagram() == nil {
if obj.IsContainer() { if obj.IsContainer() && obj.Shape.Value != "text" {
fontSize = obj.Level().LabelSize() fontSize = obj.Level().LabelSize()
} }
} else { } else {
isBold = false isBold = false
} }
if obj.Attributes.Style.FontSize != nil { if obj.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
} }
// Class and Table objects have Label set to header // Class and Table objects have Label set to header
if obj.Class != nil || obj.SQLTable != nil { if obj.Class != nil || obj.SQLTable != nil {
@ -527,12 +588,12 @@ func (obj *Object) Text() *d2target.MText {
isBold = false isBold = false
} }
return &d2target.MText{ return &d2target.MText{
Text: obj.Attributes.Label.Value, Text: obj.Label.Value,
FontSize: fontSize, FontSize: fontSize,
IsBold: isBold, IsBold: isBold,
IsItalic: isItalic, IsItalic: isItalic,
Language: obj.Attributes.Language, Language: obj.Language,
Shape: obj.Attributes.Shape.Value, Shape: obj.Shape.Value,
Dimensions: obj.LabelDimensions, Dimensions: obj.LabelDimensions,
} }
@ -547,7 +608,7 @@ func (obj *Object) newObject(id string) *Object {
child := &Object{ child := &Object{
ID: id, ID: id,
IDVal: idval, IDVal: idval,
Attributes: &Attributes{ Attributes: Attributes{
Label: Scalar{ Label: Scalar{
Value: idval, Value: idval,
}, },
@ -725,7 +786,7 @@ func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) {
func (obj *Object) ensureChildEdge(ida []string) *Object { func (obj *Object) ensureChildEdge(ida []string) *Object {
for i := range ida { for i := range ida {
switch obj.Attributes.Shape.Value { switch obj.Shape.Value {
case d2target.ShapeClass, d2target.ShapeSQLTable: case d2target.ShapeClass, d2target.ShapeSQLTable:
// This will only be called for connecting edges where we want to truncate to the // This will only be called for connecting edges where we want to truncate to the
// container. // container.
@ -804,23 +865,23 @@ func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *
} }
func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) { func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
shapeType := strings.ToLower(obj.Attributes.Shape.Value) shapeType := strings.ToLower(obj.Shape.Value)
if obj.Attributes.Style.Font != nil { if obj.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[obj.Attributes.Style.Font.Value] f := d2fonts.D2_FONT_TO_FAMILY[obj.Style.Font.Value]
fontFamily = &f fontFamily = &f
} }
var dims *d2target.TextDimensions var dims *d2target.TextDimensions
switch shapeType { switch shapeType {
case d2target.ShapeText: case d2target.ShapeText:
if obj.Attributes.Language == "latex" { if obj.Language == "latex" {
width, height, err := d2latex.Measure(obj.Text().Text) width, height, err := d2latex.Measure(obj.Text().Text)
if err != nil { if err != nil {
return nil, err return nil, err
} }
dims = d2target.NewTextDimensions(width, height) dims = d2target.NewTextDimensions(width, height)
} else if obj.Attributes.Language != "" { } else if obj.Language != "" {
var err error var err error
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily) dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
if err != nil { if err != nil {
@ -837,7 +898,7 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily) dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
} }
if shapeType == d2target.ShapeSQLTable && obj.Attributes.Label.Value == "" { if shapeType == d2target.ShapeSQLTable && obj.Label.Value == "" {
// measure with placeholder text to determine height // measure with placeholder text to determine height
placeholder := *obj.Text() placeholder := *obj.Text()
placeholder.Text = "Table" placeholder.Text = "Table"
@ -866,10 +927,21 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
labelDims.Height += INNER_LABEL_PADDING labelDims.Height += INNER_LABEL_PADDING
} }
switch strings.ToLower(obj.Attributes.Shape.Value) { switch strings.ToLower(obj.Shape.Value) {
default: default:
return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil
case d2target.ShapeText:
w := labelDims.Width
if w < MIN_SHAPE_SIZE {
w = MIN_SHAPE_SIZE
}
h := labelDims.Height
if h < MIN_SHAPE_SIZE {
h = MIN_SHAPE_SIZE
}
return d2target.NewTextDimensions(w, h), nil
case d2target.ShapeImage: case d2target.ShapeImage:
return d2target.NewTextDimensions(128, 128), nil return d2target.NewTextDimensions(128, 128), nil
@ -877,8 +949,8 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
maxWidth := go2.Max(12, labelDims.Width) maxWidth := go2.Max(12, labelDims.Width)
fontSize := d2fonts.FONT_SIZE_L fontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil { if obj.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
} }
for _, f := range obj.Class.Fields { for _, f := range obj.Class.Fields {
@ -923,8 +995,8 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
constraintWidth := 0 constraintWidth := 0
colFontSize := d2fonts.FONT_SIZE_L colFontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil { if obj.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
} }
for i := range obj.SQLTable.Columns { for i := range obj.SQLTable.Columns {
@ -968,16 +1040,22 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
return &dims, nil return &dims, nil
} }
func (obj *Object) OuterNearContainer() *Object {
for obj != nil {
if obj.NearKey != nil {
return obj
}
obj = obj.Parent
}
return nil
}
type Edge struct { type Edge struct {
Index int `json:"index"` Index int `json:"index"`
MinWidth int `json:"minWidth"`
MinHeight int `json:"minHeight"`
SrcTableColumnIndex *int `json:"srcTableColumnIndex,omitempty"` SrcTableColumnIndex *int `json:"srcTableColumnIndex,omitempty"`
DstTableColumnIndex *int `json:"dstTableColumnIndex,omitempty"` DstTableColumnIndex *int `json:"dstTableColumnIndex,omitempty"`
LabelDimensions d2target.TextDimensions `json:"label_dimensions"`
LabelPosition *string `json:"labelPosition,omitempty"` LabelPosition *string `json:"labelPosition,omitempty"`
LabelPercentage *float64 `json:"labelPercentage,omitempty"` LabelPercentage *float64 `json:"labelPercentage,omitempty"`
@ -993,7 +1071,7 @@ type Edge struct {
DstArrowhead *Attributes `json:"dstArrowhead,omitempty"` DstArrowhead *Attributes `json:"dstArrowhead,omitempty"`
References []EdgeReference `json:"references,omitempty"` References []EdgeReference `json:"references,omitempty"`
Attributes *Attributes `json:"attributes,omitempty"` Attributes `json:"attributes,omitempty"`
ZIndex int `json:"zIndex"` ZIndex int `json:"zIndex"`
} }
@ -1007,6 +1085,10 @@ type EdgeReference struct {
ScopeObj *Object `json:"-"` ScopeObj *Object `json:"-"`
} }
func (e *Edge) GetAstEdge() *d2ast.Edge {
return e.References[0].Edge
}
func (e *Edge) GetStroke(dashGapSize interface{}) string { func (e *Edge) GetStroke(dashGapSize interface{}) string {
if dashGapSize != 0.0 { if dashGapSize != 0.0 {
return color.B2 return color.B2
@ -1029,15 +1111,15 @@ func (e *Edge) ArrowString() string {
func (e *Edge) Text() *d2target.MText { func (e *Edge) Text() *d2target.MText {
fontSize := d2fonts.FONT_SIZE_M fontSize := d2fonts.FONT_SIZE_M
if e.Attributes.Style.FontSize != nil { if e.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(e.Attributes.Style.FontSize.Value) fontSize, _ = strconv.Atoi(e.Style.FontSize.Value)
} }
isBold := false isBold := false
if e.Attributes.Style.Bold != nil { if e.Style.Bold != nil {
isBold, _ = strconv.ParseBool(e.Attributes.Style.Bold.Value) isBold, _ = strconv.ParseBool(e.Style.Bold.Value)
} }
return &d2target.MText{ return &d2target.MText{
Text: e.Attributes.Label.Value, Text: e.Label.Value,
FontSize: fontSize, FontSize: fontSize,
IsBold: isBold, IsBold: isBold,
IsItalic: true, IsItalic: true,
@ -1085,7 +1167,7 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
} }
e := &Edge{ e := &Edge{
Attributes: &Attributes{ Attributes: Attributes{
Label: Scalar{ Label: Scalar{
Value: label, Value: label,
}, },
@ -1104,7 +1186,7 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
} }
func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) { func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) {
if src.Attributes.Shape.Value == d2target.ShapeSQLTable { if src.Shape.Value == d2target.ShapeSQLTable {
if src == dst { if src == dst {
// Ignore edge to column inside table. // Ignore edge to column inside table.
return return
@ -1122,7 +1204,7 @@ func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Obj
} }
} }
} }
if dst.Attributes.Shape.Value == d2target.ShapeSQLTable { if dst.Shape.Value == d2target.ShapeSQLTable {
objAbsID := obj.AbsIDArray() objAbsID := obj.AbsIDArray()
dstAbsID := dst.AbsIDArray() dstAbsID := dst.AbsIDArray()
if len(objAbsID)+len(dstID) > len(dstAbsID) { if len(objAbsID)+len(dstID) > len(dstAbsID) {
@ -1179,7 +1261,7 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
} }
if ruler != nil { if ruler != nil {
width, height, err := textmeasure.MeasureMarkdown(t.Text, ruler, fontFamily) width, height, err := textmeasure.MeasureMarkdown(t.Text, ruler, fontFamily, t.FontSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1269,16 +1351,16 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
var desiredWidth int var desiredWidth int
var desiredHeight int var desiredHeight int
if obj.Attributes.Width != nil { if obj.WidthAttr != nil {
desiredWidth, _ = strconv.Atoi(obj.Attributes.Width.Value) desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
} }
if obj.Attributes.Height != nil { if obj.HeightAttr != nil {
desiredHeight, _ = strconv.Atoi(obj.Attributes.Height.Value) desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
} }
dslShape := strings.ToLower(obj.Attributes.Shape.Value) dslShape := strings.ToLower(obj.Shape.Value)
if obj.Attributes.Label.Value == "" && if obj.Label.Value == "" &&
dslShape != d2target.ShapeImage && dslShape != d2target.ShapeImage &&
dslShape != d2target.ShapeSQLTable && dslShape != d2target.ShapeSQLTable &&
dslShape != d2target.ShapeClass { dslShape != d2target.ShapeClass {
@ -1304,11 +1386,12 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue continue
} }
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) { if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
if obj.Attributes.Language != "latex" { if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.Value) obj.Label.Value = strings.ToUpper(obj.Label.Value)
} }
} }
obj.ApplyTextTransform()
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily) labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil { if err != nil {
@ -1316,19 +1399,9 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
} }
obj.LabelDimensions = *labelDims obj.LabelDimensions = *labelDims
switch dslShape {
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
// no labels
default:
if obj.Attributes.Label.Value != "" {
obj.LabelWidth = go2.Pointer(labelDims.Width)
obj.LabelHeight = go2.Pointer(labelDims.Height)
}
}
// if there is a desired width or height, fit to content box without inner label padding for smallest minimum size // 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 && withInnerLabelPadding := desiredWidth == 0 && desiredHeight == 0 &&
dslShape != d2target.ShapeText && obj.Attributes.Label.Value != "" dslShape != d2target.ShapeText && obj.Label.Value != ""
defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims, withInnerLabelPadding) defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims, withInnerLabelPadding)
if err != nil { if err != nil {
return err return err
@ -1360,7 +1433,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
} }
// give shapes with icons extra padding to fit their label // give shapes with icons extra padding to fit their label
if obj.Attributes.Icon != nil { if obj.Icon != nil {
labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING) labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING)
// Evenly pad enough to fit label above icon // Evenly pad enough to fit label above icon
if desiredWidth == 0 { if desiredWidth == 0 {
@ -1374,10 +1447,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
switch shapeType { switch shapeType {
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.IMAGE_TYPE: case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.IMAGE_TYPE:
default: default:
if obj.Attributes.Link != nil { if obj.Link != nil {
paddingX += 32 paddingX += 32
} }
if obj.Attributes.Tooltip != nil { if obj.Tooltip != nil {
paddingX += 32 paddingX += 32
} }
} }
@ -1406,35 +1479,33 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
} }
} }
for _, edge := range g.Edges { for _, edge := range g.Edges {
endpointLabels := []string{} usedFont := fontFamily
if edge.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[edge.Style.Font.Value]
usedFont = &f
}
if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" { if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
endpointLabels = append(endpointLabels, edge.SrcArrowhead.Label.Value) t := edge.Text()
t.Text = edge.SrcArrowhead.Label.Value
dims := GetTextDimensions(mtexts, ruler, t, usedFont)
edge.SrcArrowhead.LabelDimensions = *dims
} }
if edge.DstArrowhead != nil && edge.DstArrowhead.Label.Value != "" { if edge.DstArrowhead != nil && edge.DstArrowhead.Label.Value != "" {
endpointLabels = append(endpointLabels, edge.DstArrowhead.Label.Value)
}
for _, label := range endpointLabels {
t := edge.Text() t := edge.Text()
t.Text = label t.Text = edge.DstArrowhead.Label.Value
dims := GetTextDimensions(mtexts, ruler, t, fontFamily) dims := GetTextDimensions(mtexts, ruler, t, usedFont)
edge.MinWidth += dims.Width edge.DstArrowhead.LabelDimensions = *dims
// Some padding as it's not totally near the end
edge.MinHeight += dims.Height + 5
} }
if edge.Attributes.Label.Value == "" { if edge.Label.Value == "" {
continue continue
} }
if g.Theme != nil && g.Theme.SpecialRules.CapsLock { if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !edge.Style.NoneTextTransform() {
edge.Attributes.Label.Value = strings.ToUpper(edge.Attributes.Label.Value) edge.Label.Value = strings.ToUpper(edge.Label.Value)
}
usedFont := fontFamily
if edge.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[edge.Attributes.Style.Font.Value]
usedFont = &f
} }
edge.ApplyTextTransform()
dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont) dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont)
if dims == nil { if dims == nil {
@ -1442,8 +1513,6 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
} }
edge.LabelDimensions = *dims edge.LabelDimensions = *dims
edge.MinWidth += dims.Width
edge.MinHeight += dims.Height
} }
return nil return nil
} }
@ -1454,10 +1523,11 @@ func (g *Graph) Texts() []*d2target.MText {
capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.Attributes.Label.Value != "" { if obj.Label.Value != "" {
obj.ApplyTextTransform()
text := obj.Text() text := obj.Text()
if capsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) { if capsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
if obj.Attributes.Language != "latex" { if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
text.Text = strings.ToUpper(text.Text) text.Text = strings.ToUpper(text.Text)
} }
} }
@ -1465,8 +1535,8 @@ func (g *Graph) Texts() []*d2target.MText {
} }
if obj.Class != nil { if obj.Class != nil {
fontSize := d2fonts.FONT_SIZE_L fontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil { if obj.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
} }
for _, field := range obj.Class.Fields { for _, field := range obj.Class.Fields {
texts = appendTextDedup(texts, field.Text(fontSize)) texts = appendTextDedup(texts, field.Text(fontSize))
@ -1476,8 +1546,8 @@ func (g *Graph) Texts() []*d2target.MText {
} }
} else if obj.SQLTable != nil { } else if obj.SQLTable != nil {
colFontSize := d2fonts.FONT_SIZE_L colFontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil { if obj.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
} }
for _, column := range obj.SQLTable.Columns { for _, column := range obj.SQLTable.Columns {
for _, t := range column.Texts(colFontSize) { for _, t := range column.Texts(colFontSize) {
@ -1487,9 +1557,10 @@ func (g *Graph) Texts() []*d2target.MText {
} }
} }
for _, edge := range g.Edges { for _, edge := range g.Edges {
if edge.Attributes.Label.Value != "" { if edge.Label.Value != "" {
edge.ApplyTextTransform()
text := edge.Text() text := edge.Text()
if capsLock { if capsLock && !edge.Style.NoneTextTransform() {
text.Text = strings.ToUpper(text.Text) text.Text = strings.ToUpper(text.Text)
} }
texts = appendTextDedup(texts, text) texts = appendTextDedup(texts, text)
@ -1534,6 +1605,13 @@ var SimpleReservedKeywords = map[string]struct{}{
"direction": {}, "direction": {},
"top": {}, "top": {},
"left": {}, "left": {},
"grid-rows": {},
"grid-columns": {},
"grid-gap": {},
"vertical-gap": {},
"horizontal-gap": {},
"class": {},
"classes": {},
} }
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords // ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
@ -1560,6 +1638,7 @@ var StyleKeywords = map[string]struct{}{
"bold": {}, "bold": {},
"italic": {}, "italic": {},
"underline": {}, "underline": {},
"text-transform": {},
// Only for shapes // Only for shapes
"shadow": {}, "shadow": {},
@ -1597,6 +1676,8 @@ var FillPatterns = []string{
"paper", "paper",
} }
var textTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
// BoardKeywords contains the keywords that create new boards. // BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{ var BoardKeywords = map[string]struct{}{
"layers": {}, "layers": {},

16
d2graph/grid_diagram.go Normal file
View file

@ -0,0 +1,16 @@
package d2graph
func (obj *Object) IsGridDiagram() bool {
return obj != nil &&
(obj.GridRows != nil || obj.GridColumns != nil)
}
func (obj *Object) ClosestGridDiagram() *Object {
if obj == nil {
return nil
}
if obj.IsGridDiagram() {
return obj
}
return obj.Parent.ClosestGridDiagram()
}

View file

@ -3,7 +3,7 @@ package d2graph
import "oss.terrastruct.com/d2/d2target" import "oss.terrastruct.com/d2/d2target"
func (obj *Object) IsSequenceDiagram() bool { func (obj *Object) IsSequenceDiagram() bool {
return obj != nil && obj.Attributes != nil && obj.Attributes.Shape.Value == d2target.ShapeSequenceDiagram return obj != nil && obj.Shape.Value == d2target.ShapeSequenceDiagram
} }
func (obj *Object) OuterSequenceDiagram() *Object { func (obj *Object) OuterSequenceDiagram() *Object {

View file

@ -265,47 +265,42 @@ func CompareSerializedObject(obj, other *Object) error {
} }
} }
if obj.Attributes != nil && other.Attributes == nil { if d2target.IsShape(obj.Shape.Value) != d2target.IsShape(other.Shape.Value) {
return fmt.Errorf("other should have attributes")
} else if obj.Attributes == nil && other.Attributes != nil {
return fmt.Errorf("other should not have attributes")
} else if obj.Attributes != nil {
if d2target.IsShape(obj.Attributes.Shape.Value) != d2target.IsShape(other.Attributes.Shape.Value) {
return fmt.Errorf( return fmt.Errorf(
"shapes differ: obj=%s, other=%s", "shapes differ: obj=%s, other=%s",
obj.Attributes.Shape.Value, obj.Shape.Value,
other.Attributes.Shape.Value, other.Shape.Value,
) )
} }
if obj.Attributes.Icon == nil && other.Attributes.Icon != nil { if obj.Icon == nil && other.Icon != nil {
return fmt.Errorf("other does not have an icon") return fmt.Errorf("other does not have an icon")
} else if obj.Attributes.Icon != nil && other.Attributes.Icon == nil { } else if obj.Icon != nil && other.Icon == nil {
return fmt.Errorf("obj does not have an icon") return fmt.Errorf("obj does not have an icon")
} }
if obj.Attributes.Direction.Value != other.Attributes.Direction.Value { if obj.Direction.Value != other.Direction.Value {
return fmt.Errorf( return fmt.Errorf(
"directions differ: obj=%s, other=%s", "directions differ: obj=%s, other=%s",
obj.Attributes.Direction.Value, obj.Direction.Value,
other.Attributes.Direction.Value, other.Direction.Value,
) )
} }
if obj.Attributes.Label.Value != other.Attributes.Label.Value { if obj.Label.Value != other.Label.Value {
return fmt.Errorf( return fmt.Errorf(
"labels differ: obj=%s, other=%s", "labels differ: obj=%s, other=%s",
obj.Attributes.Label.Value, obj.Label.Value,
other.Attributes.Label.Value, other.Label.Value,
) )
} }
if obj.Attributes.NearKey != nil { if obj.NearKey != nil {
if other.Attributes.NearKey == nil { if other.NearKey == nil {
return fmt.Errorf("other does not have near") return fmt.Errorf("other does not have near")
} }
objKey := strings.Join(Key(obj.Attributes.NearKey), ".") objKey := strings.Join(Key(obj.NearKey), ".")
deserKey := strings.Join(Key(other.Attributes.NearKey), ".") deserKey := strings.Join(Key(other.NearKey), ".")
if objKey != deserKey { if objKey != deserKey {
return fmt.Errorf( return fmt.Errorf(
"near differs: obj=%s, other=%s", "near differs: obj=%s, other=%s",
@ -313,9 +308,24 @@ func CompareSerializedObject(obj, other *Object) error {
deserKey, deserKey,
) )
} }
} else if other.Attributes.NearKey != nil { } else if other.NearKey != nil {
return fmt.Errorf("other should not have near") return fmt.Errorf("other should not have near")
} }
if obj.LabelDimensions.Width != other.LabelDimensions.Width {
return fmt.Errorf(
"label width differs: obj=%d, other=%d",
obj.LabelDimensions.Width,
other.LabelDimensions.Width,
)
}
if obj.LabelDimensions.Height != other.LabelDimensions.Height {
return fmt.Errorf(
"label height differs: obj=%d, other=%d",
obj.LabelDimensions.Height,
other.LabelDimensions.Height,
)
} }
if obj.SQLTable == nil && other.SQLTable != nil { if obj.SQLTable == nil && other.SQLTable != nil {
@ -334,36 +344,6 @@ func CompareSerializedObject(obj, other *Object) error {
} }
} }
if obj.LabelWidth != nil {
if other.LabelWidth == nil {
return fmt.Errorf("other does not have a label width")
}
if *obj.LabelWidth != *other.LabelWidth {
return fmt.Errorf(
"label widths differ: obj=%d, other=%d",
*obj.LabelWidth,
*other.LabelWidth,
)
}
} else if other.LabelWidth != nil {
return fmt.Errorf("other should not have label width")
}
if obj.LabelHeight != nil {
if other.LabelHeight == nil {
return fmt.Errorf("other does not have a label height")
}
if *obj.LabelHeight != *other.LabelHeight {
return fmt.Errorf(
"label heights differ: obj=%d, other=%d",
*obj.LabelHeight,
*other.LabelHeight,
)
}
} else if other.LabelHeight != nil {
return fmt.Errorf("other should not have label height")
}
return nil return nil
} }
@ -408,27 +388,11 @@ func CompareSerializedEdge(edge, other *Edge) error {
) )
} }
if edge.MinWidth != other.MinWidth { if edge.Label.Value != other.Label.Value {
return fmt.Errorf(
"min width differs: edge=%d, other=%d",
edge.MinWidth,
other.MinWidth,
)
}
if edge.MinHeight != other.MinHeight {
return fmt.Errorf(
"min height differs: edge=%d, other=%d",
edge.MinHeight,
other.MinHeight,
)
}
if edge.Attributes.Label.Value != other.Attributes.Label.Value {
return fmt.Errorf( return fmt.Errorf(
"labels differ: edge=%s, other=%s", "labels differ: edge=%s, other=%s",
edge.Attributes.Label.Value, edge.Label.Value,
other.Attributes.Label.Value, other.Label.Value,
) )
} }
@ -442,7 +406,7 @@ func CompareSerializedEdge(edge, other *Edge) error {
if edge.LabelDimensions.Height != other.LabelDimensions.Height { if edge.LabelDimensions.Height != other.LabelDimensions.Height {
return fmt.Errorf( return fmt.Errorf(
"label hieght differs: edge=%d, other=%d", "label height differs: edge=%d, other=%d",
edge.LabelDimensions.Height, edge.LabelDimensions.Height,
other.LabelDimensions.Height, other.LabelDimensions.Height,
) )

View file

@ -22,62 +22,58 @@ func Compile(ast *d2ast.Map) (*Map, error) {
m.initRoot() m.initRoot()
m.parent.(*Field).References[0].Context.Scope = ast m.parent.(*Field).References[0].Context.Scope = ast
c.compileMap(m, ast) c.compileMap(m, ast)
c.compileScenarios(m) c.compileClasses(m)
c.compileSteps(m)
if !c.err.Empty() { if !c.err.Empty() {
return nil, c.err return nil, c.err
} }
return m, nil return m, nil
} }
func (c *compiler) compileScenarios(m *Map) { func (c *compiler) compileClasses(m *Map) {
scenariosf := m.GetField("scenarios") classes := m.GetField("classes")
if scenariosf == nil { if classes == nil || classes.Map() == nil {
return
}
scenarios := scenariosf.Map()
if scenarios == nil {
return return
} }
for _, sf := range scenarios.Fields { layersField := m.GetField("layers")
if sf.Map() == nil || sf.Primary() != nil { if layersField == nil {
c.errorf(sf.References[0].Context.Key, "invalid scenario") return
}
layers := layersField.Map()
if layers == nil {
return
}
for _, lf := range layers.Fields {
if lf.Map() == nil || lf.Primary() != nil {
c.errorf(lf.References[0].Context.Key, "invalid layer")
continue continue
} }
base := m.CopyBase(sf) l := lf.Map()
OverlayMap(base, sf.Map()) lClasses := l.GetField("classes")
sf.Composite = base
c.compileScenarios(sf.Map()) if lClasses == nil {
c.compileSteps(sf.Map()) lClasses = classes.Copy(l).(*Field)
l.Fields = append(l.Fields, lClasses)
} else {
base := classes.Copy(l).(*Field)
OverlayMap(base.Map(), lClasses.Map())
l.DeleteField("classes")
l.Fields = append(l.Fields, base)
}
c.compileClasses(l)
} }
} }
func (c *compiler) compileSteps(m *Map) { func (c *compiler) overlay(base *Map, f *Field) {
stepsf := m.GetField("steps") if f.Map() == nil || f.Primary() != nil {
if stepsf == nil { c.errorf(f.References[0].Context.Key, "invalid %s", NodeBoardKind(f))
return return
} }
steps := stepsf.Map() base = base.CopyBase(f)
if steps == nil { OverlayMap(base, f.Map())
return f.Composite = base
}
for i, sf := range steps.Fields {
if sf.Map() == nil || sf.Primary() != nil {
c.errorf(sf.References[0].Context.Key, "invalid step")
break
}
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) { func (c *compiler) compileMap(dst *Map, ast *d2ast.Map) {
@ -128,7 +124,27 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
parent: f, parent: f,
} }
} }
switch NodeBoardKind(f) {
case BoardScenario:
c.overlay(ParentBoard(f).Map(), f)
case BoardStep:
stepsMap := ParentMap(f)
for i := range stepsMap.Fields {
if stepsMap.Fields[i] == f {
if i == 0 {
c.overlay(ParentBoard(f).Map(), f)
} else {
c.overlay(stepsMap.Fields[i-1].Map(), f)
}
break
}
}
}
c.compileMap(f.Map(), refctx.Key.Value.Map) c.compileMap(f.Map(), refctx.Key.Value.Map)
switch NodeBoardKind(f) {
case BoardScenario, BoardStep:
c.compileClasses(f.Map())
}
} else if refctx.Key.Value.ScalarBox().Unbox() != nil { } else if refctx.Key.Value.ScalarBox().Unbox() != nil {
// If the link is a board, we need to transform it into an absolute path. // If the link is a board, we need to transform it into an absolute path.
if f.Name == "link" { if f.Name == "link" {

View file

@ -19,6 +19,7 @@ func TestCompile(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("fields", testCompileFields) t.Run("fields", testCompileFields)
t.Run("classes", testCompileClasses)
t.Run("edges", testCompileEdges) t.Run("edges", testCompileEdges)
t.Run("layers", testCompileLayers) t.Run("layers", testCompileLayers)
t.Run("scenarios", testCompileScenarios) t.Run("scenarios", testCompileScenarios)
@ -101,10 +102,12 @@ func makeScalar(v interface{}) *d2ir.Scalar {
bv := &big.Rat{} bv := &big.Rat{}
bv.SetFloat64(v) bv.SetFloat64(v)
s.Value = &d2ast.Number{ s.Value = &d2ast.Number{
Raw: fmt.Sprint(v),
Value: bv, Value: bv,
} }
case int: case int:
s.Value = &d2ast.Number{ s.Value = &d2ast.Number{
Raw: fmt.Sprint(v),
Value: big.NewRat(int64(v), 1), Value: big.NewRat(int64(v), 1),
} }
case string: case string:
@ -379,6 +382,20 @@ scenarios: {
assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.quiche") assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.quiche")
}, },
}, },
{
name: "edge",
run: func(t testing.TB) {
m, err := compile(t, `a -> b
scenarios: {
1: {
(a -> b)[0].style.opacity: 0.1
}
}`)
assert.Success(t, err)
assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
},
},
} }
runa(t, tca) runa(t, tca)
} }
@ -431,9 +448,8 @@ scenarios: {
shape: sql_table shape: sql_table
hey: int {constraint: primary_key} hey: int {constraint: primary_key}
}`) }`)
assert.ErrorString(t, err, `TestCompile/steps/steps_panic.d2:6:3: invalid scenario assert.ErrorString(t, err, `TestCompile/steps/steps_panic.d2:3:3: invalid step
TestCompile/steps/steps_panic.d2:7:3: invalid scenario TestCompile/steps/steps_panic.d2:7:3: invalid scenario`)
TestCompile/steps/steps_panic.d2:2:3: invalid step`)
}, },
}, },
{ {
@ -490,3 +506,154 @@ steps: {
} }
runa(t, tca) runa(t, tca)
} }
func testCompileClasses(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "basic",
run: func(t testing.TB) {
_, err := compile(t, `x
classes: {
mango: {
style.fill: orange
}
}
`)
assert.Success(t, err)
},
},
{
name: "nonroot",
run: func(t testing.TB) {
_, err := compile(t, `x: {
classes: {
mango: {
style.fill: orange
}
}
}
`)
assert.ErrorString(t, err, `TestCompile/classes/nonroot.d2:2:3: classes is only allowed at a board root`)
},
},
{
name: "merge",
run: func(t testing.TB) {
m, err := compile(t, `classes: {
mango: {
style.fill: orange
width: 10
}
}
layers: {
hawaii: {
classes: {
mango: {
width: 9000
}
}
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 3, 0, nil, "layers.hawaii.classes.mango")
assertQuery(t, m, 0, 0, "orange", "layers.hawaii.classes.mango.style.fill")
assertQuery(t, m, 0, 0, 9000, "layers.hawaii.classes.mango.width")
},
},
{
name: "nested",
run: func(t testing.TB) {
m, err := compile(t, `classes: {
mango: {
style.fill: orange
}
}
layers: {
hawaii: {
layers: {
maui: {
x
}
}
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 3, 0, nil, "layers.hawaii.classes")
assertQuery(t, m, 3, 0, nil, "layers.hawaii.layers.maui.classes")
},
},
{
name: "inherited",
run: func(t testing.TB) {
m, err := compile(t, `classes: {
mango: {
style.fill: orange
}
}
scenarios: {
hawaii: {
steps: {
1: {
classes: {
cherry: {
style.fill: red
}
}
x
}
2: {
y
}
3: {
classes: {
cherry: {
style.fill: blue
}
}
y
}
4: {
layers: {
deep: {
x
}
}
x
}
}
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 3, 0, nil, "scenarios.hawaii.classes")
assertQuery(t, m, 2, 0, nil, "scenarios.hawaii.steps.2.classes.mango")
assertQuery(t, m, 2, 0, nil, "scenarios.hawaii.steps.2.classes.cherry")
assertQuery(t, m, 0, 0, "blue", "scenarios.hawaii.steps.4.classes.cherry.style.fill")
assertQuery(t, m, 0, 0, "blue", "scenarios.hawaii.steps.4.layers.deep.classes.cherry.style.fill")
},
},
{
name: "layer-modify",
run: func(t testing.TB) {
m, err := compile(t, `classes: {
orb: {
style.fill: yellow
}
}
layers: {
x: {
classes.orb.style.stroke: red
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 0, 0, "yellow", "layers.x.classes.orb.style.fill")
assertQuery(t, m, 0, 0, "red", "layers.x.classes.orb.style.stroke")
},
},
}
runa(t, tca)
}

View file

@ -601,6 +601,18 @@ func (m *Map) EdgeCountRecursive() int {
return acc return acc
} }
func (m *Map) GetClassMap(name string) *Map {
root := RootMap(m)
classes := root.Map().GetField("classes")
if classes != nil && classes.Map() != nil {
class := classes.Map().GetField(name)
if class != nil && class.Map() != nil {
return class.Map()
}
}
return nil
}
func (m *Map) GetField(ida ...string) *Field { func (m *Map) GetField(ida ...string) *Field {
for len(ida) > 0 && ida[0] == "_" { for len(ida) > 0 && ida[0] == "_" {
m = ParentMap(m) m = ParentMap(m)
@ -663,6 +675,10 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext) (*Field,
return nil, d2parser.Errorf(kp.Path[i].Unbox(), `parent "_" can only be used in the beginning of paths, e.g. "_.x"`) return nil, d2parser.Errorf(kp.Path[i].Unbox(), `parent "_" can only be used in the beginning of paths, e.g. "_.x"`)
} }
if head == "classes" && NodeBoardKind(m) == "" {
return nil, d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head)
}
if findBoardKeyword(head) != -1 && NodeBoardKind(m) == "" { if findBoardKeyword(head) != -1 && NodeBoardKind(m) == "" {
return nil, d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head) return nil, d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head)
} }
@ -935,6 +951,13 @@ func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext
} }
} }
func RootMap(m *Map) *Map {
if m.Root() {
return m
}
return RootMap(ParentMap(m))
}
func ParentMap(n Node) *Map { func ParentMap(n Node) *Map {
for { for {
n = n.Parent() n = n.Parent()
@ -1154,3 +1177,27 @@ func (m *Map) Equal(n2 Node) bool {
return true return true
} }
func (m *Map) InClass(key *d2ast.Key) bool {
classes := m.Map().GetField("classes")
if classes == nil || classes.Map() == nil {
return false
}
for _, class := range classes.Map().Fields {
if class.Map() == nil {
continue
}
classF := class.Map().GetField(key.Key.IDA()...)
if classF == nil {
continue
}
for _, ref := range classF.References {
if ref.Context.Key == key {
return true
}
}
}
return false
}

View file

@ -93,7 +93,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}, },
} }
isHorizontal := false isHorizontal := false
switch g.Root.Attributes.Direction.Value { switch g.Root.Direction.Value {
case "down": case "down":
rootAttrs.rankdir = "TB" rootAttrs.rankdir = "TB"
case "right": case "right":
@ -114,13 +114,13 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if len(obj.ChildrenArray) == 0 || obj.Parent == g.Root { if len(obj.ChildrenArray) == 0 || obj.Parent == g.Root {
continue continue
} }
if obj.LabelHeight != nil { if obj.HasLabel() {
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight+label.PADDING) maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, obj.LabelDimensions.Height+label.PADDING)
} }
if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage { if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height)) contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value] shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Shape.Value]
s := shape.NewShape(shapeType, contentBox) s := shape.NewShape(shapeType, contentBox)
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft)) iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
// Since dagre container labels are pushed up, we don't want a child container to collide // Since dagre container labels are pushed up, we don't want a child container to collide
@ -160,12 +160,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
idToObj[id] = obj idToObj[id] = obj
height := obj.Height height := obj.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil { if obj.HasLabel() {
if obj.HasOutsideBottomLabel() || obj.Attributes.Icon != nil { if obj.HasOutsideBottomLabel() || obj.Icon != nil {
height += float64(*obj.LabelHeight) + label.PADDING height += float64(obj.LabelDimensions.Height) + label.PADDING
} }
if len(obj.ChildrenArray) > 0 { if len(obj.ChildrenArray) > 0 {
height += float64(*obj.LabelHeight) + label.PADDING height += float64(obj.LabelDimensions.Height) + label.PADDING
} }
} }
loadScript += generateAddNodeLine(id, int(obj.Width), int(height)) loadScript += generateAddNodeLine(id, int(obj.Width), int(height))
@ -189,7 +189,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// We want to leave some gap between multiple edges // We want to leave some gap between multiple edges
if numEdges > 1 { if numEdges > 1 {
switch g.Root.Attributes.Direction.Value { switch g.Root.Direction.Value {
case "down", "up", "": case "down", "up", "":
width += EDGE_LABEL_GAP width += EDGE_LABEL_GAP
case "left", "right": case "left", "right":
@ -235,20 +235,20 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
obj.Width = dn.Width obj.Width = dn.Width
obj.Height = dn.Height obj.Height = dn.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil { if obj.HasLabel() {
if len(obj.ChildrenArray) > 0 { if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else if obj.HasOutsideBottomLabel() { } else if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
// remove the extra height we added to the node when passing to dagre // remove the extra height we added to the node when passing to dagre
obj.Height -= float64(*obj.LabelHeight) + label.PADDING obj.Height -= float64(obj.LabelDimensions.Height) + label.PADDING
} else if obj.Attributes.Icon != nil { } else if obj.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else { } else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
} }
if obj.Attributes.Icon != nil { if obj.Icon != nil {
if len(obj.ChildrenArray) > 0 { if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft)) obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight)) obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
@ -307,14 +307,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.LabelHeight == nil || len(obj.ChildrenArray) == 0 { if !obj.HasLabel() || len(obj.ChildrenArray) == 0 {
continue continue
} }
// usually you don't want to take away here more than what was added, which is the label height // usually you don't want to take away here more than what was added, which is the label height
// however, if the label height is more than the ranksep/2, we'll have no padding around children anymore // however, if the label height is more than the ranksep/2, we'll have no padding around children anymore
// so cap the amount taken off at ranksep/2 // so cap the amount taken off at ranksep/2
subtract := float64(go2.Min(rootAttrs.ranksep/2, *obj.LabelHeight+label.PADDING)) subtract := float64(go2.Min(rootAttrs.ranksep/2, obj.LabelDimensions.Height+label.PADDING))
obj.Height -= subtract obj.Height -= subtract
@ -373,7 +373,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// Don't move src points on side of container // Don't move src points on side of container
if almostEqual(e.Route[0].X, obj.TopLeft.X) || almostEqual(e.Route[0].X, obj.TopLeft.X+obj.Width) { if almostEqual(e.Route[0].X, obj.TopLeft.X) || almostEqual(e.Route[0].X, obj.TopLeft.X+obj.Width) {
// Unless the dst is also on a container // Unless the dst is also on a container
if e.Dst.LabelHeight == nil || len(e.Dst.ChildrenArray) <= 0 { if !e.Dst.HasLabel() || len(e.Dst.ChildrenArray) <= 0 {
continue continue
} }
} }
@ -453,18 +453,18 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
} }
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box) srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.Box) dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Shape.Value)], edge.Dst.Box)
// trace the edge to the specific shape's border // trace the edge to the specific shape's border
points[startIndex] = shape.TraceToShapeBorder(srcShape, start, points[startIndex+1]) points[startIndex] = shape.TraceToShapeBorder(srcShape, start, points[startIndex+1])
// if an edge to a container runs into its label, stop the edge at the label instead // if an edge to a container runs into its label, stop the edge at the label instead
overlapsContainerLabel := false overlapsContainerLabel := false
if edge.Dst.IsContainer() && edge.Dst.Attributes.Label.Value != "" && !dstShape.Is(shape.TEXT_TYPE) { if edge.Dst.IsContainer() && edge.Dst.Label.Value != "" && !dstShape.Is(shape.TEXT_TYPE) {
// assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label // assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label
labelWidth := float64(*edge.Dst.LabelWidth) labelWidth := float64(edge.Dst.LabelDimensions.Width)
labelHeight := float64(*edge.Dst.LabelHeight) labelHeight := float64(edge.Dst.LabelDimensions.Height)
labelTL := label.Position(*edge.Dst.LabelPosition). labelTL := label.Position(*edge.Dst.LabelPosition).
GetPointOnBox(edge.Dst.Box, label.PADDING, labelWidth, labelHeight) GetPointOnBox(edge.Dst.Box, label.PADDING, labelWidth, labelHeight)
@ -514,7 +514,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
edge.Route = path edge.Route = path
// compile needs to assign edge label positions // compile needs to assign edge label positions
if edge.Attributes.Label.Value != "" { if edge.Label.Value != "" {
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
} }

View file

@ -109,6 +109,8 @@ type elkOpts struct {
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"` ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"` ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
SelfLoopDistribution string `json:"elk.layered.edgeRouting.selfLoopDistribution,omitempty"`
NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"` NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
ContentAlignment string `json:"elk.contentAlignment,omitempty"` ContentAlignment string `json:"elk.contentAlignment,omitempty"`
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"` NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
@ -159,7 +161,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}, },
}, },
} }
switch g.Root.Attributes.Direction.Value { if elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
// +5 for a tiny bit of padding
elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(g.Root, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
}
switch g.Root.Direction.Value {
case "down": case "down":
elkGraph.LayoutOptions.Direction = "DOWN" elkGraph.LayoutOptions.Direction = "DOWN"
case "up": case "up":
@ -198,7 +204,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
} }
if incoming >= 2 || outgoing >= 2 { if incoming >= 2 || outgoing >= 2 {
switch g.Root.Attributes.Direction.Value { switch g.Root.Direction.Value {
case "right", "left": case "right", "left":
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing) obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
default: default:
@ -208,11 +214,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
height := obj.Height height := obj.Height
width := obj.Width width := obj.Width
if obj.LabelWidth != nil && obj.LabelHeight != nil { if obj.HasLabel() {
if obj.HasOutsideBottomLabel() || obj.Attributes.Icon != nil { if obj.HasOutsideBottomLabel() || obj.Icon != nil {
height += float64(*obj.LabelHeight) + label.PADDING height += float64(obj.LabelDimensions.Height) + label.PADDING
} }
width = go2.Max(width, float64(*obj.LabelWidth)) width = go2.Max(width, float64(obj.LabelDimensions.Width))
} }
n := &ELKNode{ n := &ELKNode{
@ -239,6 +245,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Padding: opts.Padding, Padding: opts.Padding,
}, },
} }
if n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(obj, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
}
switch elkGraph.LayoutOptions.Direction { switch elkGraph.LayoutOptions.Direction {
case "DOWN", "UP": case "DOWN", "UP":
@ -249,14 +258,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if n.LayoutOptions.Padding == DefaultOpts.Padding { if n.LayoutOptions.Padding == DefaultOpts.Padding {
labelHeight := 0 labelHeight := 0
if obj.LabelHeight != nil { if obj.HasLabel() {
labelHeight = *obj.LabelHeight + label.PADDING labelHeight = obj.LabelDimensions.Height + label.PADDING
} }
n.Height += 100 + float64(labelHeight) n.Height += 100 + float64(labelHeight)
n.Width += 100 n.Width += 100
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height)) contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value] shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Shape.Value]
s := shape.NewShape(shapeType, contentBox) s := shape.NewShape(shapeType, contentBox)
paddingTop := n.Height - s.GetInnerBox().Height paddingTop := n.Height - s.GetInnerBox().Height
@ -264,7 +273,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
n.Width -= 100 n.Width -= 100
iconHeight := 0 iconHeight := 0
if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage { if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
iconHeight = d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft)) + label.PADDING*2 iconHeight = d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft)) + label.PADDING*2
} }
@ -277,15 +286,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
} else { } else {
n.LayoutOptions = &elkOpts{ n.LayoutOptions = &elkOpts{
// Margins: "[top=100,left=100,bottom=100,right=100]", SelfLoopDistribution: "EQUALLY",
} }
} }
if obj.LabelWidth != nil && obj.LabelHeight != nil { if obj.HasLabel() {
n.Labels = append(n.Labels, &ELKLabel{ n.Labels = append(n.Labels, &ELKLabel{
Text: obj.Attributes.Label.Value, Text: obj.Label.Value,
Width: float64(*obj.LabelWidth), Width: float64(obj.LabelDimensions.Width),
Height: float64(*obj.LabelHeight), Height: float64(obj.LabelDimensions.Height),
}) })
} }
@ -303,9 +312,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Sources: []string{edge.Src.AbsID()}, Sources: []string{edge.Src.AbsID()},
Targets: []string{edge.Dst.AbsID()}, Targets: []string{edge.Dst.AbsID()},
} }
if edge.Attributes.Label.Value != "" { if edge.Label.Value != "" {
e.Labels = append(e.Labels, &ELKLabel{ e.Labels = append(e.Labels, &ELKLabel{
Text: edge.Attributes.Label.Value, Text: edge.Label.Value,
Width: float64(edge.LabelDimensions.Width), Width: float64(edge.LabelDimensions.Width),
Height: float64(edge.LabelDimensions.Height), Height: float64(edge.LabelDimensions.Height),
LayoutOptions: &elkOpts{ LayoutOptions: &elkOpts{
@ -391,19 +400,19 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
obj.Width = n.Width obj.Width = n.Width
obj.Height = n.Height obj.Height = n.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil { if obj.HasLabel() {
if len(obj.ChildrenArray) > 0 { if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else if obj.HasOutsideBottomLabel() { } else if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
obj.Height -= float64(*obj.LabelHeight) + label.PADDING obj.Height -= float64(obj.LabelDimensions.Height) + label.PADDING
} else if obj.Attributes.Icon != nil { } else if obj.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else { } else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
} }
if obj.Attributes.Icon != nil { if obj.Icon != nil {
if len(obj.ChildrenArray) > 0 { if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft)) obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
obj.LabelPosition = go2.Pointer(string(label.InsideTopRight)) obj.LabelPosition = go2.Pointer(string(label.InsideTopRight))
@ -444,14 +453,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
startIndex, endIndex := 0, len(points)-1 startIndex, endIndex := 0, len(points)-1
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box) srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.Box) dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Shape.Value)], edge.Dst.Box)
// trace the edge to the specific shape's border // trace the edge to the specific shape's border
points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1]) points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1])
points[endIndex] = shape.TraceToShapeBorder(dstShape, points[endIndex], points[endIndex-1]) points[endIndex] = shape.TraceToShapeBorder(dstShape, points[endIndex], points[endIndex-1])
if edge.Attributes.Label.Value != "" { if edge.Label.Value != "" {
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
@ -519,7 +528,7 @@ func deleteBends(g *d2graph.Graph) {
newStart = geo.NewPoint(end.X, start.Y) newStart = geo.NewPoint(end.X, start.Y)
} }
endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Attributes.Shape.Value)], endpoint.Box) endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Shape.Value)], endpoint.Box)
newStart = shape.TraceToShapeBorder(endpointShape, newStart, end) newStart = shape.TraceToShapeBorder(endpointShape, newStart, end)
// Check that the new segment doesn't collide with anything new // Check that the new segment doesn't collide with anything new
@ -725,3 +734,20 @@ func countEdgeIntersects(g *d2graph.Graph, sEdge *d2graph.Edge, s geo.Segment) (
} }
return crossingsCount, overlapsCount, closeOverlapsCount, touchingCount return crossingsCount, overlapsCount, closeOverlapsCount, touchingCount
} }
func childrenMaxSelfLoop(parent *d2graph.Object, isWidth bool) int {
max := 0
for _, ch := range parent.Children {
for _, e := range parent.Graph.Edges {
if e.Src == e.Dst && e.Src == ch && e.Label.Value != "" {
if isWidth {
max = go2.Max(max, e.LabelDimensions.Width)
} else {
max = go2.Max(max, e.LabelDimensions.Height)
}
}
}
}
return max
}

View file

@ -0,0 +1,116 @@
package d2grid
import (
"strconv"
"strings"
"oss.terrastruct.com/d2/d2graph"
)
type gridDiagram struct {
root *d2graph.Object
objects []*d2graph.Object
rows int
columns int
// if true, place objects left to right along rows
// if false, place objects top to bottom along columns
rowDirected bool
width float64
height float64
verticalGap int
horizontalGap int
}
func newGridDiagram(root *d2graph.Object) *gridDiagram {
gd := gridDiagram{
root: root,
objects: root.ChildrenArray,
verticalGap: DEFAULT_GAP,
horizontalGap: DEFAULT_GAP,
}
if root.GridRows != nil {
gd.rows, _ = strconv.Atoi(root.GridRows.Value)
}
if root.GridColumns != nil {
gd.columns, _ = strconv.Atoi(root.GridColumns.Value)
}
if gd.rows != 0 && gd.columns != 0 {
// . row-directed column-directed
// . ┌───────┐ ┌───────┐
// . │ a b c │ │ a d g │
// . │ d e f │ │ b e h │
// . │ g h i │ │ c f i │
// . └───────┘ └───────┘
// if keyword rows is first, make it row-directed, if columns is first it is column-directed
if root.GridRows.MapKey.Range.Before(root.GridColumns.MapKey.Range) {
gd.rowDirected = true
}
// rows and columns specified, but we want to continue naturally if user enters more objects
// e.g. 2 rows, 3 columns specified + g added: │ with 3 columns, 2 rows:
// . original add row add column │ original add row add column
// . ┌───────┐ ┌───────┐ ┌─────────┐ │ ┌───────┐ ┌───────┐ ┌─────────┐
// . │ a b c │ │ a b c │ │ a b c d │ │ │ a c e │ │ a d g │ │ a c e g │
// . │ d e f │ │ d e f │ │ e f g │ │ │ b d f │ │ b e │ │ b d f │
// . └───────┘ │ g │ └─────────┘ │ └───────┘ │ c f │ └─────────┘
// . └───────┘ ▲ │ └───────┘ ▲
// . ▲ └─existing objects modified│ ▲ └─existing columns preserved
// . └─existing rows preserved │ └─existing objects modified
capacity := gd.rows * gd.columns
for capacity < len(gd.objects) {
if gd.rowDirected {
gd.rows++
capacity += gd.columns
} else {
gd.columns++
capacity += gd.rows
}
}
} else if gd.columns == 0 {
gd.rowDirected = true
// we can only make N rows with N objects
if len(gd.objects) < gd.rows {
gd.rows = len(gd.objects)
}
} else {
if len(gd.objects) < gd.columns {
gd.columns = len(gd.objects)
}
}
// grid gap sets both, but can be overridden
if root.GridGap != nil {
gd.verticalGap, _ = strconv.Atoi(root.GridGap.Value)
gd.horizontalGap = gd.verticalGap
}
if root.VerticalGap != nil {
gd.verticalGap, _ = strconv.Atoi(root.VerticalGap.Value)
}
if root.HorizontalGap != nil {
gd.horizontalGap, _ = strconv.Atoi(root.HorizontalGap.Value)
}
return &gd
}
func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.objects {
obj.TopLeft.X += dx
obj.TopLeft.Y += dy
}
}
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range gd.objects {
obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
graph.Objects = append(graph.Objects, gd.objects...)
}

583
d2layouts/d2grid/layout.go Normal file
View file

@ -0,0 +1,583 @@
package d2grid
import (
"context"
"math"
"sort"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
)
const (
CONTAINER_PADDING = 60
DEFAULT_GAP = 40
)
// Layout runs the grid layout on containers with rows/columns
// Note: children are not allowed edges or descendants
//
// 1. Traverse graph from root, skip objects with no rows/columns
// 2. Construct a grid with the container children
// 3. Remove the children from the main graph
// 4. Run grid layout
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (without grid children)
// 7. Put grid children back in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
return func(ctx context.Context, g *d2graph.Graph) error {
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g)
if err != nil {
return err
}
if g.Root.IsGridDiagram() && len(g.Root.ChildrenArray) != 0 {
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
cleanup(g, gridDiagrams, objectOrder)
return nil
}
}
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
toRemove := make(map[*d2graph.Object]struct{})
gridDiagrams = make(map[string]*gridDiagram)
if len(g.Objects) > 0 {
queue := make([]*d2graph.Object, 1, len(g.Objects))
queue[0] = g.Root
for len(queue) > 0 {
obj := queue[0]
queue = queue[1:]
if len(obj.ChildrenArray) == 0 {
continue
}
if !obj.IsGridDiagram() {
queue = append(queue, obj.ChildrenArray...)
continue
}
gd, err := layoutGrid(g, obj)
if err != nil {
return nil, nil, err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
var dx, dy float64
width := gd.width + 2*CONTAINER_PADDING
labelWidth := float64(obj.LabelDimensions.Width) + 2*label.PADDING
if labelWidth > width {
dx = (labelWidth - width) / 2
width = labelWidth
}
height := gd.height + 2*CONTAINER_PADDING
labelHeight := float64(obj.LabelDimensions.Height) + 2*label.PADDING
if labelHeight > CONTAINER_PADDING {
// if the label doesn't fit within the padding, we need to add more
grow := labelHeight - CONTAINER_PADDING
dy = grow / 2
height += grow
}
// we need to center children if we have to expand to fit the container label
if dx != 0 || dy != 0 {
gd.shift(dx, dy)
}
obj.Box = geo.NewBox(nil, width, height)
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
gridDiagrams[obj.AbsID()] = gd
for _, o := range gd.objects {
toRemove[o] = struct{}{}
}
}
}
objectOrder = make(map[string]int)
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
if _, exists := toRemove[obj]; !exists {
layoutObjects = append(layoutObjects, obj)
}
}
g.Objects = layoutObjects
return gridDiagrams, objectOrder, nil
}
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
gd := newGridDiagram(obj)
if gd.rows != 0 && gd.columns != 0 {
gd.layoutEvenly(g, obj)
} else {
gd.layoutDynamic(g, obj)
}
// position labels and icons
for _, o := range gd.objects {
if o.Icon != nil {
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
} else {
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
return gd, nil
}
func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
// layout objects in a grid with these 2 properties:
// all objects in the same row should have the same height
// all objects in the same column should have the same width
getObject := func(rowIndex, columnIndex int) *d2graph.Object {
var index int
if gd.rowDirected {
index = rowIndex*gd.columns + columnIndex
} else {
index = columnIndex*gd.rows + rowIndex
}
if index < len(gd.objects) {
return gd.objects[index]
}
return nil
}
rowHeights := make([]float64, 0, gd.rows)
colWidths := make([]float64, 0, gd.columns)
for i := 0; i < gd.rows; i++ {
rowHeight := 0.
for j := 0; j < gd.columns; j++ {
o := getObject(i, j)
if o == nil {
break
}
rowHeight = math.Max(rowHeight, o.Height)
}
rowHeights = append(rowHeights, rowHeight)
}
for j := 0; j < gd.columns; j++ {
columnWidth := 0.
for i := 0; i < gd.rows; i++ {
o := getObject(i, j)
if o == nil {
break
}
columnWidth = math.Max(columnWidth, o.Width)
}
colWidths = append(colWidths, columnWidth)
}
horizontalGap := float64(gd.horizontalGap)
verticalGap := float64(gd.verticalGap)
cursor := geo.NewPoint(0, 0)
if gd.rowDirected {
for i := 0; i < gd.rows; i++ {
for j := 0; j < gd.columns; j++ {
o := getObject(i, j)
if o == nil {
break
}
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
cursor.X += o.Width + horizontalGap
}
cursor.X = 0
cursor.Y += rowHeights[i] + verticalGap
}
} else {
for j := 0; j < gd.columns; j++ {
for i := 0; i < gd.rows; i++ {
o := getObject(i, j)
if o == nil {
break
}
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
cursor.Y += o.Height + verticalGap
}
cursor.X += colWidths[j] + horizontalGap
cursor.Y = 0
}
}
var totalWidth, totalHeight float64
for _, w := range colWidths {
totalWidth += w + horizontalGap
}
for _, h := range rowHeights {
totalHeight += h + verticalGap
}
totalWidth -= horizontalGap
totalHeight -= verticalGap
gd.width = totalWidth
gd.height = totalHeight
}
func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
// assume we have the following objects to layout:
// . ┌A──────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
// . └───────────────┘ │ │ │ │ │ │ │ │
// . │ │ └──────────┘ │ │ │ │
// . │ │ │ │ └─────────────────┘
// . └───┘ │ │
// . └─────────┘
// Note: if the grid is row dominant, all objects should be the same height (same width if column dominant)
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ │ │ │
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ │ │ │
// . │ │ │ │ │ │ │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// . │ │ ├ ─ ┤ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ └─────────┘ └─────────────────┘
horizontalGap := float64(gd.horizontalGap)
verticalGap := float64(gd.verticalGap)
// we want to split up the total width across the N rows or columns as evenly as possible
var totalWidth, totalHeight float64
for _, o := range gd.objects {
totalWidth += o.Width
totalHeight += o.Height
}
totalWidth += horizontalGap * float64(len(gd.objects)-gd.rows)
totalHeight += verticalGap * float64(len(gd.objects)-gd.columns)
var layout [][]*d2graph.Object
if gd.rowDirected {
targetWidth := totalWidth / float64(gd.rows)
layout = gd.getBestLayout(targetWidth, false)
} else {
targetHeight := totalHeight / float64(gd.columns)
layout = gd.getBestLayout(targetHeight, true)
}
cursor := geo.NewPoint(0, 0)
var maxY, maxX float64
if gd.rowDirected {
// if we have 2 rows, then each row's objects should have the same height
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │
// . │ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ ┴
// . ┌D────────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
// . │ │ │ │ │
// . │ │ │ │ │
// . │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │
// . │ │ │ │ │
// . └─────────┘ └─────────────────┘ ┴
rowWidths := []float64{}
for _, row := range layout {
rowHeight := 0.
for _, o := range row {
o.TopLeft = cursor.Copy()
cursor.X += o.Width + horizontalGap
rowHeight = math.Max(rowHeight, o.Height)
}
rowWidth := cursor.X - horizontalGap
rowWidths = append(rowWidths, rowWidth)
maxX = math.Max(maxX, rowWidth)
// set all objects in row to the same height
for _, o := range row {
o.Height = rowHeight
}
// new row
cursor.X = 0
cursor.Y += rowHeight + verticalGap
}
maxY = cursor.Y - horizontalGap
// then expand thinnest objects to make each row the same width
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . │ │ │ │ │ │ │
// . │ │ │ │ │ │ │
// . │ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ ┴
// . ┌D────────┬────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
// . │ │ │ │ │
// . │ │ │ │ │ │
// . │ │ │ │ │
// . │ │ │ │ │ │
// . └─────────┴────┘ └─────────────────┘ ┴
for i, row := range layout {
rowWidth := rowWidths[i]
if rowWidth == maxX {
continue
}
delta := maxX - rowWidth
objects := []*d2graph.Object{}
var widest float64
for _, o := range row {
widest = math.Max(widest, o.Width)
objects = append(objects, o)
}
sort.Slice(objects, func(i, j int) bool {
return objects[i].Width < objects[j].Width
})
// expand smaller objects to fill remaining space
for _, o := range objects {
if o.Width < widest {
var index int
for i, rowObj := range row {
if o == rowObj {
index = i
break
}
}
grow := math.Min(widest-o.Width, delta)
o.Width += grow
// shift following objects
for i := index + 1; i < len(row); i++ {
row[i].TopLeft.X += grow
}
delta -= grow
if delta <= 0 {
break
}
}
}
if delta > 0 {
grow := delta / float64(len(row))
for i := len(row) - 1; i >= 0; i-- {
o := row[i]
o.TopLeft.X += grow * float64(i)
o.Width += grow
delta -= grow
}
}
}
} else {
// if we have 3 columns, then each column's objects should have the same width
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
// . └──────────────┘ │ │ │ │
// . ┌B──┬──────────┐ └──────────┘ │ │
// . │ │ ┌D────────┬┐ └─────────────────┘
// . │ │ │ │ │
// . │ │ │ ││
// . └───┴──────────┘ │ │
// . │ ││
// . └─────────┴┘
colHeights := []float64{}
for _, column := range layout {
colWidth := 0.
for _, o := range column {
o.TopLeft = cursor.Copy()
cursor.Y += o.Height + verticalGap
colWidth = math.Max(colWidth, o.Width)
}
colHeight := cursor.Y - verticalGap
colHeights = append(colHeights, colHeight)
maxY = math.Max(maxY, colHeight)
// set all objects in column to the same width
for _, o := range column {
o.Width = colWidth
}
// new column
cursor.Y = 0
cursor.X += colWidth + horizontalGap
}
maxX = cursor.X - horizontalGap
// then expand shortest objects to make each column the same height
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
// . ├ ─ ─ ─ ─ ─ ─ ┤ │ │ │ │
// . │ │ └──────────┘ │ │
// . └──────────────┘ ┌D─────────┐ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// . ┌B─────────────┐ │ │ │ │
// . │ │ │ │ │ │
// . │ │ │ │ │ │
// . │ │ │ │ │ │
// . └──────────────┘ └──────────┘ └─────────────────┘
for i, column := range layout {
colHeight := colHeights[i]
if colHeight == maxY {
continue
}
delta := maxY - colHeight
objects := []*d2graph.Object{}
var tallest float64
for _, o := range column {
tallest = math.Max(tallest, o.Height)
objects = append(objects, o)
}
sort.Slice(objects, func(i, j int) bool {
return objects[i].Height < objects[j].Height
})
// expand smaller objects to fill remaining space
for _, o := range objects {
if o.Height < tallest {
var index int
for i, colObj := range column {
if o == colObj {
index = i
break
}
}
grow := math.Min(tallest-o.Height, delta)
o.Height += grow
// shift following objects
for i := index + 1; i < len(column); i++ {
column[i].TopLeft.Y += grow
}
delta -= grow
if delta <= 0 {
break
}
}
}
if delta > 0 {
grow := delta / float64(len(column))
for i := len(column) - 1; i >= 0; i-- {
o := column[i]
o.TopLeft.Y += grow * float64(i)
o.Height += grow
delta -= grow
}
}
}
}
gd.width = maxX
gd.height = maxY
}
// generate the best layout of objects aiming for each row to be the targetSize width
// if columns is true, each column aims to have the targetSize height
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
var nCuts int
if columns {
nCuts = gd.columns - 1
} else {
nCuts = gd.rows - 1
}
if nCuts == 0 {
return genLayout(gd.objects, nil)
}
// get all options for where to place these cuts, preferring later cuts over earlier cuts
// with 5 objects and 2 cuts we have these options:
// . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐
// . A B │ C D │ E └──┘ └──┘ └──┘
// . A │ B C D │ E ┌D───────────┐
// . A B │ C │ D E └────────────┘
// . A │ B C │ D E ┌E───────────┐
// . A │ B │ C D E └────────────┘
divisions := genDivisions(gd.objects, nCuts)
var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64
// of these divisions, find the layout with rows closest to the targetSize
for _, division := range divisions {
layout := genLayout(gd.objects, division)
dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if dist < bestDist {
bestLayout = layout
bestDist = dist
}
}
return bestLayout
}
// get all possible divisions of objects by the number of cuts
func genDivisions(objects []*d2graph.Object, nCuts int) (divisions [][]int) {
if len(objects) < 2 || nCuts == 0 {
return nil
}
// we go in this order to prefer extra objects in starting rows rather than later ones
lastObj := len(objects) - 1
for index := lastObj; index >= nCuts; index-- {
if nCuts > 1 {
for _, inner := range genDivisions(objects[:index], nCuts-1) {
divisions = append(divisions, append(inner, index-1))
}
} else {
divisions = append(divisions, []int{index - 1})
}
}
return divisions
}
// generate a grid of objects from the given cut indices
func genLayout(objects []*d2graph.Object, cutIndices []int) [][]*d2graph.Object {
layout := make([][]*d2graph.Object, len(cutIndices)+1)
objIndex := 0
for i := 0; i <= len(cutIndices); i++ {
var stop int
if i < len(cutIndices) {
stop = cutIndices[i]
} else {
stop = len(objects) - 1
}
for ; objIndex <= stop; objIndex++ {
layout[i] = append(layout[i], objects[objIndex])
}
}
return layout
}
func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalGap, verticalGap float64, columns bool) float64 {
totalDelta := 0.
for _, row := range layout {
rowSize := 0.
for _, o := range row {
if columns {
rowSize += o.Height + verticalGap
} else {
rowSize += o.Width + horizontalGap
}
}
totalDelta += math.Abs(rowSize - targetSize)
}
return totalDelta
}
// cleanup restores the graph after the core layout engine finishes
// - translating the grid to its position placed by the core layout engine
// - restore the children of the grid
// - sorts objects to their original graph order
func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objectsOrder map[string]int) {
defer func() {
sort.SliceStable(graph.Objects, func(i, j int) bool {
return objectsOrder[graph.Objects[i].AbsID()] < objectsOrder[graph.Objects[j].AbsID()]
})
}()
if graph.Root.IsGridDiagram() {
gd, exists := gridDiagrams[graph.Root.AbsID()]
if exists {
gd.cleanup(graph.Root, graph)
return
}
}
for _, obj := range graph.Objects {
gd, exists := gridDiagrams[obj.AbsID()]
if !exists {
continue
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
// shift the grid from (0, 0)
gd.shift(
obj.TopLeft.X+CONTAINER_PADDING,
obj.TopLeft.Y+CONTAINER_PADDING,
)
gd.cleanup(obj, graph)
}
}

View file

@ -1,6 +1,6 @@
package d2layoutfeatures package d2layoutfeatures
// When this is true, objects can set ther `near` key to another object // When this is true, objects can set their `near` key to another object
// When this is false, objects can only set `near` to constants // When this is false, objects can only set `near` to constants
const NEAR_OBJECT = "near_object" const NEAR_OBJECT = "near_object"

View file

@ -10,44 +10,62 @@ import (
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
) )
const pad = 20 const pad = 20
// Layout finds the shapes which are assigned constant near keywords and places them. // Layout finds the shapes which are assigned constant near keywords and places them.
func Layout(ctx context.Context, g *d2graph.Graph, constantNears []*d2graph.Object) error { func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
if len(constantNears) == 0 { if len(constantNearGraphs) == 0 {
return nil return nil
} }
for _, tempGraph := range constantNearGraphs {
tempGraph.Root.ChildrenArray[0].Parent = g.Root
for _, obj := range tempGraph.Objects {
obj.Graph = g
}
}
// Imagine the graph has two long texts, one at top center and one at top left. // Imagine the graph has two long texts, one at top center and one at top left.
// Top left should go left enough to not collide with center. // Top left should go left enough to not collide with center.
// So place the center ones first, then the later ones will consider them for bounding box // So place the center ones first, then the later ones will consider them for bounding box
for _, processCenters := range []bool{true, false} { for _, processCenters := range []bool{true, false} {
for _, obj := range constantNears { for _, tempGraph := range constantNearGraphs {
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "-center") { obj := tempGraph.Root.ChildrenArray[0]
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") {
prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
obj.TopLeft = geo.NewPoint(place(obj)) obj.TopLeft = geo.NewPoint(place(obj))
} dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
}
for _, obj := range constantNears {
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, obj)
obj.Parent.Children[obj.ID] = obj
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
}
}
}
// These shapes skipped core layout, which means they also skipped label placements for _, subObject := range tempGraph.Objects {
for _, obj := range constantNears { // `obj` already been replaced above by `place(obj)`
if obj.HasOutsideBottomLabel() { if subObject == obj {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) continue
} else if obj.Attributes.Icon != nil { }
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) subObject.TopLeft.X += dx
} else { subObject.TopLeft.Y += dy
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) }
for _, subEdge := range tempGraph.Edges {
for _, point := range subEdge.Route {
point.X += dx
point.Y += dy
}
}
}
}
for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0]
if processCenters == strings.Contains(d2graph.Key(obj.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...)
}
} }
} }
@ -59,43 +77,91 @@ func place(obj *d2graph.Object) (float64, float64) {
tl, br := boundingBox(obj.Graph) tl, br := boundingBox(obj.Graph)
w := br.X - tl.X w := br.X - tl.X
h := br.Y - tl.Y h := br.Y - tl.Y
switch d2graph.Key(obj.Attributes.NearKey)[0] {
nearKeyStr := d2graph.Key(obj.NearKey)[0]
var x, y float64
switch nearKeyStr {
case "top-left": case "top-left":
return tl.X - obj.Width - pad, tl.Y - obj.Height - pad x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
break
case "top-center": case "top-center":
return tl.X + w/2 - obj.Width/2, tl.Y - obj.Height - pad x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
break
case "top-right": case "top-right":
return br.X + pad, tl.Y - obj.Height - pad x, y = br.X+pad, tl.Y-obj.Height-pad
break
case "center-left": case "center-left":
return tl.X - obj.Width - pad, tl.Y + h/2 - obj.Height/2 x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
break
case "center-right": case "center-right":
return br.X + pad, tl.Y + h/2 - obj.Height/2 x, y = br.X+pad, tl.Y+h/2-obj.Height/2
break
case "bottom-left": case "bottom-left":
return tl.X - obj.Width - pad, br.Y + pad x, y = tl.X-obj.Width-pad, br.Y+pad
break
case "bottom-center": case "bottom-center":
return br.X - w/2 - obj.Width/2, br.Y + pad x, y = br.X-w/2-obj.Width/2, br.Y+pad
break
case "bottom-right": case "bottom-right":
return br.X + pad, br.Y + pad x, y = br.X+pad, br.Y+pad
break
} }
return 0, 0
if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
if strings.Contains(*obj.LabelPosition, "_TOP_") {
// label is on the top, and container is placed on the bottom
if strings.Contains(nearKeyStr, "bottom") {
y += float64(obj.LabelDimensions.Height)
}
} else if strings.Contains(*obj.LabelPosition, "_LEFT_") {
// label is on the left, and container is placed on the right
if strings.Contains(nearKeyStr, "right") {
x += float64(obj.LabelDimensions.Width)
}
} else if strings.Contains(*obj.LabelPosition, "_RIGHT_") {
// label is on the right, and container is placed on the left
if strings.Contains(nearKeyStr, "left") {
x -= float64(obj.LabelDimensions.Width)
}
} else if strings.Contains(*obj.LabelPosition, "_BOTTOM_") {
// label is on the bottom, and container is placed on the top
if strings.Contains(nearKeyStr, "top") {
y -= float64(obj.LabelDimensions.Height)
}
}
}
return x, y
} }
// WithoutConstantNears plucks out the graph objects which have "near" set to a constant value // WithoutConstantNears plucks out the graph objects which have "near" set to a constant value
// This is to be called before layout engines so they don't take part in regular positioning // This is to be called before layout engines so they don't take part in regular positioning
func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2graph.Object) { func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (constantNearGraphs []*d2graph.Graph) {
for i := 0; i < len(g.Objects); i++ { for i := 0; i < len(g.Objects); i++ {
obj := g.Objects[i] obj := g.Objects[i]
if obj.Attributes.NearKey == nil { if obj.NearKey == nil {
continue continue
} }
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) _, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
if isKey { if isKey {
continue continue
} }
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]] _, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isConst { if isConst {
nears = append(nears, obj) descendantObjects, edges := pluckObjAndEdges(g, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
tempGraph := d2graph.NewGraph()
tempGraph.Root.ChildrenArray = []*d2graph.Object{obj}
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
for _, descendantObj := range descendantObjects {
descendantObj.Graph = tempGraph
}
tempGraph.Objects = descendantObjects
tempGraph.Edges = edges
constantNearGraphs = append(constantNearGraphs, tempGraph)
i-- i--
delete(obj.Parent.Children, strings.ToLower(obj.ID)) delete(obj.Parent.Children, strings.ToLower(obj.ID))
for i := 0; i < len(obj.Parent.ChildrenArray); i++ { for i := 0; i < len(obj.Parent.ChildrenArray); i++ {
@ -104,9 +170,38 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2gra
break break
} }
} }
obj.Parent = tempGraph.Root
} }
} }
return nears return constantNearGraphs
}
func pluckObjAndEdges(g *d2graph.Graph, obj *d2graph.Object) (descendantsObjects []*d2graph.Object, edges []*d2graph.Edge) {
for i := 0; i < len(g.Edges); i++ {
edge := g.Edges[i]
if edge.Src == obj || edge.Dst == obj {
edges = append(edges, edge)
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
i--
}
}
for i := 0; i < len(g.Objects); i++ {
temp := g.Objects[i]
if temp.AbsID() == obj.AbsID() {
descendantsObjects = append(descendantsObjects, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
for _, child := range obj.ChildrenArray {
subObjects, subEdges := pluckObjAndEdges(g, child)
descendantsObjects = append(descendantsObjects, subObjects...)
edges = append(edges, subEdges...)
}
break
}
}
return descendantsObjects, edges
} }
// boundingBox gets the center of the graph as defined by shapes // boundingBox gets the center of the graph as defined by shapes
@ -122,10 +217,10 @@ func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
y2 := math.Inf(-1) y2 := math.Inf(-1)
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.Attributes.NearKey != nil { if obj.NearKey != nil {
// Top left should not be MORE top than top-center // Top left should not be MORE top than top-center
// But it should go more left if top-center label extends beyond bounds of diagram // But it should go more left if top-center label extends beyond bounds of diagram
switch d2graph.Key(obj.Attributes.NearKey)[0] { switch d2graph.Key(obj.NearKey)[0] {
case "top-center", "bottom-center": case "top-center", "bottom-center":
x1 = math.Min(x1, obj.TopLeft.X) x1 = math.Min(x1, obj.TopLeft.X)
x2 = math.Max(x2, obj.TopLeft.X+obj.Width) x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
@ -134,18 +229,21 @@ func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height) y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
} }
} else { } else {
if obj.OuterNearContainer() != nil {
continue
}
x1 = math.Min(x1, obj.TopLeft.X) x1 = math.Min(x1, obj.TopLeft.X)
y1 = math.Min(y1, obj.TopLeft.Y) y1 = math.Min(y1, obj.TopLeft.Y)
x2 = math.Max(x2, obj.TopLeft.X+obj.Width) x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height) y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
if obj.Attributes.Label.Value != "" && obj.LabelPosition != nil { if obj.Label.Value != "" && obj.LabelPosition != nil {
labelPosition := label.Position(*obj.LabelPosition) labelPosition := label.Position(*obj.LabelPosition)
if labelPosition.IsOutside() { if labelPosition.IsOutside() {
labelTL := labelPosition.GetPointOnBox(obj.Box, label.PADDING, float64(*obj.LabelWidth), float64(*obj.LabelHeight)) labelTL := labelPosition.GetPointOnBox(obj.Box, label.PADDING, float64(obj.LabelDimensions.Width), float64(obj.LabelDimensions.Height))
x1 = math.Min(x1, labelTL.X) x1 = math.Min(x1, labelTL.X)
y1 = math.Min(y1, labelTL.Y) y1 = math.Min(y1, labelTL.Y)
x2 = math.Max(x2, labelTL.X+float64(*obj.LabelWidth)) x2 = math.Max(x2, labelTL.X+float64(obj.LabelDimensions.Width))
y2 = math.Max(y2, labelTL.Y+float64(*obj.LabelHeight)) y2 = math.Max(y2, labelTL.Y+float64(obj.LabelDimensions.Height))
} }
} }
} }

View file

@ -13,6 +13,33 @@ import (
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
) )
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
//
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
// 2. Construct a sequence diagram from all descendant objects and edges
// 3. Remove those objects and edges from the main graph
// 4. Run layout on sequence diagrams
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (still without sequence diagram innards)
// 7. Put back sequence diagram innards in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g)
if err != nil {
return err
}
if g.Root.IsSequenceDiagram() {
// the sequence diagram is the only layout engine if the whole diagram is
// shape: sequence_diagram
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
return nil
}
func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) { func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) {
objectsToRemove := make(map[*d2graph.Object]struct{}) objectsToRemove := make(map[*d2graph.Object]struct{})
edgesToRemove := make(map[*d2graph.Edge]struct{}) edgesToRemove := make(map[*d2graph.Edge]struct{})
@ -27,7 +54,7 @@ func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]
if len(obj.ChildrenArray) == 0 { if len(obj.ChildrenArray) == 0 {
continue continue
} }
if obj.Attributes.Shape.Value != d2target.ShapeSequenceDiagram { if obj.Shape.Value != d2target.ShapeSequenceDiagram {
queue = append(queue, obj.ChildrenArray...) queue = append(queue, obj.ChildrenArray...)
continue continue
} }
@ -69,33 +96,6 @@ func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]
return sequenceDiagrams, objectOrder, edgeOrder, nil return sequenceDiagrams, objectOrder, edgeOrder, nil
} }
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
//
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
// 2. Construct a sequence diagram from all descendant objects and edges
// 3. Remove those objects and edges from the main graph
// 4. Run layout on sequence diagrams
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (still without sequence diagram innards)
// 7. Put back sequence diagram innards in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g)
if err != nil {
return err
}
if g.Root.IsSequenceDiagram() {
// the sequence diagram is the only layout engine if the whole diagram is
// shape: sequence_diagram
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
return nil
}
// layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants // layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) { func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
var edges []*d2graph.Edge var edges []*d2graph.Edge
@ -154,11 +154,11 @@ func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, obj
objects = g.Objects objects = g.Objects
} }
for _, obj := range objects { for _, obj := range objects {
if _, exists := sequenceDiagrams[obj.AbsID()]; !exists { sd, exists := sequenceDiagrams[obj.AbsID()]
if !exists {
continue continue
} }
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
sd := sequenceDiagrams[obj.AbsID()]
// shift the sequence diagrams as they are always placed at (0, 0) with some padding // shift the sequence diagrams as they are always placed at (0, 0) with some padding
sd.shift( sd.shift(
@ -171,22 +171,22 @@ func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, obj
obj.Children = make(map[string]*d2graph.Object) obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0) obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range sd.actors { for _, child := range sd.actors {
obj.Children[child.ID] = child obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child) obj.ChildrenArray = append(obj.ChildrenArray, child)
} }
for _, child := range sd.groups { for _, child := range sd.groups {
if child.Parent.AbsID() == obj.AbsID() { if child.Parent.AbsID() == obj.AbsID() {
obj.Children[child.ID] = child obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child) obj.ChildrenArray = append(obj.ChildrenArray, child)
} }
} }
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].messages...) g.Edges = append(g.Edges, sd.messages...)
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].lifelines...) g.Edges = append(g.Edges, sd.lifelines...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].actors...) g.Objects = append(g.Objects, sd.actors...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].notes...) g.Objects = append(g.Objects, sd.notes...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].groups...) g.Objects = append(g.Objects, sd.groups...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].spans...) g.Objects = append(g.Objects, sd.spans...)
} }
// no new objects, so just keep the same position // no new objects, so just keep the same position

View file

@ -180,7 +180,7 @@ b -> a.t2`
g, err := d2compiler.Compile("", strings.NewReader(input), nil) g, err := d2compiler.Compile("", strings.NewReader(input), nil)
assert.Nil(t, err) assert.Nil(t, err)
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
a, has := g.Root.HasChild([]string{"a"}) a, has := g.Root.HasChild([]string{"a"})
assert.True(t, has) assert.True(t, has)
@ -217,14 +217,14 @@ b -> a.t2`
}) })
// check properties // check properties
assert.Equal(t, strings.ToLower(shape.PERSON_TYPE), strings.ToLower(a.Attributes.Shape.Value)) assert.Equal(t, strings.ToLower(shape.PERSON_TYPE), strings.ToLower(a.Shape.Value))
if a_t1.Attributes.Label.Value != "" { if a_t1.Label.Value != "" {
t.Fatalf("expected no label for span, got %s", a_t1.Attributes.Label.Value) t.Fatalf("expected no label for span, got %s", a_t1.Label.Value)
} }
if a_t1.Attributes.Shape.Value != shape.SQUARE_TYPE { if a_t1.Shape.Value != shape.SQUARE_TYPE {
t.Fatalf("expected square shape for span, got %s", a_t1.Attributes.Shape.Value) t.Fatalf("expected square shape for span, got %s", a_t1.Shape.Value)
} }
if a_t1.Height != b_t1.Height { if a_t1.Height != b_t1.Height {
@ -323,7 +323,7 @@ container -> c: edge 1
c := g.Root.EnsureChild([]string{"c"}) c := g.Root.EnsureChild([]string{"c"})
c.Box = geo.NewBox(nil, 100, 100) c.Box = geo.NewBox(nil, 100, 100)
c.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSquare} c.Shape = d2graph.Scalar{Value: d2target.ShapeSquare}
layoutFn := func(ctx context.Context, g *d2graph.Graph) error { layoutFn := func(ctx context.Context, g *d2graph.Graph) error {
if len(g.Objects) != 2 { if len(g.Objects) != 2 {
@ -378,7 +378,7 @@ container -> c: edge 1
func TestSelfEdges(t *testing.T) { func TestSelfEdges(t *testing.T) {
g := d2graph.NewGraph() g := d2graph.NewGraph()
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
n1 := g.Root.EnsureChild([]string{"n1"}) n1 := g.Root.EnsureChild([]string{"n1"})
n1.Box = geo.NewBox(nil, 100, 100) n1.Box = geo.NewBox(nil, 100, 100)
@ -387,7 +387,7 @@ func TestSelfEdges(t *testing.T) {
Src: n1, Src: n1,
Dst: n1, Dst: n1,
Index: 0, Index: 0,
Attributes: &d2graph.Attributes{ Attributes: d2graph.Attributes{
Label: d2graph.Scalar{Value: "left to right"}, Label: d2graph.Scalar{Value: "left to right"},
}, },
}, },
@ -414,10 +414,10 @@ func TestSelfEdges(t *testing.T) {
func TestSequenceToDescendant(t *testing.T) { func TestSequenceToDescendant(t *testing.T) {
g := d2graph.NewGraph() g := d2graph.NewGraph()
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram} g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
a := g.Root.EnsureChild([]string{"a"}) a := g.Root.EnsureChild([]string{"a"})
a.Box = geo.NewBox(nil, 100, 100) a.Box = geo.NewBox(nil, 100, 100)
a.Attributes = &d2graph.Attributes{ a.Attributes = d2graph.Attributes{
Shape: d2graph.Scalar{Value: shape.PERSON_TYPE}, Shape: d2graph.Scalar{Value: shape.PERSON_TYPE},
} }
a_t1 := a.EnsureChild([]string{"t1"}) a_t1 := a.EnsureChild([]string{"t1"})
@ -428,12 +428,10 @@ func TestSequenceToDescendant(t *testing.T) {
Src: a, Src: a,
Dst: a_t1, Dst: a_t1,
Index: 0, Index: 0,
Attributes: &d2graph.Attributes{},
}, { }, {
Src: a_t1, Src: a_t1,
Dst: a, Dst: a,
Index: 0, Index: 0,
Attributes: &d2graph.Attributes{},
}, },
} }

View file

@ -110,7 +110,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
sd.objectRank[actor] = rank sd.objectRank[actor] = rank
if actor.Width < MIN_ACTOR_WIDTH { if actor.Width < MIN_ACTOR_WIDTH {
dslShape := strings.ToLower(actor.Attributes.Shape.Value) dslShape := strings.ToLower(actor.Shape.Value)
switch dslShape { switch dslShape {
case d2target.ShapePerson, d2target.ShapeOval, d2target.ShapeSquare, d2target.ShapeCircle: case d2target.ShapePerson, d2target.ShapeOval, d2target.ShapeSquare, d2target.ShapeCircle:
// scale shape up to min width uniformly // scale shape up to min width uniformly
@ -131,7 +131,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
// edge groups are children of actors with no edges and children edges // edge groups are children of actors with no edges and children edges
if child.IsSequenceDiagramNote() { if child.IsSequenceDiagramNote() {
sd.verticalIndices[child.AbsID()] = getObjEarliestLineNum(child) sd.verticalIndices[child.AbsID()] = getObjEarliestLineNum(child)
child.Attributes.Shape = d2graph.Scalar{Value: shape.PAGE_TYPE} child.Shape = d2graph.Scalar{Value: shape.PAGE_TYPE}
sd.notes = append(sd.notes, child) sd.notes = append(sd.notes, child)
sd.objectRank[child] = rank sd.objectRank[child] = rank
child.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) child.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
@ -139,8 +139,8 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
} else { } else {
// spans have no labels // spans have no labels
// TODO why not? Spans should be able to // TODO why not? Spans should be able to
child.Attributes.Label = d2graph.Scalar{Value: ""} child.Label = d2graph.Scalar{Value: ""}
child.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} child.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
sd.spans = append(sd.spans, child) sd.spans = append(sd.spans, child)
sd.objectRank[child] = rank sd.objectRank[child] = rank
} }
@ -186,8 +186,8 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
sd.yStep += VERTICAL_PAD sd.yStep += VERTICAL_PAD
sd.maxActorHeight += VERTICAL_PAD sd.maxActorHeight += VERTICAL_PAD
if sd.root.LabelHeight != nil { if sd.root.HasLabel() {
sd.maxActorHeight += float64(*sd.root.LabelHeight) sd.maxActorHeight += float64(sd.root.LabelDimensions.Height)
} }
return sd, nil return sd, nil
@ -282,11 +282,11 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
} }
func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) { func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) {
if group.LabelHeight == nil { if !group.HasLabel() {
return return
} }
heightAdd := (*group.LabelHeight + EDGE_GROUP_LABEL_PADDING) - GROUP_CONTAINER_PADDING heightAdd := (group.LabelDimensions.Height + EDGE_GROUP_LABEL_PADDING) - GROUP_CONTAINER_PADDING
if heightAdd < 0 { if heightAdd < 0 {
return return
} }
@ -339,8 +339,8 @@ func (sd *sequenceDiagram) placeActors() {
if actor.HasOutsideBottomLabel() { if actor.HasOutsideBottomLabel() {
actor.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) actor.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
yOffset = sd.maxActorHeight - actor.Height yOffset = sd.maxActorHeight - actor.Height
if actor.LabelHeight != nil { if actor.HasLabel() {
yOffset -= float64(*actor.LabelHeight) yOffset -= float64(actor.LabelDimensions.Height)
} }
} else { } else {
actor.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) actor.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
@ -381,18 +381,24 @@ func (sd *sequenceDiagram) addLifelineEdges() {
for _, actor := range sd.actors { for _, actor := range sd.actors {
actorBottom := actor.Center() actorBottom := actor.Center()
actorBottom.Y = actor.TopLeft.Y + actor.Height actorBottom.Y = actor.TopLeft.Y + actor.Height
if *actor.LabelPosition == string(label.OutsideBottomCenter) && actor.LabelHeight != nil { if *actor.LabelPosition == string(label.OutsideBottomCenter) && actor.HasLabel() {
actorBottom.Y += float64(*actor.LabelHeight) + LIFELINE_LABEL_PAD actorBottom.Y += float64(actor.LabelDimensions.Height) + LIFELINE_LABEL_PAD
} }
actorLifelineEnd := actor.Center() actorLifelineEnd := actor.Center()
actorLifelineEnd.Y = endY actorLifelineEnd.Y = endY
sd.lifelines = append(sd.lifelines, &d2graph.Edge{ style := d2graph.Style{
Attributes: &d2graph.Attributes{
Style: d2graph.Style{
StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)}, StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)},
StrokeWidth: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_WIDTH)}, StrokeWidth: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_WIDTH)},
}, }
}, if actor.Style.StrokeDash != nil {
style.StrokeDash = &d2graph.Scalar{Value: actor.Style.StrokeDash.Value}
}
if actor.Style.Stroke != nil {
style.Stroke = &d2graph.Scalar{Value: actor.Style.Stroke.Value}
}
sd.lifelines = append(sd.lifelines, &d2graph.Edge{
Attributes: d2graph.Attributes{Style: style},
Src: actor, Src: actor,
SrcArrow: false, SrcArrow: false,
Dst: &d2graph.Object{ Dst: &d2graph.Object{
@ -575,7 +581,7 @@ func (sd *sequenceDiagram) routeMessages() error {
} }
messageOffset += sd.yStep messageOffset += sd.yStep
if message.Attributes.Label.Value != "" { if message.Label.Value != "" {
message.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) message.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
} }

View file

@ -10,6 +10,7 @@ import (
"oss.terrastruct.com/d2/d2exporter" "oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near" "oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence" "oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
@ -68,14 +69,23 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
return nil, err return nil, err
} }
constantNears := d2near.WithoutConstantNears(ctx, g) constantNearGraphs := d2near.WithoutConstantNears(ctx, g)
err = d2sequence.Layout(ctx, g, coreLayout) layoutWithGrids := d2grid.Layout(ctx, g, coreLayout)
// run core layout for constantNears
for _, tempGraph := range constantNearGraphs {
if err = layoutWithGrids(ctx, tempGraph); err != nil {
return nil, err
}
}
err = d2sequence.Layout(ctx, g, layoutWithGrids)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = d2near.Layout(ctx, g, constantNears) err = d2near.Layout(ctx, g, constantNearGraphs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -110,7 +120,7 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
return d, nil return d, nil
} }
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) { func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
if opts.Layout != nil { if opts.Layout != nil {
return opts.Layout, nil return opts.Layout, nil
} else if os.Getenv("D2_LAYOUT") == "dagre" { } else if os.Getenv("D2_LAYOUT") == "dagre" {

View file

@ -17,6 +17,7 @@ import (
"oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2ir"
"oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
) )
@ -159,12 +160,12 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
} }
} }
if obj.Attributes.Label.MapKey != nil && obj.Map == nil && (!found || reserved || len(mk.Edges) > 0) { if obj.Label.MapKey != nil && obj.Map == nil && (!found || reserved || len(mk.Edges) > 0) {
obj.Map = &d2ast.Map{ obj.Map = &d2ast.Map{
Range: d2ast.MakeRange(",1:0:0-1:0:0"), Range: d2ast.MakeRange(",1:0:0-1:0:0"),
} }
obj.Attributes.Label.MapKey.Primary = obj.Attributes.Label.MapKey.Value.ScalarBox() obj.Label.MapKey.Primary = obj.Label.MapKey.Value.ScalarBox()
obj.Attributes.Label.MapKey.Value = d2ast.MakeValueBox(obj.Map) obj.Label.MapKey.Value = d2ast.MakeValueBox(obj.Map)
scope = obj.Map scope = obj.Map
mk.Key.Path = mk.Key.Path[toSkip-1:] mk.Key.Path = mk.Key.Path[toSkip-1:]
@ -180,6 +181,10 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
} }
} }
ir, err := d2ir.Compile(g.AST)
if err != nil {
return err
}
attrs := obj.Attributes attrs := obj.Attributes
var edge *d2graph.Edge var edge *d2graph.Edge
if len(mk.Edges) == 1 { if len(mk.Edges) == 1 {
@ -247,10 +252,10 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
if n.MapKey.Key.Path[0].Unbox().ScalarString() == mk.Key.Path[toSkip-1].Unbox().ScalarString() { if n.MapKey.Key.Path[0].Unbox().ScalarString() == mk.Key.Path[toSkip-1].Unbox().ScalarString() {
scope = n.MapKey.Value.Map scope = n.MapKey.Value.Map
if mk.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" && edge.SrcArrowhead != nil { if mk.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" && edge.SrcArrowhead != nil {
attrs = edge.SrcArrowhead attrs = *edge.SrcArrowhead
} }
if mk.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" && edge.DstArrowhead != nil { if mk.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" && edge.DstArrowhead != nil {
attrs = edge.DstArrowhead attrs = *edge.DstArrowhead
} }
reservedKey = mk.Key.Path[0].Unbox().ScalarString() reservedKey = mk.Key.Path[0].Unbox().ScalarString()
mk.Key.Path = mk.Key.Path[1:] mk.Key.Path = mk.Key.Path[1:]
@ -273,6 +278,9 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
} }
if reserved { if reserved {
inlined := func(s *d2graph.Scalar) bool {
return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey)
}
reservedIndex := toSkip - 1 reservedIndex := toSkip - 1
if mk.Key != nil && len(mk.Key.Path) > 0 { if mk.Key != nil && len(mk.Key.Path) > 0 {
if reservedKey == "" { if reservedKey == "" {
@ -280,47 +288,73 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
} }
switch reservedKey { switch reservedKey {
case "shape": case "shape":
if attrs.Shape.MapKey != nil { if inlined(&attrs.Shape) {
attrs.Shape.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Shape.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "link": case "link":
if attrs.Link != nil && attrs.Link.MapKey != nil { if inlined(attrs.Link) {
attrs.Link.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Link.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "tooltip": case "tooltip":
if attrs.Tooltip != nil && attrs.Tooltip.MapKey != nil { if inlined(attrs.Tooltip) {
attrs.Tooltip.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Tooltip.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "width": case "width":
if attrs.Width != nil && attrs.Width.MapKey != nil { if inlined(attrs.WidthAttr) {
attrs.Width.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.WidthAttr.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "height": case "height":
if attrs.Height != nil && attrs.Height.MapKey != nil { if inlined(attrs.HeightAttr) {
attrs.Height.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.HeightAttr.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "top": case "top":
if attrs.Top != nil && attrs.Top.MapKey != nil { if inlined(attrs.Top) {
attrs.Top.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Top.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "left": case "left":
if attrs.Left != nil && attrs.Left.MapKey != nil { if inlined(attrs.Left) {
attrs.Left.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Left.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "source-arrowhead", "target-arrowhead": case "grid-rows":
if reservedKey == "source-arrowhead" { if inlined(attrs.GridRows) {
attrs = edge.SrcArrowhead attrs.GridRows.MapKey.SetScalar(mk.Value.ScalarBox())
} else { return nil
attrs = edge.DstArrowhead
} }
if attrs != nil { case "grid-columns":
if inlined(attrs.GridColumns) {
attrs.GridColumns.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "grid-gap":
if inlined(attrs.GridGap) {
attrs.GridGap.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "vertical-gap":
if inlined(attrs.VerticalGap) {
attrs.VerticalGap.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "horizontal-gap":
if inlined(attrs.HorizontalGap) {
attrs.HorizontalGap.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "source-arrowhead", "target-arrowhead":
var arrowhead *d2graph.Attributes
if reservedKey == "source-arrowhead" {
arrowhead = edge.SrcArrowhead
} else {
arrowhead = edge.DstArrowhead
}
if arrowhead != nil {
if reservedTargetKey == "" { if reservedTargetKey == "" {
if len(mk.Key.Path[reservedIndex:]) != 2 { if len(mk.Key.Path[reservedIndex:]) != 2 {
return errors.New("malformed style setting, expected 2 part path") return errors.New("malformed style setting, expected 2 part path")
@ -329,13 +363,13 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
} }
switch reservedTargetKey { switch reservedTargetKey {
case "shape": case "shape":
if attrs.Shape.MapKey != nil { if inlined(&arrowhead.Shape) {
attrs.Shape.MapKey.SetScalar(mk.Value.ScalarBox()) arrowhead.Shape.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "label": case "label":
if attrs.Label.MapKey != nil { if inlined(&arrowhead.Label) {
attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox()) arrowhead.Label.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
} }
@ -349,98 +383,98 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
} }
switch reservedTargetKey { switch reservedTargetKey {
case "opacity": case "opacity":
if attrs.Style.Opacity != nil { if inlined(attrs.Style.Opacity) {
attrs.Style.Opacity.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Opacity.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "stroke": case "stroke":
if attrs.Style.Stroke != nil { if inlined(attrs.Style.Stroke) {
attrs.Style.Stroke.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Stroke.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "fill": case "fill":
if attrs.Style.Fill != nil { if inlined(attrs.Style.Fill) {
attrs.Style.Fill.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Fill.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "stroke-width": case "stroke-width":
if attrs.Style.StrokeWidth != nil { if inlined(attrs.Style.StrokeWidth) {
attrs.Style.StrokeWidth.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.StrokeWidth.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "stroke-dash": case "stroke-dash":
if attrs.Style.StrokeDash != nil { if inlined(attrs.Style.StrokeDash) {
attrs.Style.StrokeDash.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.StrokeDash.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "border-radius": case "border-radius":
if attrs.Style.BorderRadius != nil { if inlined(attrs.Style.BorderRadius) {
attrs.Style.BorderRadius.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.BorderRadius.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "shadow": case "shadow":
if attrs.Style.Shadow != nil { if inlined(attrs.Style.Shadow) {
attrs.Style.Shadow.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Shadow.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "3d": case "3d":
if attrs.Style.ThreeDee != nil { if inlined(attrs.Style.ThreeDee) {
attrs.Style.ThreeDee.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.ThreeDee.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "multiple": case "multiple":
if attrs.Style.Multiple != nil { if inlined(attrs.Style.Multiple) {
attrs.Style.Multiple.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Multiple.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "double-border": case "double-border":
if attrs.Style.DoubleBorder != nil { if inlined(attrs.Style.DoubleBorder) {
attrs.Style.DoubleBorder.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.DoubleBorder.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "font": case "font":
if attrs.Style.Font != nil { if inlined(attrs.Style.Font) {
attrs.Style.Font.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Font.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "font-size": case "font-size":
if attrs.Style.FontSize != nil { if inlined(attrs.Style.FontSize) {
attrs.Style.FontSize.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.FontSize.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "font-color": case "font-color":
if attrs.Style.FontColor != nil { if inlined(attrs.Style.FontColor) {
attrs.Style.FontColor.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.FontColor.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "animated": case "animated":
if attrs.Style.Animated != nil { if inlined(attrs.Style.Animated) {
attrs.Style.Animated.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Animated.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "bold": case "bold":
if attrs.Style.Bold != nil { if inlined(attrs.Style.Bold) {
attrs.Style.Bold.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Bold.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "italic": case "italic":
if attrs.Style.Italic != nil { if inlined(attrs.Style.Italic) {
attrs.Style.Italic.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Italic.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "underline": case "underline":
if attrs.Style.Underline != nil { if inlined(attrs.Style.Underline) {
attrs.Style.Underline.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.Underline.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
case "fill-pattern": case "fill-pattern":
if attrs.Style.FillPattern != nil { if inlined(attrs.Style.FillPattern) {
attrs.Style.FillPattern.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Style.FillPattern.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
} }
case "label": case "label":
if attrs.Label.MapKey != nil { if inlined(&attrs.Label) {
attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox()) attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox())
return nil return nil
} }
@ -639,7 +673,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
if !ok { if !ok {
return g, nil return g, nil
} }
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass { if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
return g, nil return g, nil
} }
@ -936,7 +970,7 @@ func deleteObject(g *d2graph.Graph, key *d2ast.KeyPath, obj *d2graph.Object) (*d
isSpecial := isReserved || x.Unbox().ScalarString() == "_" isSpecial := isReserved || x.Unbox().ScalarString() == "_"
return !isSpecial return !isSpecial
}) })
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass { if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
deleteFromMap(ref.Scope, ref.MapKey) deleteFromMap(ref.Scope, ref.MapKey)
} else if len(withoutSpecial) == 0 { } else if len(withoutSpecial) == 0 {
hoistRefChildren(g, key, ref) hoistRefChildren(g, key, ref)
@ -965,7 +999,7 @@ func deleteObject(g *d2graph.Graph, key *d2ast.KeyPath, obj *d2graph.Object) (*d
} else if ref.InEdge() { } else if ref.InEdge() {
edge := ref.MapKey.Edges[ref.MapKeyEdgeIndex] edge := ref.MapKey.Edges[ref.MapKeyEdgeIndex]
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass { if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
if ref.MapKeyEdgeDest() { if ref.MapKeyEdgeDest() {
ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, edge.Src, true) ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, edge.Src, true)
} else { } else {

View file

@ -50,11 +50,11 @@ func TestCreate(t *testing.T) {
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Label.MapKey.Value.Unbox() != nil { if g.Objects[0].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[0].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[0].Attributes.Label.MapKey.Value) t.Fatalf("expected g.Objects[0].Label.Node.Value.Unbox() == nil: %#v", g.Objects[0].Label.MapKey.Value)
} }
if d2format.Format(g.Objects[0].Attributes.Label.MapKey.Key) != "square" { if d2format.Format(g.Objects[0].Label.MapKey.Key) != "square" {
t.Fatalf("expected g.Objects[0].Attributes.Label.Node.Key to be square: %#v", g.Objects[0].Attributes.Label.MapKey.Key) t.Fatalf("expected g.Objects[0].Label.Node.Key to be square: %#v", g.Objects[0].Label.MapKey.Key)
} }
}, },
}, },
@ -92,11 +92,11 @@ x 2
if g.Objects[2].AbsID() != "b.c.square" { if g.Objects[2].AbsID() != "b.c.square" {
t.Fatalf("bad absolute ID: %#v", g.Objects[2].AbsID()) t.Fatalf("bad absolute ID: %#v", g.Objects[2].AbsID())
} }
if d2format.Format(g.Objects[2].Attributes.Label.MapKey.Key) != "b.c.square" { if d2format.Format(g.Objects[2].Label.MapKey.Key) != "b.c.square" {
t.Fatalf("bad mapkey: %#v", g.Objects[2].Attributes.Label.MapKey.Key) t.Fatalf("bad mapkey: %#v", g.Objects[2].Label.MapKey.Key)
} }
if g.Objects[2].Attributes.Label.MapKey.Value.Unbox() != nil { if g.Objects[2].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected nil mapkey value: %#v", g.Objects[2].Attributes.Label.MapKey.Value) t.Fatalf("expected nil mapkey value: %#v", g.Objects[2].Label.MapKey.Value)
} }
}, },
}, },
@ -116,11 +116,11 @@ square 2
if g.Objects[1].ID != "square 2" { if g.Objects[1].ID != "square 2" {
t.Fatalf("expected g.Objects[1].ID to be square 2: %#v", g.Objects[1]) t.Fatalf("expected g.Objects[1].ID to be square 2: %#v", g.Objects[1])
} }
if g.Objects[1].Attributes.Label.MapKey.Value.Unbox() != nil { if g.Objects[1].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[1].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[1].Attributes.Label.MapKey.Value) t.Fatalf("expected g.Objects[1].Label.Node.Value.Unbox() == nil: %#v", g.Objects[1].Label.MapKey.Value)
} }
if d2format.Format(g.Objects[1].Attributes.Label.MapKey.Key) != "square 2" { if d2format.Format(g.Objects[1].Label.MapKey.Key) != "square 2" {
t.Fatalf("expected g.Objects[1].Attributes.Label.Node.Key to be square 2: %#v", g.Objects[1].Attributes.Label.MapKey.Key) t.Fatalf("expected g.Objects[1].Label.Node.Key to be square 2: %#v", g.Objects[1].Label.MapKey.Key)
} }
}, },
}, },
@ -160,11 +160,11 @@ x.y.z.square 2
if g.Objects[3].ID != "square" { if g.Objects[3].ID != "square" {
t.Fatalf("expected g.Objects[3].ID to be square: %#v", g.Objects[3]) t.Fatalf("expected g.Objects[3].ID to be square: %#v", g.Objects[3])
} }
if g.Objects[3].Attributes.Label.MapKey.Value.Unbox() != nil { if g.Objects[3].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[3].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[3].Attributes.Label.MapKey.Value) t.Fatalf("expected g.Objects[3].Label.Node.Value.Unbox() == nil: %#v", g.Objects[3].Label.MapKey.Value)
} }
if d2format.Format(g.Objects[3].Attributes.Label.MapKey.Key) != "square" { if d2format.Format(g.Objects[3].Label.MapKey.Key) != "square" {
t.Fatalf("expected g.Objects[3].Attributes.Label.Node.Key to be square: %#v", g.Objects[3].Attributes.Label.MapKey.Key) t.Fatalf("expected g.Objects[3].Label.Node.Key to be square: %#v", g.Objects[3].Label.MapKey.Key)
} }
}, },
}, },
@ -188,11 +188,11 @@ x.y.z.square 2
if g.Objects[4].ID != "square 2" { if g.Objects[4].ID != "square 2" {
t.Fatalf("expected g.Objects[4].ID to be square 2: %#v", g.Objects[4]) t.Fatalf("expected g.Objects[4].ID to be square 2: %#v", g.Objects[4])
} }
if g.Objects[4].Attributes.Label.MapKey.Value.Unbox() != nil { if g.Objects[4].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[4].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[4].Attributes.Label.MapKey.Value) t.Fatalf("expected g.Objects[4].Label.Node.Value.Unbox() == nil: %#v", g.Objects[4].Label.MapKey.Value)
} }
if d2format.Format(g.Objects[4].Attributes.Label.MapKey.Key) != "square 2" { if d2format.Format(g.Objects[4].Label.MapKey.Key) != "square 2" {
t.Fatalf("expected g.Objects[4].Attributes.Label.Node.Key to be square 2: %#v", g.Objects[4].Attributes.Label.MapKey.Key) t.Fatalf("expected g.Objects[4].Label.Node.Key to be square 2: %#v", g.Objects[4].Label.MapKey.Key)
} }
}, },
}, },
@ -234,8 +234,8 @@ x.y.z.square 2
if g.Objects[13].ID != "square 11" { if g.Objects[13].ID != "square 11" {
t.Fatalf("expected g.Objects[13].ID to be square 11: %#v", g.Objects[13]) t.Fatalf("expected g.Objects[13].ID to be square 11: %#v", g.Objects[13])
} }
if d2format.Format(g.Objects[13].Attributes.Label.MapKey.Key) != "square 11" { if d2format.Format(g.Objects[13].Label.MapKey.Key) != "square 11" {
t.Fatalf("expected g.Objects[13].Attributes.Label.Node.Key to be square 11: %#v", g.Objects[13].Attributes.Label.MapKey.Key) t.Fatalf("expected g.Objects[13].Label.Node.Key to be square 11: %#v", g.Objects[13].Label.MapKey.Key)
} }
}, },
}, },
@ -517,11 +517,11 @@ func TestSet(t *testing.T) {
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Label.MapKey.Value.Unbox() != nil { if g.Objects[0].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[0].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[0].Attributes.Label.MapKey.Value) t.Fatalf("expected g.Objects[0].Label.Node.Value.Unbox() == nil: %#v", g.Objects[0].Label.MapKey.Value)
} }
if d2format.Format(g.Objects[0].Attributes.Label.MapKey.Key) != "square" { if d2format.Format(g.Objects[0].Label.MapKey.Key) != "square" {
t.Fatalf("expected g.Objects[0].Attributes.Label.Node.Key to be square: %#v", g.Objects[0].Attributes.Label.MapKey.Key) t.Fatalf("expected g.Objects[0].Label.Node.Key to be square: %#v", g.Objects[0].Label.MapKey.Key)
} }
}, },
}, },
@ -546,8 +546,8 @@ func TestSet(t *testing.T) {
if g.Edges[0].Dst.ID != "y" { if g.Edges[0].Dst.ID != "y" {
t.Fatalf("expected g.Edges[0].Dst.ID == y: %#v", g.Edges[0].Dst.ID) t.Fatalf("expected g.Edges[0].Dst.ID == y: %#v", g.Edges[0].Dst.ID)
} }
if g.Edges[0].Attributes.Label.Value != "two" { if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label.Value == two: %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("expected g.Edges[0].Label.Value == two: %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -566,8 +566,8 @@ func TestSet(t *testing.T) {
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeSquare { if g.Objects[0].Shape.Value != d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == square: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected g.Objects[0].Shape.Value == square: %#v", g.Objects[0].Shape.Value)
} }
}, },
}, },
@ -586,8 +586,8 @@ func TestSet(t *testing.T) {
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle { if g.Objects[0].Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == circle: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected g.Objects[0].Shape.Value == circle: %#v", g.Objects[0].Shape.Value)
} }
}, },
}, },
@ -606,7 +606,7 @@ func TestSet(t *testing.T) {
if len(g.Objects) != 1 { if len(g.Objects) != 1 {
t.Fatalf("expected 1 object but got %#v", len(g.Objects)) t.Fatalf("expected 1 object but got %#v", len(g.Objects))
} }
f, err := strconv.ParseFloat(g.Objects[0].Attributes.Style.Opacity.Value, 64) f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 { if err != nil || f != 0.2 {
t.Fatalf("expected g.Objects[0].Map.Nodes[0].MapKey.Value.Number.Value.Float64() == 0.2: %#v", f) t.Fatalf("expected g.Objects[0].Map.Nodes[0].MapKey.Value.Number.Value.Float64() == 0.2: %#v", f)
} }
@ -652,7 +652,7 @@ func TestSet(t *testing.T) {
if len(g.AST.Nodes[0].MapKey.Value.Map.Nodes) != 1 { if len(g.AST.Nodes[0].MapKey.Value.Map.Nodes) != 1 {
t.Fatalf("expected 1 node within square but got %v", len(g.AST.Nodes[0].MapKey.Value.Map.Nodes)) t.Fatalf("expected 1 node within square but got %v", len(g.AST.Nodes[0].MapKey.Value.Map.Nodes))
} }
f, err := strconv.ParseFloat(g.Objects[0].Attributes.Style.Opacity.Value, 64) f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 { if err != nil || f != 0.2 {
t.Fatal(err, f) t.Fatal(err, f)
} }
@ -670,7 +670,7 @@ func TestSet(t *testing.T) {
if len(g.AST.Nodes) != 1 { if len(g.AST.Nodes) != 1 {
t.Fatal(g.AST) t.Fatal(g.AST)
} }
f, err := strconv.ParseFloat(g.Objects[0].Attributes.Style.Opacity.Value, 64) f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 { if err != nil || f != 0.2 {
t.Fatal(err, f) t.Fatal(err, f)
} }
@ -689,7 +689,7 @@ square.style.opacity: 0.2
if len(g.AST.Nodes) != 2 { if len(g.AST.Nodes) != 2 {
t.Fatal(g.AST) t.Fatal(g.AST)
} }
f, err := strconv.ParseFloat(g.Objects[0].Attributes.Style.Opacity.Value, 64) f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 { if err != nil || f != 0.2 {
t.Fatal(err, f) t.Fatal(err, f)
} }
@ -859,6 +859,85 @@ square.style.opacity: 0.2
exp: `square: { exp: `square: {
style.fill-pattern: grain style.fill-pattern: grain
} }
`,
},
{
name: "classes-style",
text: `classes: {
a: {
style.fill: red
}
}
b.class: a
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b.class: a
b.style.fill: green
`,
},
{
name: "dupe-classes-style",
text: `classes: {
a: {
style.fill: red
}
}
b.class: a
b.style.fill: red
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b.class: a
b.style.fill: green
`,
},
{
name: "unapplied-classes-style",
text: `classes: {
a: {
style.fill: red
}
}
b.style.fill: red
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b.style.fill: green
`,
},
{
name: "unapplied-classes-style-2",
text: `classes: {
a: {
style.fill: red
}
}
b
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b: {style.fill: green}
`, `,
}, },
{ {
@ -877,8 +956,8 @@ square.style.opacity: 0.2
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value == d2target.ShapeSquare { if g.Objects[0].Shape.Value == d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == square: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected g.Objects[0].Shape.Value == square: %#v", g.Objects[0].Shape.Value)
} }
}, },
}, },
@ -897,8 +976,8 @@ square.style.opacity: 0.2
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0]) t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
} }
if g.Objects[0].Attributes.Shape.Value == d2target.ShapeSquare { if g.Objects[0].Shape.Value == d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == square: %#v", g.Objects[0].Attributes.Shape.Value) t.Fatalf("expected g.Objects[0].Shape.Value == square: %#v", g.Objects[0].Shape.Value)
} }
}, },
}, },
@ -920,8 +999,8 @@ square.style.opacity: 0.2
if g.Objects[0].ID != "square" { if g.Objects[0].ID != "square" {
t.Fatal(g.Objects[0]) t.Fatal(g.Objects[0])
} }
if g.Objects[0].Attributes.Label.Value == "I am deeply CONCERNED and I want something GOOD for BREAKFAST!" { if g.Objects[0].Label.Value == "I am deeply CONCERNED and I want something GOOD for BREAKFAST!" {
t.Fatal(g.Objects[0].Attributes.Label.Value) t.Fatal(g.Objects[0].Label.Value)
} }
}, },
}, },
@ -1036,8 +1115,8 @@ z: {
if len(g.Edges) != 2 { if len(g.Edges) != 2 {
t.Fatalf("expected 2 edges: %#v", g.Edges) t.Fatalf("expected 2 edges: %#v", g.Edges)
} }
if g.Edges[0].Attributes.Label.Value != "two" { if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label.Value == two: %#v", g.Edges[0].Attributes.Label.Value) t.Fatalf("expected g.Edges[0].Label.Value == two: %#v", g.Edges[0].Label.Value)
} }
}, },
}, },
@ -1054,8 +1133,8 @@ z: {
if len(g.Objects) != 1 { if len(g.Objects) != 1 {
t.Fatal(g.Objects) t.Fatal(g.Objects)
} }
if g.Objects[0].Attributes.Icon.String() != "https://icons.terrastruct.com/essentials/087-menu.svg" { if g.Objects[0].Icon.String() != "https://icons.terrastruct.com/essentials/087-menu.svg" {
t.Fatal(g.Objects[0].Attributes.Icon.String()) t.Fatal(g.Objects[0].Icon.String())
} }
}, },
}, },
@ -1133,7 +1212,7 @@ z: {
assert.JSON(t, 3, len(g.Objects)) assert.JSON(t, 3, len(g.Objects))
assert.JSON(t, 1, len(g.Edges)) assert.JSON(t, 1, len(g.Edges))
assert.JSON(t, "q", g.Edges[0].Src.ID) assert.JSON(t, "q", g.Edges[0].Src.ID)
assert.JSON(t, "0.4", g.Edges[0].Attributes.Style.Opacity.Value) assert.JSON(t, "0.4", g.Edges[0].Style.Opacity.Value)
}, },
}, },
{ {
@ -1309,8 +1388,8 @@ a.b -> a.c: {style.animated: true}
if g.Edges[0].Src.ID != "q" { if g.Edges[0].Src.ID != "q" {
t.Fatal(g.Edges[0].Src.ID) t.Fatal(g.Edges[0].Src.ID)
} }
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" { if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Edges[0].Attributes.Style.Opacity.Value) t.Fatal(g.Edges[0].Style.Opacity.Value)
} }
}, },
}, },

View file

@ -8,7 +8,7 @@ import (
type PluginFeature string type PluginFeature string
// When this is true, objects can set ther `near` key to another object // When this is true, objects can set their `near` key to another object
// When this is false, objects can only set `near` to constants // When this is false, objects can only set `near` to constants
const NEAR_OBJECT PluginFeature = "near_object" const NEAR_OBJECT PluginFeature = "near_object"
@ -33,19 +33,19 @@ func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error {
} }
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.Attributes.Top != nil || obj.Attributes.Left != nil { if obj.Top != nil || obj.Left != nil {
if _, ok := featureMap[TOP_LEFT]; !ok { if _, ok := featureMap[TOP_LEFT]; !ok {
return fmt.Errorf(`Object "%s" has attribute "top" and/or "left" set, but layout engine "%s" does not support locked positions.`, obj.AbsID(), info.Name) return fmt.Errorf(`Object "%s" has attribute "top" and/or "left" set, but layout engine "%s" does not support locked positions.`, obj.AbsID(), info.Name)
} }
} }
if (obj.Attributes.Width != nil || obj.Attributes.Height != nil) && len(obj.ChildrenArray) > 0 { if (obj.WidthAttr != nil || obj.HeightAttr != nil) && len(obj.ChildrenArray) > 0 {
if _, ok := featureMap[CONTAINER_DIMENSIONS]; !ok { if _, ok := featureMap[CONTAINER_DIMENSIONS]; !ok {
return fmt.Errorf(`Object "%s" has attribute "width" and/or "height" set, but layout engine "%s" does not support dimensions set on containers.`, obj.AbsID(), info.Name) return fmt.Errorf(`Object "%s" has attribute "width" and/or "height" set, but layout engine "%s" does not support dimensions set on containers.`, obj.AbsID(), info.Name)
} }
} }
if obj.Attributes.NearKey != nil { if obj.NearKey != nil {
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) _, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
if isKey { if isKey {
if _, ok := featureMap[NEAR_OBJECT]; !ok { if _, ok := featureMap[NEAR_OBJECT]; !ok {
return fmt.Errorf(`Object "%s" has "near" set to another object, but layout engine "%s" only supports constant values for "near".`, obj.AbsID(), info.Name) return fmt.Errorf(`Object "%s" has "near" set to another object, but layout engine "%s" only supports constant values for "near".`, obj.AbsID(), info.Name)

View file

@ -1,7 +1,7 @@
# d2fonts # d2fonts
The SVG renderer embeds fonts directly into the SVG as base64 data. This is to give The SVG renderer embeds fonts directly into the SVG as base64 data. This is to give
determinstic outputs and load without a network call. deterministic outputs and load without a network call.
To include your own font, e.g. `Helvetica`, you must include the Truetype glyphs: To include your own font, e.g. `Helvetica`, you must include the Truetype glyphs:
- `./ttf/Helvetica-Bold.ttf` - `./ttf/Helvetica-Bold.ttf`

View file

@ -66,6 +66,7 @@ const (
FONT_STYLE_REGULAR FontStyle = "regular" FONT_STYLE_REGULAR FontStyle = "regular"
FONT_STYLE_BOLD FontStyle = "bold" FONT_STYLE_BOLD FontStyle = "bold"
FONT_STYLE_SEMIBOLD FontStyle = "semibold"
FONT_STYLE_ITALIC FontStyle = "italic" FONT_STYLE_ITALIC FontStyle = "italic"
SourceSansPro FontFamily = "SourceSansPro" SourceSansPro FontFamily = "SourceSansPro"
@ -86,6 +87,7 @@ var FontSizes = []int{
var FontStyles = []FontStyle{ var FontStyles = []FontStyle{
FONT_STYLE_REGULAR, FONT_STYLE_REGULAR,
FONT_STYLE_BOLD, FONT_STYLE_BOLD,
FONT_STYLE_SEMIBOLD,
FONT_STYLE_ITALIC, FONT_STYLE_ITALIC,
} }
@ -101,6 +103,9 @@ var sourceSansProRegularBase64 string
//go:embed encoded/SourceSansPro-Bold.txt //go:embed encoded/SourceSansPro-Bold.txt
var sourceSansProBoldBase64 string var sourceSansProBoldBase64 string
//go:embed encoded/SourceSansPro-Semibold.txt
var sourceSansProSemiboldBase64 string
//go:embed encoded/SourceSansPro-Italic.txt //go:embed encoded/SourceSansPro-Italic.txt
var sourceSansProItalicBase64 string var sourceSansProItalicBase64 string
@ -110,6 +115,9 @@ var sourceCodeProRegularBase64 string
//go:embed encoded/SourceCodePro-Bold.txt //go:embed encoded/SourceCodePro-Bold.txt
var sourceCodeProBoldBase64 string var sourceCodeProBoldBase64 string
//go:embed encoded/SourceCodePro-Semibold.txt
var sourceCodeProSemiboldBase64 string
//go:embed encoded/SourceCodePro-Italic.txt //go:embed encoded/SourceCodePro-Italic.txt
var sourceCodeProItalicBase64 string var sourceCodeProItalicBase64 string
@ -135,6 +143,10 @@ func init() {
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}: sourceSansProBoldBase64, }: sourceSansProBoldBase64,
{
Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD,
}: sourceSansProSemiboldBase64,
{ {
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
@ -147,6 +159,10 @@ func init() {
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}: sourceCodeProBoldBase64, }: sourceCodeProBoldBase64,
{
Family: SourceCodePro,
Style: FONT_STYLE_SEMIBOLD,
}: sourceCodeProSemiboldBase64,
{ {
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
@ -164,6 +180,11 @@ func init() {
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}: fuzzyBubblesBoldBase64, }: fuzzyBubblesBoldBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_SEMIBOLD,
// This font has no semibold, so just reuse bold
}: fuzzyBubblesBoldBase64,
} }
for k, v := range FontEncodings { for k, v := range FontEncodings {
@ -195,6 +216,14 @@ func init() {
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }] = b
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Semibold.ttf")
if err != nil {
panic(err)
}
FontFaces[Font{
Family: SourceCodePro,
Style: FONT_STYLE_SEMIBOLD,
}] = b
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
@ -211,6 +240,14 @@ func init() {
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }] = b
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Semibold.ttf")
if err != nil {
panic(err)
}
FontFaces[Font{
Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD,
}] = b
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
@ -239,6 +276,10 @@ func init() {
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }] = b
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_SEMIBOLD,
}] = b
} }
var D2_FONT_TO_FAMILY = map[string]FontFamily{ var D2_FONT_TO_FAMILY = map[string]FontFamily{
@ -259,7 +300,7 @@ func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
return nil return nil
} }
func AddFontFamily(name string, regularTTF, italicTTF, boldTTF []byte) (*FontFamily, error) { func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []byte) (*FontFamily, error) {
customFontFamily := FontFamily(name) customFontFamily := FontFamily(name)
regularFont := Font{ regularFont := Font{
@ -316,6 +357,24 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF []byte) (*FontFam
FontEncodings[boldFont] = FontEncodings[fallbackFont] FontEncodings[boldFont] = FontEncodings[fallbackFont]
} }
semiboldFont := Font{
Family: customFontFamily,
Style: FONT_STYLE_SEMIBOLD,
}
if semiboldTTF != nil {
err := AddFontStyle(semiboldFont, FONT_STYLE_SEMIBOLD, semiboldTTF)
if err != nil {
return nil, err
}
} else {
fallbackFont := Font{
Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD,
}
FontFaces[semiboldFont] = FontFaces[fallbackFont]
FontEncodings[semiboldFont] = FontEncodings[fallbackFont]
}
FontFamilies = append(FontFamilies, customFontFamily) FontFamilies = append(FontFamilies, customFontFamily)
return &customFontFamily, nil return &customFontFamily, nil

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -1293,6 +1293,14 @@ queue -> package -> step
callout -> stored_data -> person callout -> stored_data -> person
diamond -> oval -> circle diamond -> oval -> circle
hexagon -> cloud hexagon -> cloud
`,
},
{
name: "long_arrowhead_label",
script: `
a -> b: {
target-arrowhead: "a to b with unexpectedly long target arrowhead label"
}
`, `,
}, },
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-3945613123" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-3945613123" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3945613123 .text-mono { .d2-3945613123 .text-mono {
font-family: "d2-3945613123-font-mono"; font-family: "d2-3945613123-font-mono";
} }

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1416 386"><svg id="d2-svg" class="d2-2363156786" width="1416" height="386" viewBox="-101 -101 1416 386"><rect x="-101.000000" y="-101.000000" width="1416.000000" height="386.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1416 386"><svg id="d2-svg" class="d2-2363156786" width="1416" height="386" viewBox="-101 -101 1416 386"><rect x="-101.000000" y="-101.000000" width="1416.000000" height="386.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2363156786 .text { .d2-2363156786 .text {
font-family: "d2-2363156786-font-regular"; font-family: "d2-2363156786-font-regular";
} }

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-3945613123" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-3945613123" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3945613123 .text-mono { .d2-3945613123 .text-mono {
font-family: "d2-3945613123-font-mono"; font-family: "d2-3945613123-font-mono";
} }

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 161 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 383 287"><svg id="d2-svg" class="d2-1134711225" width="383" height="287" viewBox="-101 -117 383 287"><rect x="-101.000000" y="-117.000000" width="383.000000" height="287.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 383 287"><svg id="d2-svg" class="d2-1134711225" width="383" height="287" viewBox="-101 -117 383 287"><rect x="-101.000000" y="-117.000000" width="383.000000" height="287.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1134711225 .text-bold { .d2-1134711225 .text-bold {
font-family: "d2-1134711225-font-bold"; font-family: "d2-1134711225-font-bold";
} }

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 942 367"><svg id="d2-svg" class="d2-3223990890" width="942" height="367" viewBox="-101 -100 942 367"><rect x="-101.000000" y="-100.000000" width="942.000000" height="367.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 942 367"><svg id="d2-svg" class="d2-3223990890" width="942" height="367" viewBox="-101 -100 942 367"><rect x="-101.000000" y="-100.000000" width="942.000000" height="367.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3223990890 .text { .d2-3223990890 .text {
font-family: "d2-3223990890-font-regular"; font-family: "d2-3223990890-font-regular";
} }

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 103 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-2779170942" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-2779170942" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2779170942 .text-bold { .d2-2779170942 .text-bold {
font-family: "d2-2779170942-font-bold"; font-family: "d2-2779170942-font-bold";
} }

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 493 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 676 KiB

After

Width:  |  Height:  |  Size: 676 KiB

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-3205202238" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-916646398" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
} }
.d2-3205202238 .text-bold { .d2-916646398 .text-bold {
font-family: "d2-3205202238-font-bold"; font-family: "d2-916646398-font-bold";
} }
@font-face { @font-face {
font-family: d2-3205202238-font-bold; font-family: d2-916646398-font-bold;
src: url("data:application/font-woff;base64,d09GRgABAAAAAAkcAAoAAAAADnQAAguFAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAGAAAABgXxHXrmNtYXAAAAFUAAAAZQAAAHwB5gIbZ2x5ZgAAAbwAAANOAAAD2Mcgs/ZoZWFkAAAFDAAAADYAAAA2G38e1GhoZWEAAAVEAAAAJAAAACQKfwXLaG10eAAABWgAAAAwAAAAMBYfAgdsb2NhAAAFmAAAABoAAAAaByQGRG1heHAAAAW0AAAAIAAAACAAJAD3bmFtZQAABdQAAAMoAAAIKgjwVkFwb3N0AAAI/AAAAB0AAAAg/9EAMgADAioCvAAFAAACigJYAAAASwKKAlgAAAFeADIBKQAAAgsHAwMEAwICBGAAAvcAAAADAAAAAAAAAABBREJPACAAIP//Au7/BgAAA9gBESAAAZ8AAAAAAfAClAAAACAAA3icXMy9CQJBAAbRt7fnv4FYiGBJBpsJghhoF4KIkWBbBtbyCWbehBM8FFXBUq9hbaXqbGztNHsHJ5eEv3d0TvLJO68888g9t1x/0rCFuaJT9UbGJqZmfAEAAP//AQAA///s4hfyAAAAeJxkk01sG0UUx9+s17v2ZolZe2fXduJs7bF34nw4xOPdwXUdJ6mJkXBa46pNUdpG5MBX0gSlrhyVA5eckKoiNUKBQ7jADU6cqARIXIAzVJU4geAMQbI4OTZaWxCk3kf//+/93jzwQwNA2BQOwQdBCEEYMADTklqGUUpkzjgnpo9TpMkNIdz79BOaFbNZcerckXVvYwOt3hIOT7fXVzc3/94olXrHXz7q3Ud3HgEIMNXvoJ9QF2JAAMyU7RRcbtskJcnUdVnewBqhRJJ43uWOJGHd+LraOHgokKy1mHbmts5vvLaviFYtEMtELl2w1LXKpeuhJI3iVxPpnb3e72yc7JmRNWU6ETXB60v3O+hX1IUoWAD+lO0Vej0G1iU5aRgsz01J8rGCx4Cs2t7yxe1S7eacKPSeKCvzjjtv3/roCzqTctWFVvPlVqWyVY1kgi5LvhKfQOezzhwAAIKlfgeFha8gNJxKY5pusLzrhX9fLz3Ugn5ZCqsZdf0lgZw+McMI3fbLQz5BRl0IwdhTfBLNu84ADOsGMiq71epupbJTre5UZnO52dzsrFq+27zSKpdbV5p3y+3VxaV6fWlxdcADgB6gLoS9vTGTDUJNeahaW9pXxLG6jceV6DOxZ8fLOjpZy8/7/e+KYjbf+wUQ4H4HfYy6QAfzUO6Z8mBsmhOcwlkY1g1zQsC69OP86/ZyqmIlJxK5+ERp8s2rxTVrOV6IF4v2uXL2DdW2bsTGzIhmRBQ1Xcy+cI1Gr+sGjcZGR0gxd/Hm0KPW76AdoQXmwIbjEIdzhhkm+D+fCG5crta1e+02SagxxYxw9a1rP9yWDg7ufDeVkcQtSR1mjQKgDjqBGACLUGYahueBcyabhNq2989kefTowfGMYihiIBxIHb3/4fFzqqmKQT1IkfBHA09jPI0b/b+aeAbjaaPp5ar9BXSKTryNnbnh3Pe/Bt+osG8kQ3E5HMhMKvI3h7WRsCIGtOCF+5+Zz1/+VhLfRv50Io5+e5xayZAaedwbWbg6NeReAUA/C++ACsAcphHHdTnTGF55r114MbXdbqPddWVcP+22h+/L/Q78CZ/DyL8X5d2RLn1gM2bbjKkOnXScSerAPwAAAP//AQAA//+mJsX8AAAAAQAAAAILhb0aRt9fDzz1AAED6AAAAADYXaCEAAAAAN1mLzb+N/7ECG0D8QABAAMAAgAAAAAAAAABAAAD2P7vAAAImP43/jcIbQABAAAAAAAAAAAAAAAAAAAADAKyAFACDwAqAgYAJAEeAEECKwAkAY4AQQG7ABUBfwARAgIADgIJAAwCEABGASwAPQAAACwAZACYALQA4AEAATwBYgGOAb4B1gHsAAAAAQAAAAwAkAAMAGMABwABAAAAAAAAAAAAAAAAAAQAA3icnJTPbhtVFMZ/TmzTCsECRVW6ie6CRZHo2FRJ1TYrh9SKRRQHjwtCQkgTz/iPMp4ZeSYO4QlY8xa8RVc8BM+BWKP5fOzYBdEmipJ8d+75851zvnOBHf5mm0r1IfBHPTFcYa9+bniLB/UTw9u061uGqzyp/Wm4RlibG67zea1n+CPeVn8z/ID96k+GH7JbbRv+mGfVHcOfbDv+Mvwp+7xd4Aq84FfDFXbJDG+xw4+Gt3mExaxUeUTTcI3P2DNcZw/oM6EgZkLCCMeQCSOumBGR4xMxY8KQiBBHhxYxhb4mBEKO0X9+DfApmBEo4pgCR4xPTEDO2CL+Iq+Uc2Uc6jSzuxYFYwIu5HFJQIIjZURKQsSl4hQUZLyiQYOcgfhmFOR45EyI8UiZMaJBlzan9BkzIcfRVqSSmU/KkIJrAuV3ZlF2ZkBEQm6srkgIxdOJXyTvDqc4umSyXY98uhHhSxzfybvklsr2Kzz9ujVmm3mXbALm6mesrsS6udYEx7ot87b4VrjgFe5e/dlk8v4ehfpfKPIFV5p/qEklYpLg3C4tfCnId49xHOncwVdHvqdDnxO6vKGvc4sePVqc0afDa/l26eH4mi5nHMujI7y4a0sxZ/yA4xs6siljR9afxcQifiYzdefiOFMdUzL1vGTuqdZIFd59wuUOpRvqyOUz0B6Vlk7zS7RnASNTRSaGU/VyqY3c+heaIqaqpZzt7X25DXPbveUW35Bqh0u1LjiVk1swet9UvXc0c60fj4CQlAtZDEiZ0qDgRrzPCbgixnGs7p1oSwpaK58yz41UEjEVgw6J4szI9Dcw3fjGfbChe2dvSSj/kunlqqr7ZHHq1e2M3qh7yzvfuhytTaBhU03X1DQQ18S0H2mn1vn78s31uqU85YiUmPBfL8AzPJrsc8AhY2UY6GZur0NTL0STlxyq+ksiWQ2l58giHODxnAMOeMnzd/q4ZOKMi1txWc/d4pgjuhx+UBUL+y5HvF59+/+sv4tpU7U4nq5OL+49xSd3UOsX2rPb97KniZWTmFu02604I2BacnG76zW5x3j/AAAA//8BAAD///S3T1F4nGJgZgCD/+cYjBiwAAAAAAD//wEAAP//LwECAwAAAA=="); src: url("data:application/font-woff;base64,d09GRgABAAAAAAkcAAoAAAAADnQAAguFAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAGAAAABgXxHXrmNtYXAAAAFUAAAAZQAAAHwB5gIbZ2x5ZgAAAbwAAANOAAAD2Mcgs/ZoZWFkAAAFDAAAADYAAAA2G38e1GhoZWEAAAVEAAAAJAAAACQKfwXLaG10eAAABWgAAAAwAAAAMBYfAgdsb2NhAAAFmAAAABoAAAAaByQGRG1heHAAAAW0AAAAIAAAACAAJAD3bmFtZQAABdQAAAMoAAAIKgjwVkFwb3N0AAAI/AAAAB0AAAAg/9EAMgADAioCvAAFAAACigJYAAAASwKKAlgAAAFeADIBKQAAAgsHAwMEAwICBGAAAvcAAAADAAAAAAAAAABBREJPACAAIP//Au7/BgAAA9gBESAAAZ8AAAAAAfAClAAAACAAA3icXMy9CQJBAAbRt7fnv4FYiGBJBpsJghhoF4KIkWBbBtbyCWbehBM8FFXBUq9hbaXqbGztNHsHJ5eEv3d0TvLJO68888g9t1x/0rCFuaJT9UbGJqZmfAEAAP//AQAA///s4hfyAAAAeJxkk01sG0UUx9+s17v2ZolZe2fXduJs7bF34nw4xOPdwXUdJ6mJkXBa46pNUdpG5MBX0gSlrhyVA5eckKoiNUKBQ7jADU6cqARIXIAzVJU4geAMQbI4OTZaWxCk3kf//+/93jzwQwNA2BQOwQdBCEEYMADTklqGUUpkzjgnpo9TpMkNIdz79BOaFbNZcerckXVvYwOt3hIOT7fXVzc3/94olXrHXz7q3Ud3HgEIMNXvoJ9QF2JAAMyU7RRcbtskJcnUdVnewBqhRJJ43uWOJGHd+LraOHgokKy1mHbmts5vvLaviFYtEMtELl2w1LXKpeuhJI3iVxPpnb3e72yc7JmRNWU6ETXB60v3O+hX1IUoWAD+lO0Vej0G1iU5aRgsz01J8rGCx4Cs2t7yxe1S7eacKPSeKCvzjjtv3/roCzqTctWFVvPlVqWyVY1kgi5LvhKfQOezzhwAAIKlfgeFha8gNJxKY5pusLzrhX9fLz3Ugn5ZCqsZdf0lgZw+McMI3fbLQz5BRl0IwdhTfBLNu84ADOsGMiq71epupbJTre5UZnO52dzsrFq+27zSKpdbV5p3y+3VxaV6fWlxdcADgB6gLoS9vTGTDUJNeahaW9pXxLG6jceV6DOxZ8fLOjpZy8/7/e+KYjbf+wUQ4H4HfYy6QAfzUO6Z8mBsmhOcwlkY1g1zQsC69OP86/ZyqmIlJxK5+ERp8s2rxTVrOV6IF4v2uXL2DdW2bsTGzIhmRBQ1Xcy+cI1Gr+sGjcZGR0gxd/Hm0KPW76AdoQXmwIbjEIdzhhkm+D+fCG5crta1e+02SagxxYxw9a1rP9yWDg7ufDeVkcQtSR1mjQKgDjqBGACLUGYahueBcyabhNq2989kefTowfGMYihiIBxIHb3/4fFzqqmKQT1IkfBHA09jPI0b/b+aeAbjaaPp5ar9BXSKTryNnbnh3Pe/Bt+osG8kQ3E5HMhMKvI3h7WRsCIGtOCF+5+Zz1/+VhLfRv50Io5+e5xayZAaedwbWbg6NeReAUA/C++ACsAcphHHdTnTGF55r114MbXdbqPddWVcP+22h+/L/Q78CZ/DyL8X5d2RLn1gM2bbjKkOnXScSerAPwAAAP//AQAA//+mJsX8AAAAAQAAAAILhb0aRt9fDzz1AAED6AAAAADYXaCEAAAAAN1mLzb+N/7ECG0D8QABAAMAAgAAAAAAAAABAAAD2P7vAAAImP43/jcIbQABAAAAAAAAAAAAAAAAAAAADAKyAFACDwAqAgYAJAEeAEECKwAkAY4AQQG7ABUBfwARAgIADgIJAAwCEABGASwAPQAAACwAZACYALQA4AEAATwBYgGOAb4B1gHsAAAAAQAAAAwAkAAMAGMABwABAAAAAAAAAAAAAAAAAAQAA3icnJTPbhtVFMZ/TmzTCsECRVW6ie6CRZHo2FRJ1TYrh9SKRRQHjwtCQkgTz/iPMp4ZeSYO4QlY8xa8RVc8BM+BWKP5fOzYBdEmipJ8d+75851zvnOBHf5mm0r1IfBHPTFcYa9+bniLB/UTw9u061uGqzyp/Wm4RlibG67zea1n+CPeVn8z/ID96k+GH7JbbRv+mGfVHcOfbDv+Mvwp+7xd4Aq84FfDFXbJDG+xw4+Gt3mExaxUeUTTcI3P2DNcZw/oM6EgZkLCCMeQCSOumBGR4xMxY8KQiBBHhxYxhb4mBEKO0X9+DfApmBEo4pgCR4xPTEDO2CL+Iq+Uc2Uc6jSzuxYFYwIu5HFJQIIjZURKQsSl4hQUZLyiQYOcgfhmFOR45EyI8UiZMaJBlzan9BkzIcfRVqSSmU/KkIJrAuV3ZlF2ZkBEQm6srkgIxdOJXyTvDqc4umSyXY98uhHhSxzfybvklsr2Kzz9ujVmm3mXbALm6mesrsS6udYEx7ot87b4VrjgFe5e/dlk8v4ehfpfKPIFV5p/qEklYpLg3C4tfCnId49xHOncwVdHvqdDnxO6vKGvc4sePVqc0afDa/l26eH4mi5nHMujI7y4a0sxZ/yA4xs6siljR9afxcQifiYzdefiOFMdUzL1vGTuqdZIFd59wuUOpRvqyOUz0B6Vlk7zS7RnASNTRSaGU/VyqY3c+heaIqaqpZzt7X25DXPbveUW35Bqh0u1LjiVk1swet9UvXc0c60fj4CQlAtZDEiZ0qDgRrzPCbgixnGs7p1oSwpaK58yz41UEjEVgw6J4szI9Dcw3fjGfbChe2dvSSj/kunlqqr7ZHHq1e2M3qh7yzvfuhytTaBhU03X1DQQ18S0H2mn1vn78s31uqU85YiUmPBfL8AzPJrsc8AhY2UY6GZur0NTL0STlxyq+ksiWQ2l58giHODxnAMOeMnzd/q4ZOKMi1txWc/d4pgjuhx+UBUL+y5HvF59+/+sv4tpU7U4nq5OL+49xSd3UOsX2rPb97KniZWTmFu02604I2BacnG76zW5x3j/AAAA//8BAAD///S3T1F4nGJgZgCD/+cYjBiwAAAAAAD//wEAAP//LwECAwAAAA==");
}]]></style><style type="text/css"><![CDATA[.shape { }]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision; shape-rendering: geometricPrecision;
@ -21,78 +21,78 @@
opacity: 0.5; opacity: 0.5;
} }
.d2-3205202238 .fill-N1{fill:#0A0F25;} .d2-916646398 .fill-N1{fill:#0A0F25;}
.d2-3205202238 .fill-N2{fill:#676C7E;} .d2-916646398 .fill-N2{fill:#676C7E;}
.d2-3205202238 .fill-N3{fill:#9499AB;} .d2-916646398 .fill-N3{fill:#9499AB;}
.d2-3205202238 .fill-N4{fill:#CFD2DD;} .d2-916646398 .fill-N4{fill:#CFD2DD;}
.d2-3205202238 .fill-N5{fill:#DEE1EB;} .d2-916646398 .fill-N5{fill:#DEE1EB;}
.d2-3205202238 .fill-N6{fill:#EEF1F8;} .d2-916646398 .fill-N6{fill:#EEF1F8;}
.d2-3205202238 .fill-N7{fill:#FFFFFF;} .d2-916646398 .fill-N7{fill:#FFFFFF;}
.d2-3205202238 .fill-B1{fill:#0D32B2;} .d2-916646398 .fill-B1{fill:#0D32B2;}
.d2-3205202238 .fill-B2{fill:#0D32B2;} .d2-916646398 .fill-B2{fill:#0D32B2;}
.d2-3205202238 .fill-B3{fill:#E3E9FD;} .d2-916646398 .fill-B3{fill:#E3E9FD;}
.d2-3205202238 .fill-B4{fill:#E3E9FD;} .d2-916646398 .fill-B4{fill:#E3E9FD;}
.d2-3205202238 .fill-B5{fill:#EDF0FD;} .d2-916646398 .fill-B5{fill:#EDF0FD;}
.d2-3205202238 .fill-B6{fill:#F7F8FE;} .d2-916646398 .fill-B6{fill:#F7F8FE;}
.d2-3205202238 .fill-AA2{fill:#4A6FF3;} .d2-916646398 .fill-AA2{fill:#4A6FF3;}
.d2-3205202238 .fill-AA4{fill:#EDF0FD;} .d2-916646398 .fill-AA4{fill:#EDF0FD;}
.d2-3205202238 .fill-AA5{fill:#F7F8FE;} .d2-916646398 .fill-AA5{fill:#F7F8FE;}
.d2-3205202238 .fill-AB4{fill:#EDF0FD;} .d2-916646398 .fill-AB4{fill:#EDF0FD;}
.d2-3205202238 .fill-AB5{fill:#F7F8FE;} .d2-916646398 .fill-AB5{fill:#F7F8FE;}
.d2-3205202238 .stroke-N1{stroke:#0A0F25;} .d2-916646398 .stroke-N1{stroke:#0A0F25;}
.d2-3205202238 .stroke-N2{stroke:#676C7E;} .d2-916646398 .stroke-N2{stroke:#676C7E;}
.d2-3205202238 .stroke-N3{stroke:#9499AB;} .d2-916646398 .stroke-N3{stroke:#9499AB;}
.d2-3205202238 .stroke-N4{stroke:#CFD2DD;} .d2-916646398 .stroke-N4{stroke:#CFD2DD;}
.d2-3205202238 .stroke-N5{stroke:#DEE1EB;} .d2-916646398 .stroke-N5{stroke:#DEE1EB;}
.d2-3205202238 .stroke-N6{stroke:#EEF1F8;} .d2-916646398 .stroke-N6{stroke:#EEF1F8;}
.d2-3205202238 .stroke-N7{stroke:#FFFFFF;} .d2-916646398 .stroke-N7{stroke:#FFFFFF;}
.d2-3205202238 .stroke-B1{stroke:#0D32B2;} .d2-916646398 .stroke-B1{stroke:#0D32B2;}
.d2-3205202238 .stroke-B2{stroke:#0D32B2;} .d2-916646398 .stroke-B2{stroke:#0D32B2;}
.d2-3205202238 .stroke-B3{stroke:#E3E9FD;} .d2-916646398 .stroke-B3{stroke:#E3E9FD;}
.d2-3205202238 .stroke-B4{stroke:#E3E9FD;} .d2-916646398 .stroke-B4{stroke:#E3E9FD;}
.d2-3205202238 .stroke-B5{stroke:#EDF0FD;} .d2-916646398 .stroke-B5{stroke:#EDF0FD;}
.d2-3205202238 .stroke-B6{stroke:#F7F8FE;} .d2-916646398 .stroke-B6{stroke:#F7F8FE;}
.d2-3205202238 .stroke-AA2{stroke:#4A6FF3;} .d2-916646398 .stroke-AA2{stroke:#4A6FF3;}
.d2-3205202238 .stroke-AA4{stroke:#EDF0FD;} .d2-916646398 .stroke-AA4{stroke:#EDF0FD;}
.d2-3205202238 .stroke-AA5{stroke:#F7F8FE;} .d2-916646398 .stroke-AA5{stroke:#F7F8FE;}
.d2-3205202238 .stroke-AB4{stroke:#EDF0FD;} .d2-916646398 .stroke-AB4{stroke:#EDF0FD;}
.d2-3205202238 .stroke-AB5{stroke:#F7F8FE;} .d2-916646398 .stroke-AB5{stroke:#F7F8FE;}
.d2-3205202238 .background-color-N1{background-color:#0A0F25;} .d2-916646398 .background-color-N1{background-color:#0A0F25;}
.d2-3205202238 .background-color-N2{background-color:#676C7E;} .d2-916646398 .background-color-N2{background-color:#676C7E;}
.d2-3205202238 .background-color-N3{background-color:#9499AB;} .d2-916646398 .background-color-N3{background-color:#9499AB;}
.d2-3205202238 .background-color-N4{background-color:#CFD2DD;} .d2-916646398 .background-color-N4{background-color:#CFD2DD;}
.d2-3205202238 .background-color-N5{background-color:#DEE1EB;} .d2-916646398 .background-color-N5{background-color:#DEE1EB;}
.d2-3205202238 .background-color-N6{background-color:#EEF1F8;} .d2-916646398 .background-color-N6{background-color:#EEF1F8;}
.d2-3205202238 .background-color-N7{background-color:#FFFFFF;} .d2-916646398 .background-color-N7{background-color:#FFFFFF;}
.d2-3205202238 .background-color-B1{background-color:#0D32B2;} .d2-916646398 .background-color-B1{background-color:#0D32B2;}
.d2-3205202238 .background-color-B2{background-color:#0D32B2;} .d2-916646398 .background-color-B2{background-color:#0D32B2;}
.d2-3205202238 .background-color-B3{background-color:#E3E9FD;} .d2-916646398 .background-color-B3{background-color:#E3E9FD;}
.d2-3205202238 .background-color-B4{background-color:#E3E9FD;} .d2-916646398 .background-color-B4{background-color:#E3E9FD;}
.d2-3205202238 .background-color-B5{background-color:#EDF0FD;} .d2-916646398 .background-color-B5{background-color:#EDF0FD;}
.d2-3205202238 .background-color-B6{background-color:#F7F8FE;} .d2-916646398 .background-color-B6{background-color:#F7F8FE;}
.d2-3205202238 .background-color-AA2{background-color:#4A6FF3;} .d2-916646398 .background-color-AA2{background-color:#4A6FF3;}
.d2-3205202238 .background-color-AA4{background-color:#EDF0FD;} .d2-916646398 .background-color-AA4{background-color:#EDF0FD;}
.d2-3205202238 .background-color-AA5{background-color:#F7F8FE;} .d2-916646398 .background-color-AA5{background-color:#F7F8FE;}
.d2-3205202238 .background-color-AB4{background-color:#EDF0FD;} .d2-916646398 .background-color-AB4{background-color:#EDF0FD;}
.d2-3205202238 .background-color-AB5{background-color:#F7F8FE;} .d2-916646398 .background-color-AB5{background-color:#F7F8FE;}
.d2-3205202238 .color-N1{color:#0A0F25;} .d2-916646398 .color-N1{color:#0A0F25;}
.d2-3205202238 .color-N2{color:#676C7E;} .d2-916646398 .color-N2{color:#676C7E;}
.d2-3205202238 .color-N3{color:#9499AB;} .d2-916646398 .color-N3{color:#9499AB;}
.d2-3205202238 .color-N4{color:#CFD2DD;} .d2-916646398 .color-N4{color:#CFD2DD;}
.d2-3205202238 .color-N5{color:#DEE1EB;} .d2-916646398 .color-N5{color:#DEE1EB;}
.d2-3205202238 .color-N6{color:#EEF1F8;} .d2-916646398 .color-N6{color:#EEF1F8;}
.d2-3205202238 .color-N7{color:#FFFFFF;} .d2-916646398 .color-N7{color:#FFFFFF;}
.d2-3205202238 .color-B1{color:#0D32B2;} .d2-916646398 .color-B1{color:#0D32B2;}
.d2-3205202238 .color-B2{color:#0D32B2;} .d2-916646398 .color-B2{color:#0D32B2;}
.d2-3205202238 .color-B3{color:#E3E9FD;} .d2-916646398 .color-B3{color:#E3E9FD;}
.d2-3205202238 .color-B4{color:#E3E9FD;} .d2-916646398 .color-B4{color:#E3E9FD;}
.d2-3205202238 .color-B5{color:#EDF0FD;} .d2-916646398 .color-B5{color:#EDF0FD;}
.d2-3205202238 .color-B6{color:#F7F8FE;} .d2-916646398 .color-B6{color:#F7F8FE;}
.d2-3205202238 .color-AA2{color:#4A6FF3;} .d2-916646398 .color-AA2{color:#4A6FF3;}
.d2-3205202238 .color-AA4{color:#EDF0FD;} .d2-916646398 .color-AA4{color:#EDF0FD;}
.d2-3205202238 .color-AA5{color:#F7F8FE;} .d2-916646398 .color-AA5{color:#F7F8FE;}
.d2-3205202238 .color-AB4{color:#EDF0FD;} .d2-916646398 .color-AB4{color:#EDF0FD;}
.d2-3205202238 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><a href="root.layers.x" xlink:href="root.layers.x"><g id="x"><g class="shape" ><rect x="0.000000" y="0.000000" width="85.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="42.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text><g transform="translate(69 -16)" class="appendix-icon"><circle cx="16" cy="16" r="16" fill="white" stroke="#DEE1EB" /><text class="text-bold" x="16" y="21" style="font-size: 16px;text-anchor:middle;">1</text></g></g></a><mask id="d2-3205202238" maskUnits="userSpaceOnUse" x="-101" y="-118" width="304" height="285"> .d2-916646398 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><a href="root.layers.x" xlink:href="root.layers.x"><g id="x"><g class="shape" ><rect x="0.000000" y="0.000000" width="85.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="42.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text><g transform="translate(69 -16)" class="appendix-icon"><circle cx="16" cy="16" r="16" fill="white" stroke="#DEE1EB" /><text class="text-bold" x="16" y="21" style="font-size: 16px;text-anchor:middle;">1</text></g></g></a><mask id="d2-916646398" maskUnits="userSpaceOnUse" x="-101" y="-118" width="304" height="285">
<rect x="-101" y="-118" width="304" height="285" fill="white"></rect> <rect x="-101" y="-118" width="304" height="285" fill="white"></rect>
</mask><line x1="-41.000000" x2="143.000000" y1="117.000000" y2="117.000000" class=" stroke-B2" /><g class="appendix" x="-1" y="67" width="104" height="100%"><g transform="translate(0 167)" class="appendix-icon"><circle cx="16" cy="0" r="16" fill="white" stroke="#DEE1EB" /><text class="text-bold" x="16" y="5" style="font-size: 16px;text-anchor:middle;">1</text></g><text class="text" x="48" y="172" style="font-size: 16px;">root &gt; x</text></g> </mask><line x1="-41.000000" x2="143.000000" y1="117.000000" y2="117.000000" class=" stroke-B2" /><g class="appendix" x="-1" y="67" width="104" height="100%"><g transform="translate(0 167)" class="appendix-icon"><circle cx="16" cy="0" r="16" fill="white" stroke="#DEE1EB" /><text class="text-bold" x="16" y="5" style="font-size: 16px;text-anchor:middle;">1</text></g><text class="text" x="48" y="172" style="font-size: 16px;">root &gt; x</text></g>

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 657 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 660 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 660 KiB

View file

@ -20,8 +20,6 @@ import (
"github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles" "github.com/alecthomas/chroma/v2/styles"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex" "oss.terrastruct.com/d2/d2renderers/d2latex"
@ -40,7 +38,6 @@ import (
const ( const (
DEFAULT_PADDING = 100 DEFAULT_PADDING = 100
MIN_ARROWHEAD_STROKE_WIDTH = 2
appendixIconRadius = 16 appendixIconRadius = 16
) )
@ -109,56 +106,13 @@ func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string {
))) )))
} }
func arrowheadDimensions(arrowhead d2target.Arrowhead, strokeWidth float64) (width, height float64) {
var baseWidth, baseHeight float64
var widthMultiplier, heightMultiplier float64
switch arrowhead {
case d2target.ArrowArrowhead:
baseWidth = 4
baseHeight = 4
widthMultiplier = 4
heightMultiplier = 4
case d2target.TriangleArrowhead:
baseWidth = 4
baseHeight = 4
widthMultiplier = 3
heightMultiplier = 4
case d2target.LineArrowhead:
widthMultiplier = 5
heightMultiplier = 8
case d2target.FilledDiamondArrowhead:
baseWidth = 11
baseHeight = 7
widthMultiplier = 5.5
heightMultiplier = 3.5
case d2target.DiamondArrowhead:
baseWidth = 11
baseHeight = 9
widthMultiplier = 5.5
heightMultiplier = 4.5
case d2target.FilledCircleArrowhead, d2target.CircleArrowhead:
baseWidth = 8
baseHeight = 8
widthMultiplier = 5
heightMultiplier = 5
case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired:
baseWidth = 9
baseHeight = 9
widthMultiplier = 4.5
heightMultiplier = 4.5
}
clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth)
return baseWidth + clippedStrokeWidth*widthMultiplier, baseHeight + clippedStrokeWidth*heightMultiplier
}
func arrowheadMarker(isTarget bool, id string, connection d2target.Connection) string { func arrowheadMarker(isTarget bool, id string, connection d2target.Connection) string {
arrowhead := connection.DstArrow arrowhead := connection.DstArrow
if !isTarget { if !isTarget {
arrowhead = connection.SrcArrow arrowhead = connection.SrcArrow
} }
strokeWidth := float64(connection.StrokeWidth) strokeWidth := float64(connection.StrokeWidth)
width, height := arrowheadDimensions(arrowhead, strokeWidth) width, height := arrowhead.Dimensions(strokeWidth)
var path string var path string
switch arrowhead { switch arrowhead {
@ -515,7 +469,12 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
if connection.Opacity != 1.0 { if connection.Opacity != 1.0 {
opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity) opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity)
} }
fmt.Fprintf(writer, `<g id="%s"%s>`, svg.EscapeText(connection.ID), opacityStyle)
classStr := ""
if len(connection.Classes) > 0 {
classStr = fmt.Sprintf(` class="%s"`, strings.Join(connection.Classes, " "))
}
fmt.Fprintf(writer, `<g id="%s"%s%s>`, svg.EscapeText(connection.ID), opacityStyle, classStr)
var markerStart string var markerStart string
if connection.SrcArrow != d2target.NoArrowhead { if connection.SrcArrow != d2target.NoArrowhead {
id := arrowheadMarkerID(false, connection) id := arrowheadMarkerID(false, connection)
@ -615,38 +574,40 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
fmt.Fprint(writer, textEl.Render()) fmt.Fprint(writer, textEl.Render())
} }
length := geo.Route(connection.Route).Length() if connection.SrcLabel != nil && connection.SrcLabel.Label != "" {
if connection.SrcLabel != "" { fmt.Fprint(writer, renderArrowheadLabel(connection, connection.SrcLabel.Label, false))
// TODO use arrowhead label dimensions https://github.com/terrastruct/d2/issues/183
size := float64(connection.FontSize)
position := 0.
if length > 0 {
position = size / length
} }
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.SrcLabel, position, size, size)) if connection.DstLabel != nil && connection.DstLabel.Label != "" {
} fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel.Label, true))
if connection.DstLabel != "" {
// TODO use arrowhead label dimensions https://github.com/terrastruct/d2/issues/183
size := float64(connection.FontSize)
position := 1.
if length > 0 {
position -= size / length
}
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel, position, size, size))
} }
fmt.Fprintf(writer, `</g>`) fmt.Fprintf(writer, `</g>`)
return return
} }
func renderArrowheadLabel(connection d2target.Connection, text string, position, width, height float64) string { func renderArrowheadLabel(connection d2target.Connection, text string, isDst bool) string {
labelTL := label.UnlockedTop.GetPointOnRoute(connection.Route, float64(connection.StrokeWidth), position, width, height) var width, height float64
if isDst {
width = float64(connection.DstLabel.LabelWidth)
height = float64(connection.DstLabel.LabelHeight)
} else {
width = float64(connection.SrcLabel.LabelWidth)
height = float64(connection.SrcLabel.LabelHeight)
}
labelTL := connection.GetArrowheadLabelPosition(isDst)
// svg text is positioned with the center of its baseline
baselineCenter := geo.Point{
X: labelTL.X + width/2.,
Y: labelTL.Y + float64(connection.FontSize),
}
textEl := d2themes.NewThemableElement("text") textEl := d2themes.NewThemableElement("text")
textEl.X = labelTL.X + width/2 textEl.X = baselineCenter.X
textEl.Y = labelTL.Y + float64(connection.FontSize) textEl.Y = baselineCenter.Y
textEl.Fill = d2target.FG_COLOR textEl.Fill = d2target.FG_COLOR
textEl.ClassName = "text-italic" textEl.ClassName = "text-italic"
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize) textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
textEl.Content = RenderText(text, textEl.X, height) textEl.Content = RenderText(text, textEl.X, height)
return textEl.Render() return textEl.Render()
} }
@ -919,7 +880,11 @@ func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape,
if targetShape.BorderRadius != 0 && (targetShape.Type == d2target.ShapeClass || targetShape.Type == d2target.ShapeSQLTable) { if targetShape.BorderRadius != 0 && (targetShape.Type == d2target.ShapeClass || targetShape.Type == d2target.ShapeSQLTable) {
fmt.Fprint(writer, clipPathForBorderRadius(diagramHash, targetShape)) fmt.Fprint(writer, clipPathForBorderRadius(diagramHash, targetShape))
} }
fmt.Fprintf(writer, `<g id="%s"%s>`, svg.EscapeText(targetShape.ID), opacityStyle) classStr := ""
if len(targetShape.Classes) > 0 {
classStr = fmt.Sprintf(` class="%s"`, strings.Join(targetShape.Classes, " "))
}
fmt.Fprintf(writer, `<g id="%s"%s%s>`, svg.EscapeText(targetShape.ID), opacityStyle, classStr)
tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)) tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
width := float64(targetShape.Width) width := float64(targetShape.Width)
height := float64(targetShape.Height) height := float64(targetShape.Height)
@ -1322,6 +1287,9 @@ func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape,
mdEl := d2themes.NewThemableElement("div") mdEl := d2themes.NewThemableElement("div")
mdEl.ClassName = "md" mdEl.ClassName = "md"
mdEl.Content = render mdEl.Content = render
if targetShape.FontSize != textmeasure.MarkdownFontSize {
mdEl.Style = fmt.Sprintf("font-size:%vpx", targetShape.FontSize)
}
fmt.Fprint(writer, mdEl.Render()) fmt.Fprint(writer, mdEl.Render())
fmt.Fprint(writer, `</foreignObject></g>`) fmt.Fprint(writer, `</foreignObject></g>`)
} else { } else {
@ -1423,6 +1391,20 @@ func EmbedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
), ),
) )
appendOnTrigger(
buf,
source,
[]string{`class="md"`},
fmt.Sprintf(`
@font-face {
font-family: %s-font-semibold;
src: url("%s");
}`,
diagramHash,
fontFamily.Font(0, d2fonts.FONT_STYLE_SEMIBOLD).GetEncodedSubset(corpus),
),
)
appendOnTrigger( appendOnTrigger(
buf, buf,
source, source,
@ -1753,10 +1735,12 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
} }
if hasMarkdown { if hasMarkdown {
css := MarkdownCSS css := MarkdownCSS
css = strings.ReplaceAll(css, ".md", fmt.Sprintf(".%s .md", diagramHash))
css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash)) css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash)) css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash)) css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash)) css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
css = strings.ReplaceAll(css, "font-semibold", fmt.Sprintf("%s-font-semibold", diagramHash))
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css) fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
} }
@ -2153,3 +2137,38 @@ func hash(s string) string {
h.Write([]byte(fmt.Sprintf("%s%s", s, secret))) h.Write([]byte(fmt.Sprintf("%s%s", s, secret)))
return fmt.Sprint(h.Sum32()) return fmt.Sprint(h.Sum32())
} }
func RenderMultiboard(diagram *d2target.Diagram, opts *RenderOpts) ([][]byte, error) {
var boards [][]byte
for _, dl := range diagram.Layers {
childrenBoards, err := RenderMultiboard(dl, opts)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Scenarios {
childrenBoards, err := RenderMultiboard(dl, opts)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Steps {
childrenBoards, err := RenderMultiboard(dl, opts)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
if !diagram.IsFolderOnly {
out, err := Render(diagram, opts)
if err != nil {
return boards, err
}
boards = append([][]byte{out}, boards...)
return boards, nil
}
return boards, nil
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-3945613123" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-3945613123" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3945613123 .text-mono { .d2-3945613123 .text-mono {
font-family: "d2-3945613123-font-mono"; font-family: "d2-3945613123-font-mono";
} }

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-2779170942" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[ <?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-2779170942" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2779170942 .text-bold { .d2-2779170942 .text-bold {
font-family: "d2-2779170942-font-bold"; font-family: "d2-2779170942-font-bold";
} }

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -70,7 +70,6 @@
.md h1 { .md h1 {
margin: 0.67em 0; margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em; padding-bottom: 0.3em;
font-size: 2em; font-size: 2em;
border-bottom: 1px solid var(--color-border-muted); border-bottom: 1px solid var(--color-border-muted);
@ -240,35 +239,30 @@
.md h6 { .md h6 {
margin-top: 24px; margin-top: 24px;
margin-bottom: 16px; margin-bottom: 16px;
font-weight: 600; font-weight: 400;
line-height: 1.25; line-height: 1.25;
font-family: "font-regular"; font-family: "font-semibold";
} }
.md h2 { .md h2 {
font-weight: 600;
padding-bottom: 0.3em; padding-bottom: 0.3em;
font-size: 1.5em; font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted); border-bottom: 1px solid var(--color-border-muted);
} }
.md h3 { .md h3 {
font-weight: 600;
font-size: 1.25em; font-size: 1.25em;
} }
.md h4 { .md h4 {
font-weight: 600;
font-size: 1em; font-size: 1em;
} }
.md h5 { .md h5 {
font-weight: 600;
font-size: 0.875em; font-size: 0.875em;
} }
.md h6 { .md h6 {
font-weight: 600;
font-size: 0.85em; font-size: 0.85em;
color: var(--color-fg-muted); color: var(--color-fg-muted);
} }
@ -465,7 +459,7 @@
margin-top: 16px; margin-top: 16px;
font-size: 1em; font-size: 1em;
font-style: italic; font-style: italic;
font-weight: 600; font-family: "font-semibold";
} }
.md dl dd { .md dl dd {
@ -474,7 +468,7 @@
} }
.md table th { .md table th {
font-weight: 600; font-family: "font-semibold";
} }
.md table th, .md table th,
@ -677,7 +671,7 @@
} }
.md .csv-data th { .md .csv-data th {
font-weight: 600; font-family: "font-semibold";
background: var(--color-canvas-subtle); background: var(--color-canvas-subtle);
border-top: 0; border-top: 0;
} }

View file

@ -29,6 +29,9 @@ const (
BG_COLOR = color.N7 BG_COLOR = color.N7
FG_COLOR = color.N1 FG_COLOR = color.N1
MIN_ARROWHEAD_STROKE_WIDTH = 2
ARROWHEAD_PADDING = 2.
) )
var BorderOffset = geo.NewVector(5, 5) var BorderOffset = geo.NewVector(5, 5)
@ -232,6 +235,20 @@ func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
x2 = go2.Max(x2, int(labelTL.X)+connection.LabelWidth) x2 = go2.Max(x2, int(labelTL.X)+connection.LabelWidth)
y2 = go2.Max(y2, int(labelTL.Y)+connection.LabelHeight) y2 = go2.Max(y2, int(labelTL.Y)+connection.LabelHeight)
} }
if connection.SrcLabel != nil && connection.SrcLabel.Label != "" {
labelTL := connection.GetArrowheadLabelPosition(false)
x1 = go2.Min(x1, int(labelTL.X))
y1 = go2.Min(y1, int(labelTL.Y))
x2 = go2.Max(x2, int(labelTL.X)+connection.SrcLabel.LabelWidth)
y2 = go2.Max(y2, int(labelTL.Y)+connection.SrcLabel.LabelHeight)
}
if connection.DstLabel != nil && connection.DstLabel.Label != "" {
labelTL := connection.GetArrowheadLabelPosition(true)
x1 = go2.Min(x1, int(labelTL.X))
y1 = go2.Min(y1, int(labelTL.Y))
x2 = go2.Max(x2, int(labelTL.X)+connection.DstLabel.LabelWidth)
y2 = go2.Max(y2, int(labelTL.Y)+connection.DstLabel.LabelHeight)
}
} }
return Point{x1, y1}, Point{x2, y2} return Point{x1, y1}, Point{x2, y2}
@ -286,8 +303,12 @@ func (diagram Diagram) GetCorpus() string {
} }
for _, c := range diagram.Connections { for _, c := range diagram.Connections {
corpus += c.Label corpus += c.Label
corpus += c.SrcLabel if c.SrcLabel != nil {
corpus += c.DstLabel corpus += c.SrcLabel.Label
}
if c.DstLabel != nil {
corpus += c.DstLabel.Label
}
} }
return corpus return corpus
@ -305,6 +326,8 @@ type Shape struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Classes []string `json:"classes,omitempty"`
Pos Point `json:"pos"` Pos Point `json:"pos"`
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
@ -425,13 +448,15 @@ func BaseShape() *Shape {
type Connection struct { type Connection struct {
ID string `json:"id"` ID string `json:"id"`
Classes []string `json:"classes,omitempty"`
Src string `json:"src"` Src string `json:"src"`
SrcArrow Arrowhead `json:"srcArrow"` SrcArrow Arrowhead `json:"srcArrow"`
SrcLabel string `json:"srcLabel"` SrcLabel *Text `json:"srcLabel,omitempty"`
Dst string `json:"dst"` Dst string `json:"dst"`
DstArrow Arrowhead `json:"dstArrow"` DstArrow Arrowhead `json:"dstArrow"`
DstLabel string `json:"dstLabel"` DstLabel *Text `json:"dstLabel,omitempty"`
Opacity float64 `json:"opacity"` Opacity float64 `json:"opacity"`
StrokeDash float64 `json:"strokeDash"` StrokeDash float64 `json:"strokeDash"`
@ -502,13 +527,85 @@ func (c Connection) CSSStyle() string {
} }
func (c *Connection) GetLabelTopLeft() *geo.Point { func (c *Connection) GetLabelTopLeft() *geo.Point {
return label.Position(c.LabelPosition).GetPointOnRoute( point, _ := label.Position(c.LabelPosition).GetPointOnRoute(
c.Route, c.Route,
float64(c.StrokeWidth), float64(c.StrokeWidth),
c.LabelPercentage, c.LabelPercentage,
float64(c.LabelWidth), float64(c.LabelWidth),
float64(c.LabelHeight), float64(c.LabelHeight),
) )
return point
}
func (connection *Connection) GetArrowheadLabelPosition(isDst bool) *geo.Point {
var width, height float64
if isDst {
width = float64(connection.DstLabel.LabelWidth)
height = float64(connection.DstLabel.LabelHeight)
} else {
width = float64(connection.SrcLabel.LabelWidth)
height = float64(connection.SrcLabel.LabelHeight)
}
// get the start/end points of edge segment with arrowhead
index := 0
if isDst {
index = len(connection.Route) - 2
}
start, end := connection.Route[index], connection.Route[index+1]
// how much to move the label back from the very end of the edge
var shift float64
if start.Y == end.Y {
// shift left/right to fit on horizontal segment
shift = width/2. + label.PADDING
} else if start.X == end.X {
// shift up/down to fit on vertical segment
shift = height/2. + label.PADDING
} else {
// TODO compute amount to shift according to angle instead of max
shift = math.Max(width, height)
}
length := geo.Route(connection.Route).Length()
var position float64
if isDst {
position = 1.
if length > 0 {
position -= shift / length
}
} else {
position = 0.
if length > 0 {
position = shift / length
}
}
strokeWidth := float64(connection.StrokeWidth)
labelTL, index := label.UnlockedTop.GetPointOnRoute(connection.Route, strokeWidth, position, width, height)
var arrowSize float64
if isDst && connection.DstArrow != NoArrowhead {
// Note: these dimensions are for rendering arrowheads on their side so we want the height
_, arrowSize = connection.DstArrow.Dimensions(strokeWidth)
} else if connection.SrcArrow != NoArrowhead {
_, arrowSize = connection.SrcArrow.Dimensions(strokeWidth)
}
if arrowSize > 0 {
// labelTL already accounts for strokeWidth and padding, we only want to shift further if the arrow is larger than this
offset := (arrowSize/2 + ARROWHEAD_PADDING) - strokeWidth/2 - label.PADDING
if offset > 0 {
start, end = connection.Route[index], connection.Route[index+1]
// Note: end to start to get normal towards unlocked top position
normalX, normalY := geo.GetUnitNormalVector(end.X, end.Y, start.X, start.Y)
labelTL.X += normalX * offset
labelTL.Y += normalY * offset
}
}
return labelTL
} }
func (c Connection) GetZIndex() int { func (c Connection) GetZIndex() int {
@ -581,6 +678,49 @@ func ToArrowhead(arrowheadType string, filled bool) Arrowhead {
} }
} }
func (arrowhead Arrowhead) Dimensions(strokeWidth float64) (width, height float64) {
var baseWidth, baseHeight float64
var widthMultiplier, heightMultiplier float64
switch arrowhead {
case ArrowArrowhead:
baseWidth = 4
baseHeight = 4
widthMultiplier = 4
heightMultiplier = 4
case TriangleArrowhead:
baseWidth = 4
baseHeight = 4
widthMultiplier = 3
heightMultiplier = 4
case LineArrowhead:
widthMultiplier = 5
heightMultiplier = 8
case FilledDiamondArrowhead:
baseWidth = 11
baseHeight = 7
widthMultiplier = 5.5
heightMultiplier = 3.5
case DiamondArrowhead:
baseWidth = 11
baseHeight = 9
widthMultiplier = 5.5
heightMultiplier = 4.5
case FilledCircleArrowhead, CircleArrowhead:
baseWidth = 8
baseHeight = 8
widthMultiplier = 5
heightMultiplier = 5
case CfOne, CfMany, CfOneRequired, CfManyRequired:
baseWidth = 9
baseHeight = 9
widthMultiplier = 4.5
heightMultiplier = 4.5
}
clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth)
return baseWidth + clippedStrokeWidth*widthMultiplier, baseHeight + clippedStrokeWidth*heightMultiplier
}
type Point struct { type Point struct {
X int `json:"x"` X int `json:"x"`
Y int `json:"y"` Y int `json:"y"`

View file

@ -78,6 +78,16 @@ var WarmNeutral = Neutral{
} }
var DarkNeutral = Neutral{ var DarkNeutral = Neutral{
N1: "#F4F6FA",
N2: "#BBBEC9",
N3: "#868A96",
N4: "#676D7D",
N5: "#3A3D49",
N6: "#191C28",
N7: "#000410",
}
var DarkMauveNeutral = Neutral{
N1: "#CDD6F4", N1: "#CDD6F4",
N2: "#BAC2DE", N2: "#BAC2DE",
N3: "#A6ADC8", N3: "#A6ADC8",

Some files were not shown because too many files have changed in this diff Show more