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
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version-file: ./go.mod
cache: true

View file

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

View file

@ -9,7 +9,7 @@ jobs:
d2-project:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.4.0
- uses: actions/add-to-project@v0.5.0
with:
project-url: https://github.com/orgs/terrastruct/projects/34
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
- **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)
- **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)
@ -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)
- **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)
- **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter)
### Misc

View file

@ -1,16 +1,10 @@
#### Features 🚀
- 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)
- ELK self loops get distributed around the object instead of stacking [#1232](https://github.com/terrastruct/d2/pull/1232)
#### Bugfixes ⛑️
- 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)
- Fixes an issue with markdown labels that are empty when rendered [#1223](https://github.com/terrastruct/d2/issues/1223)
- ELK self loops always have enough space for long labels [#1232](https://github.com/terrastruct/d2/pull/1232)

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)
- 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.
[#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)

View file

@ -9,7 +9,7 @@
#### 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)
- 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)

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)
- 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"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
@ -20,6 +21,7 @@ import (
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2animate"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
@ -32,10 +34,11 @@ import (
"oss.terrastruct.com/d2/lib/imgbundler"
ctxlog "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/pdf"
pdflib "oss.terrastruct.com/d2/lib/pdf"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/pptx"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xgif"
"cdr.dev/slog"
"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.")
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.")
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)
if err != nil {
@ -118,7 +122,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
return nil
}
fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag)
fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag, *fontSemiboldFlag)
if err != nil {
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")
}
}
if filepath.Ext(outputPath) == ".ppt" {
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
}
outputFormat := getExportExtension(outputPath)
if outputPath != "-" {
outputPath = ms.AbsPath(outputPath)
if *animateIntervalFlag > 0 {
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath))
}
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG or GIF.\nYou provided: %s", filepath.Ext(outputPath))
} else if *animateIntervalFlag <= 0 && outputFormat.requiresAnimationInterval() {
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)
var pw png.Playwright
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
if !outputFormat.supportsDarkTheme() {
if darkThemeFlag != nil {
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
darkThemeFlag = nil
}
}
var pw png.Playwright
if outputFormat.requiresPNGRenderer() {
pw, err = png.InitPlaywright()
if err != nil {
return err
@ -347,16 +356,66 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
return nil, false, err
}
if filepath.Ext(outputPath) == ".pdf" {
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
ext := getExportExtension(outputPath)
switch ext {
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 {
return pdf, 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 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)
if animateInterval <= 0 {
// Rename all the "root.layers.x" to the paths that the boards get output to
@ -364,28 +423,34 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
if err != nil {
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)
if err != nil {
return nil, false, err
}
out := boards[0]
if animateInterval > 0 {
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
if err != nil {
return nil, false, err
var out []byte
if len(boards) > 0 {
out = boards[0]
if animateInterval > 0 {
out, err = d2animate.Wrap(diagram, boards, renderOpts, 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
}
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
}
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
}
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
}
return out, true, nil
}
@ -462,26 +527,40 @@ func resolveLinks(currDiagramPath, outputPath string, diagram *d2target.Diagram)
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 {
if shape.Link != "" {
for k, v := range linkToOutput {
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
}
}
}
}
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 {
relink(board, linkToOutput)
err := relink(strings.Join([]string{currDiagramPath, "scenarios", board.Name}, "."), board, linkToOutput)
if err != nil {
return err
}
}
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) {
@ -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) {
toPNG := filepath.Ext(outputPath) == ".png"
toPNG := getExportExtension(outputPath) == PNG
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
@ -638,25 +717,13 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
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
if pdf == nil {
pdf = pdflib.Init()
if doc == nil {
doc = pdf.Init()
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 {
rootFill := diagram.Root.Fill
// 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 {
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 {
return svg, err
}
}
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 {
return nil, err
}
}
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 {
return nil, err
}
}
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 {
return nil, err
}
}
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 {
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
func DiscardSlog(ctx context.Context) context.Context {
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
}
func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string) (*d2fonts.FontFamily, error) {
if pathToRegular == "" && pathToItalic == "" && pathToBold == "" {
func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToSemibold string) (*d2fonts.FontFamily, error) {
if pathToRegular == "" && pathToItalic == "" && pathToBold == "" && pathToSemibold == "" {
return nil, nil
}
var regularTTF []byte
var italicTTF []byte
var boldTTF []byte
var semiboldTTF []byte
var err error
if pathToRegular != "" {
@ -813,6 +1019,99 @@ func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string)
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.validateNear(g)
c.validateEdges(g)
c.compileBoardsField(g, ir, "layers")
c.compileBoardsField(g, ir, "scenarios")
@ -115,8 +116,7 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
}
type compiler struct {
inEdgeGroup bool
err d2parser.ParseError
err d2parser.ParseError
}
func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
@ -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) {
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")
if shape != nil {
c.compileField(obj, shape)
@ -138,7 +150,7 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
c.compileField(obj, f)
}
switch obj.Attributes.Shape.Value {
switch obj.Shape.Value {
case d2target.ShapeClass:
c.compileClass(obj)
case d2target.ShapeSQLTable:
@ -158,26 +170,46 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
return
}
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(obj.Attributes, f)
if f.Name == "classes" {
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
} else if f.Name == "style" {
if f.Map() == nil {
return
}
c.compileStyle(obj.Attributes, f.Map())
if obj.Attributes.Style.Animated != nil {
c.errorf(obj.Attributes.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
c.compileStyle(&obj.Attributes, f.Map())
if obj.Style.Animated != nil {
c.errorf(obj.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
}
return
}
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")
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")
return
}
@ -185,14 +217,14 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
if f.Primary() != nil {
c.compileLabel(obj.Attributes, f)
c.compileLabel(&obj.Attributes, f)
}
if f.Map() != nil {
c.compileMap(obj, f.Map())
}
if obj.Attributes.Label.MapKey == nil {
obj.Attributes.Label.MapKey = f.LastPrimaryKey()
if obj.Label.MapKey == nil {
obj.Label.MapKey = f.LastPrimaryKey()
}
for _, fr := range f.References {
if fr.Primary() {
@ -218,7 +250,7 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
scalar := f.Primary().Value
switch scalar := scalar.(type) {
case *d2ast.Null:
// TODO: Delete instaed.
// TODO: Delete instead.
attrs.Label.Value = scalar.ScalarString()
case *d2ast.BlockString:
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)
return
}
attrs.Width = &d2graph.Scalar{}
attrs.Width.Value = scalar.ScalarString()
attrs.Width.MapKey = f.LastPrimaryKey()
attrs.WidthAttr = &d2graph.Scalar{}
attrs.WidthAttr.Value = scalar.ScalarString()
attrs.WidthAttr.MapKey = f.LastPrimaryKey()
case "height":
_, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer height %#v: %s", scalar.ScalarString(), err)
return
}
attrs.Height = &d2graph.Scalar{}
attrs.Height.Value = scalar.ScalarString()
attrs.Height.MapKey = f.LastPrimaryKey()
attrs.HeightAttr = &d2graph.Scalar{}
attrs.HeightAttr.Value = scalar.ScalarString()
attrs.HeightAttr.MapKey = f.LastPrimaryKey()
case "top":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
@ -362,6 +394,74 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
}
attrs.Constraint.Value = scalar.ScalarString()
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 {
@ -430,15 +530,17 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
case "filled":
attrs.Style.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "width":
attrs.Width = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
attrs.WidthAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "height":
attrs.Height = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
attrs.HeightAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "top":
attrs.Top = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "left":
attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "double-border":
attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "text-transform":
attrs.Style.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
}
}
@ -450,20 +552,13 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
}
if e.Primary() != nil {
c.compileLabel(edge.Attributes, e)
c.compileLabel(&edge.Attributes, e)
}
if e.Map() != nil {
for _, f := range e.Map().Fields {
_, ok := d2graph.ReservedKeywords[f.Name]
if !ok {
c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`)
continue
}
c.compileEdgeField(edge, f)
}
c.compileEdgeMap(edge, e.Map())
}
edge.Attributes.Label.MapKey = e.LastPrimaryKey()
edge.Label.MapKey = e.LastPrimaryKey()
for _, er := range e.References {
scopeObjIDA := d2ir.BoardIDA(er.Context.ScopeMap)
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) {
keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword]
@ -486,13 +604,13 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
}
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(edge.Attributes, f)
c.compileReserved(&edge.Attributes, f)
return
} else if f.Name == "style" {
if f.Map() == nil {
return
}
c.compileStyle(edge.Attributes, f.Map())
c.compileStyle(&edge.Attributes, f.Map())
return
}
@ -551,7 +669,7 @@ var FullToShortLanguageAliases map[string]string
func (c *compiler) compileClass(obj *d2graph.Object) {
obj.Class = &d2target.Class{}
for _, f := range obj.ChildrenArray {
visiblity := "public"
visibility := "public"
name := f.IDVal
// See https://www.uml-diagrams.org/visibility.html
if name != "" {
@ -559,35 +677,35 @@ func (c *compiler) compileClass(obj *d2graph.Object) {
case '+':
name = name[1:]
case '-':
visiblity = "private"
visibility = "private"
name = name[1:]
case '#':
visiblity = "protected"
visibility = "protected"
name = name[1:]
}
}
if !strings.Contains(f.IDVal, "(") {
typ := f.Attributes.Label.Value
typ := f.Label.Value
if typ == f.IDVal {
typ = ""
}
obj.Class.Fields = append(obj.Class.Fields, d2target.ClassField{
Name: name,
Type: typ,
Visibility: visiblity,
Visibility: visibility,
})
} else {
// TODO: Not great, AST should easily allow specifying alternate primary field
// as an explicit label should change the name.
returnType := f.Attributes.Label.Value
returnType := f.Label.Value
if returnType == f.IDVal {
returnType = "void"
}
obj.Class.Methods = append(obj.Class.Methods, d2target.ClassMethod{
Name: name,
Return: returnType,
Visibility: visiblity,
Visibility: visibility,
})
}
}
@ -607,7 +725,7 @@ func (c *compiler) compileClass(obj *d2graph.Object) {
func (c *compiler) compileSQLTable(obj *d2graph.Object) {
obj.SQLTable = &d2target.SQLTable{}
for _, col := range obj.ChildrenArray {
typ := col.Attributes.Label.Value
typ := col.Label.Value
if typ == col.IDVal {
// Not great, AST should easily allow specifying alternate primary field
// 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},
Type: d2target.Text{Label: typ},
}
if col.Attributes.Constraint.Value != "" {
d2Col.Constraint = col.Attributes.Constraint.Value
if col.Constraint.Value != "" {
d2Col.Constraint = col.Constraint.Value
}
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)
_, isReserved := d2graph.ReservedKeywords[keyword]
if isReserved {
switch obj.Attributes.Shape.Value {
switch obj.Shape.Value {
case d2target.ShapeCircle, d2target.ShapeSquare:
checkEqual := (keyword == "width" && obj.Attributes.Height != nil) || (keyword == "height" && obj.Attributes.Width != nil)
if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value {
c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Attributes.Shape.Value)
checkEqual := (keyword == "width" && obj.HeightAttr != nil) || (keyword == "height" && obj.WidthAttr != nil)
if checkEqual && obj.WidthAttr.Value != obj.HeightAttr.Value {
c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Shape.Value)
}
}
switch f.Name {
case "style":
if obj.Attributes.Style.ThreeDee != nil {
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeHexagon) {
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`)
if obj.Style.ThreeDee != nil {
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.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`)
}
}
if obj.Attributes.Style.DoubleBorder != nil {
if obj.Attributes.Shape.Value != "" && obj.Attributes.Shape.Value != d2target.ShapeSquare && obj.Attributes.Shape.Value != d2target.ShapeRectangle && obj.Attributes.Shape.Value != d2target.ShapeCircle && obj.Attributes.Shape.Value != d2target.ShapeOval {
c.errorf(obj.Attributes.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
if obj.Style.DoubleBorder != nil {
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.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
}
}
case "shape":
if obj.Attributes.Shape.Value == d2target.ShapeImage && obj.Attributes.Icon == nil {
if obj.Shape.Value == d2target.ShapeImage && obj.Icon == nil {
c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`)
}
in := d2target.IsShape(obj.Attributes.Shape.Value)
_, arrowheadIn := d2target.Arrowheads[obj.Attributes.Shape.Value]
in := d2target.IsShape(obj.Shape.Value)
_, arrowheadIn := d2target.Arrowheads[obj.Shape.Value]
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
}
if obj.Attributes.Shape.Value == d2target.ShapeImage {
if obj.Shape.Value == d2target.ShapeImage {
c.errorf(f.LastRef().AST(), "image shapes cannot have children.")
return
}
@ -695,9 +820,9 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
func (c *compiler) validateNear(g *d2graph.Graph) {
for _, obj := range g.Objects {
if obj.Attributes.NearKey != nil {
nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]]
if obj.NearKey != nil {
nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isKey {
// Doesn't make sense to set near to an ancestor or descendant
nearIsAncestor := false
@ -708,7 +833,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
}
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
}
nearIsDescendant := false
@ -719,55 +844,72 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
}
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
}
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
}
if nearObj.Attributes.NearKey != nil {
_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.Attributes.NearKey)[0]]
if nearObj.NearKey != nil {
_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
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
}
}
} 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 {
c.errorf(obj.Attributes.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")
c.errorf(obj.NearKey, "constant near keys can only be set on root level shapes")
continue
}
} 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
}
}
}
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) {
for _, obj := range g.Objects {
if obj.Attributes.Link == nil {
if obj.Link == nil {
continue
}
linkKey, err := d2parser.ParseKey(obj.Attributes.Link.Value)
linkKey, err := d2parser.ParseKey(obj.Link.Value)
if err != nil {
continue
}
@ -777,7 +919,7 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
}
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
}
}

View file

@ -43,8 +43,8 @@ x: {
t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value to be circle: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value != d2target.ShapeCircle {
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])
}
if g.Objects[0].Attributes.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)
if g.Objects[0].Style.Opacity.Value != "0.4" {
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" {
t.Fatalf("expected g.Objects[0].ID to be 'hey': %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeHexagon {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value to be hexagon: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value != d2target.ShapeHexagon {
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" {
t.Fatalf("expected g.Objects[0].Attributes.Width.Value to be 200: %#v", g.Objects[0].Attributes.Width.Value)
if g.Objects[0].WidthAttr.Value != "200" {
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" {
t.Fatalf("expected g.Objects[0].Attributes.Height.Value to be 230: %#v", g.Objects[0].Attributes.Height.Value)
if g.Objects[0].HeightAttr.Value != "230" {
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) {
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" {
t.Fatalf("expected ID to be 'hey': %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
}
if g.Objects[0].Attributes.Width != nil {
t.Fatalf("expected Attributes.Width to be nil: %#v", g.Objects[0].Attributes.Width)
if g.Objects[0].WidthAttr != nil {
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")
}
},
@ -237,7 +237,7 @@ containers: {
}
`,
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")
}
},
@ -326,7 +326,7 @@ containers: {
if len(g.Objects) != 1 {
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")
}
},
@ -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 {
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" {
t.Fatalf("expected g.Objects[1].Label.Value to be last value: %#v", g.Objects[1].Attributes.Label.Value)
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].Label.Value)
}
},
},
@ -470,8 +470,8 @@ x: {
if len(g.Root.ChildrenArray) != 2 {
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." {
t.Fatalf("expected g.Objects[0].Label.Value to be last value: %#v", g.Objects[0].Attributes.Label.Value)
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].Label.Value)
}
},
},
@ -626,11 +626,11 @@ x: {
if g.Edges[1].Dst.ID != "b" {
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" {
t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Attributes.Label)
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].Label)
}
if g.Edges[1].Attributes.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)
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].Label)
}
},
},
@ -656,8 +656,8 @@ x: {
if g.Edges[0].Dst.ID != "b" {
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." {
t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Attributes.Label)
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].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])
}
if g.Edges[0].Attributes.Label.Value != "The kids will love our inflatable slides" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label: %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "The kids will love our inflatable slides" {
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" {
t.Fatalf("unexpected g.Edges[1].Attributes.Label: %#v", g.Edges[1].Attributes.Label.Value)
if g.Edges[1].Label.Value != "The kids will love our inflatable slides" {
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 {
t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
}
if g.Edges[0].Attributes.Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label to be two: %#v", g.Edges[0].Attributes.Label)
if g.Edges[0].Label.Value != "two" {
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 {
t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
}
if g.Edges[0].Attributes.Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label to be two: %#v", g.Edges[0].Attributes.Label)
if g.Edges[0].Label.Value != "two" {
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 {
t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
}
if g.Edges[0].Attributes.Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label to be two: %#v", g.Edges[0].Attributes.Label)
if g.Edges[0].Label.Value != "two" {
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" {
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." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Label.Value != "asdf" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "asdf" {
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)
}
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
exp := `x -> y: {
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, "QOTD", g.Edges[0].DstArrowhead.Label.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].Attributes.Label.Value)
assert.JSON(t, nil, g.Edges[0].Attributes.Style.Filled)
assert.String(t, "", g.Edges[0].Shape.Value)
assert.String(t, "", g.Edges[0].Label.Value)
assert.JSON(t, nil, g.Edges[0].Style.Filled)
},
},
{
@ -1044,7 +1044,7 @@ x -> y: {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
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)
}
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)
}
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)
}
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].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Animated.Value != "true" {
t.Fatalf("Edges[0].Attributes.Style.Animated.Value: %#v", g.Edges[0].Attributes.Style.Animated.Value)
if g.Edges[0].Style.Animated.Value != "true" {
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 {
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." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
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." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[1].Attributes.Label.Value)
if g.Edges[1].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
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 {
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." {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Attributes.Style.Opacity.Value: %#v", g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
}
if g.Edges[0].Attributes.Label.Value != "" {
t.Fatalf("unexpected g.Edges[0].Attributes.Label.Value : %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].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 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Link.Value)
if g.Objects[0].Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Link.Value)
}
},
},
@ -1465,8 +1465,8 @@ x -> y: {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Tooltip.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Tooltip.Value)
if g.Objects[0].Tooltip.Value != "https://google.com" {
t.Fatal(g.Objects[0].Tooltip.Value)
}
},
},
@ -1482,12 +1482,12 @@ x -> y: {
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Link.Value)
if g.Objects[0].Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Link.Value)
}
if g.Objects[0].Attributes.Tooltip.Value != "hello world" {
t.Fatal(g.Objects[0].Attributes.Tooltip.Value)
if g.Objects[0].Tooltip.Value != "hello world" {
t.Fatal(g.Objects[0].Tooltip.Value)
}
},
},
@ -1517,8 +1517,8 @@ b: {
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Link.Value != "Overview.Untitled board 7.zzzzz" {
t.Fatal(g.Objects[0].Attributes.Link.Value)
if g.Objects[0].Link.Value != "Overview.Untitled board 7.zzzzz" {
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`,
},
{
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",
text: `x: {
near: top-center
}
x -> y
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:2:9: constant near keys cannot be set on connected shapes`,
text: `
x: {
near: top-center
}
x -> y
`,
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",
@ -1601,20 +1603,20 @@ y
if len(g.Objects) != 2 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.NearKey == nil {
if g.Objects[0].NearKey == nil {
t.Fatal("missing near key")
}
if g.Objects[0].Attributes.Icon.Path != "orange" {
t.Fatal(g.Objects[0].Attributes.Icon)
if g.Objects[0].Icon.Path != "orange" {
t.Fatal(g.Objects[0].Icon)
}
if g.Objects[0].Attributes.Style.Opacity.Value != "0.5" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity)
if g.Objects[0].Style.Opacity.Value != "0.5" {
t.Fatal(g.Objects[0].Style.Opacity)
}
if g.Objects[0].Attributes.Style.Stroke.Value != "red" {
t.Fatal(g.Objects[0].Attributes.Style.Stroke)
if g.Objects[0].Style.Stroke.Value != "red" {
t.Fatal(g.Objects[0].Style.Stroke)
}
if g.Objects[0].Attributes.Style.Fill.Value != "green" {
t.Fatal(g.Objects[0].Attributes.Style.Fill)
if g.Objects[0].Style.Fill.Value != "green" {
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
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)
}
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 {
t.Fatal(len(g.Objects[0].Class.Methods))
}
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity.Value)
if g.Objects[0].Style.Opacity.Value != "0.4" {
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 {
t.Fatal(len(g.Objects[0].SQLTable.Columns))
}
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity.Value)
if g.Objects[0].Style.Opacity.Value != "0.4" {
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 {
t.Fatal(len(g.Objects[0].SQLTable.Columns))
}
if g.Objects[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Attributes.Style.Opacity.Value)
if g.Objects[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Objects[0].Style.Opacity.Value)
}
},
},
@ -1794,7 +1796,7 @@ x.y -> a.b: {
}
`,
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) {
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
`,
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`,
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`,
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
}`,
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
}`,
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) {
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) {
tassert.Equal(t, "root.layers.cat", g.Objects[0].Attributes.Link.Value)
tassert.Equal(t, "root.layers.cat", g.Scenarios[0].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].Link.Value)
},
},
{
@ -2203,7 +2205,7 @@ layers: {
}
}`,
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) {
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) {
tassert.NotNil(t, g.Layers[0].Layers[0].Objects[0].Attributes.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[1].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].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`,
},
{
name: "text-transform",
text: `direction: right
x -> y: hi {
style: {
text-transform: capitalize
}
}
x.style.text-transform: uppercase
y.style.text-transform: lowercase`,
},
{
name: "near_near_const",
text: `
@ -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`,
},
{
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 {
@ -2412,6 +2579,19 @@ layers: {
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",
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) {
shape.Stroke = obj.GetStroke(shape.StrokeDash)
shape.Fill = obj.GetFill()
if obj.Attributes.Shape.Value == d2target.ShapeText {
if obj.Shape.Value == d2target.ShapeText {
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.SecondaryAccentColor = color.AA2
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) {
if obj.Attributes.Style.Opacity != nil {
shape.Opacity, _ = strconv.ParseFloat(obj.Attributes.Style.Opacity.Value, 64)
if obj.Style.Opacity != nil {
shape.Opacity, _ = strconv.ParseFloat(obj.Style.Opacity.Value, 64)
}
if obj.Attributes.Style.StrokeDash != nil {
shape.StrokeDash, _ = strconv.ParseFloat(obj.Attributes.Style.StrokeDash.Value, 64)
if obj.Style.StrokeDash != nil {
shape.StrokeDash, _ = strconv.ParseFloat(obj.Style.StrokeDash.Value, 64)
}
if obj.Attributes.Style.Fill != nil {
shape.Fill = obj.Attributes.Style.Fill.Value
} else if obj.Attributes.Shape.Value == d2target.ShapeText {
if obj.Style.Fill != nil {
shape.Fill = obj.Style.Fill.Value
} else if obj.Shape.Value == d2target.ShapeText {
shape.Fill = "transparent"
}
if obj.Attributes.Style.FillPattern != nil {
shape.FillPattern = obj.Attributes.Style.FillPattern.Value
if obj.Style.FillPattern != nil {
shape.FillPattern = obj.Style.FillPattern.Value
}
if obj.Attributes.Style.Stroke != nil {
shape.Stroke = obj.Attributes.Style.Stroke.Value
if obj.Style.Stroke != nil {
shape.Stroke = obj.Style.Stroke.Value
}
if obj.Attributes.Style.StrokeWidth != nil {
shape.StrokeWidth, _ = strconv.Atoi(obj.Attributes.Style.StrokeWidth.Value)
if obj.Style.StrokeWidth != nil {
shape.StrokeWidth, _ = strconv.Atoi(obj.Style.StrokeWidth.Value)
}
if obj.Attributes.Style.Shadow != nil {
shape.Shadow, _ = strconv.ParseBool(obj.Attributes.Style.Shadow.Value)
if obj.Style.Shadow != nil {
shape.Shadow, _ = strconv.ParseBool(obj.Style.Shadow.Value)
}
if obj.Attributes.Style.ThreeDee != nil {
shape.ThreeDee, _ = strconv.ParseBool(obj.Attributes.Style.ThreeDee.Value)
if obj.Style.ThreeDee != nil {
shape.ThreeDee, _ = strconv.ParseBool(obj.Style.ThreeDee.Value)
}
if obj.Attributes.Style.Multiple != nil {
shape.Multiple, _ = strconv.ParseBool(obj.Attributes.Style.Multiple.Value)
if obj.Style.Multiple != nil {
shape.Multiple, _ = strconv.ParseBool(obj.Style.Multiple.Value)
}
if obj.Attributes.Style.BorderRadius != nil {
shape.BorderRadius, _ = strconv.Atoi(obj.Attributes.Style.BorderRadius.Value)
if obj.Style.BorderRadius != nil {
shape.BorderRadius, _ = strconv.Atoi(obj.Style.BorderRadius.Value)
}
if obj.Attributes.Style.FontColor != nil {
shape.Color = obj.Attributes.Style.FontColor.Value
if obj.Style.FontColor != nil {
shape.Color = obj.Style.FontColor.Value
}
if obj.Attributes.Style.Italic != nil {
shape.Italic, _ = strconv.ParseBool(obj.Attributes.Style.Italic.Value)
if obj.Style.Italic != nil {
shape.Italic, _ = strconv.ParseBool(obj.Style.Italic.Value)
}
if obj.Attributes.Style.Bold != nil {
shape.Bold, _ = strconv.ParseBool(obj.Attributes.Style.Bold.Value)
if obj.Style.Bold != nil {
shape.Bold, _ = strconv.ParseBool(obj.Style.Bold.Value)
}
if obj.Attributes.Style.Underline != nil {
shape.Underline, _ = strconv.ParseBool(obj.Attributes.Style.Underline.Value)
if obj.Style.Underline != nil {
shape.Underline, _ = strconv.ParseBool(obj.Style.Underline.Value)
}
if obj.Attributes.Style.Font != nil {
shape.FontFamily = obj.Attributes.Style.Font.Value
if obj.Style.Font != nil {
shape.FontFamily = obj.Style.Font.Value
}
if obj.Attributes.Style.DoubleBorder != nil {
shape.DoubleBorder, _ = strconv.ParseBool(obj.Attributes.Style.DoubleBorder.Value)
if obj.Style.DoubleBorder != nil {
shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value)
}
}
func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value)
shape.SetType(obj.Shape.Value)
shape.ID = obj.AbsID()
shape.Classes = obj.Classes
shape.ZIndex = obj.ZIndex
shape.Level = int(obj.Level())
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)
applyStyles(shape, obj)
switch obj.Attributes.Shape.Value {
switch obj.Shape.Value {
case d2target.ShapeCode, d2target.ShapeText:
shape.Language = obj.Attributes.Language
shape.Label = obj.Attributes.Label.Value
shape.Language = obj.Language
shape.Label = obj.Label.Value
case d2target.ShapeClass:
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
@ -177,13 +178,13 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
}
}
if obj.Attributes.Tooltip != nil {
shape.Tooltip = obj.Attributes.Tooltip.Value
if obj.Tooltip != nil {
shape.Tooltip = obj.Tooltip.Value
}
if obj.Attributes.Link != nil {
shape.Link = obj.Attributes.Link.Value
if obj.Link != nil {
shape.Link = obj.Link.Value
}
shape.Icon = obj.Attributes.Icon
shape.Icon = obj.Icon
if obj.IconPosition != nil {
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 {
connection := d2target.BaseConnection()
connection.ID = edge.AbsID()
connection.Classes = edge.Classes
connection.ZIndex = edge.ZIndex
text := edge.Text()
@ -211,7 +213,11 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
}
if edge.SrcArrowhead != nil {
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 {
@ -228,66 +234,70 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
}
if edge.DstArrowhead != nil {
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 {
connection.BorderRadius = 0
}
if edge.Attributes.Style.BorderRadius != nil {
connection.BorderRadius, _ = strconv.ParseFloat(edge.Attributes.Style.BorderRadius.Value, 64)
if edge.Style.BorderRadius != nil {
connection.BorderRadius, _ = strconv.ParseFloat(edge.Style.BorderRadius.Value, 64)
}
if edge.Attributes.Style.Opacity != nil {
connection.Opacity, _ = strconv.ParseFloat(edge.Attributes.Style.Opacity.Value, 64)
if edge.Style.Opacity != nil {
connection.Opacity, _ = strconv.ParseFloat(edge.Style.Opacity.Value, 64)
}
if edge.Attributes.Style.StrokeDash != nil {
connection.StrokeDash, _ = strconv.ParseFloat(edge.Attributes.Style.StrokeDash.Value, 64)
if edge.Style.StrokeDash != nil {
connection.StrokeDash, _ = strconv.ParseFloat(edge.Style.StrokeDash.Value, 64)
}
connection.Stroke = edge.GetStroke(connection.StrokeDash)
if edge.Attributes.Style.Stroke != nil {
connection.Stroke = edge.Attributes.Style.Stroke.Value
if edge.Style.Stroke != nil {
connection.Stroke = edge.Style.Stroke.Value
}
if edge.Attributes.Style.StrokeWidth != nil {
connection.StrokeWidth, _ = strconv.Atoi(edge.Attributes.Style.StrokeWidth.Value)
if edge.Style.StrokeWidth != nil {
connection.StrokeWidth, _ = strconv.Atoi(edge.Style.StrokeWidth.Value)
}
if edge.Attributes.Style.Fill != nil {
connection.Fill = edge.Attributes.Style.Fill.Value
if edge.Style.Fill != nil {
connection.Fill = edge.Style.Fill.Value
}
connection.FontSize = text.FontSize
if edge.Attributes.Style.FontSize != nil {
connection.FontSize, _ = strconv.Atoi(edge.Attributes.Style.FontSize.Value)
if edge.Style.FontSize != nil {
connection.FontSize, _ = strconv.Atoi(edge.Style.FontSize.Value)
}
if edge.Attributes.Style.Animated != nil {
connection.Animated, _ = strconv.ParseBool(edge.Attributes.Style.Animated.Value)
if edge.Style.Animated != nil {
connection.Animated, _ = strconv.ParseBool(edge.Style.Animated.Value)
}
if edge.Attributes.Tooltip != nil {
connection.Tooltip = edge.Attributes.Tooltip.Value
if edge.Tooltip != nil {
connection.Tooltip = edge.Tooltip.Value
}
connection.Icon = edge.Attributes.Icon
connection.Icon = edge.Icon
if edge.Attributes.Style.Italic != nil {
connection.Italic, _ = strconv.ParseBool(edge.Attributes.Style.Italic.Value)
if edge.Style.Italic != nil {
connection.Italic, _ = strconv.ParseBool(edge.Style.Italic.Value)
}
connection.Color = text.GetColor(connection.Italic)
if edge.Attributes.Style.FontColor != nil {
connection.Color = edge.Attributes.Style.FontColor.Value
if edge.Style.FontColor != nil {
connection.Color = edge.Style.FontColor.Value
}
if edge.Attributes.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Attributes.Style.Bold.Value)
if edge.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Style.Bold.Value)
}
if theme != nil && theme.SpecialRules.Mono {
connection.FontFamily = "mono"
}
if edge.Attributes.Style.Font != nil {
connection.FontFamily = edge.Attributes.Style.Font.Value
if edge.Style.Font != nil {
connection.FontFamily = edge.Style.Font.Value
}
connection.Label = text.Text
connection.LabelWidth = text.Dimensions.Width

View file

@ -16,6 +16,7 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
@ -231,7 +232,7 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil)
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 {
t.Fatal(err)
}

View file

@ -1,6 +1,7 @@
package d2graph
import (
"context"
"errors"
"fmt"
"math"
@ -9,6 +10,9 @@ import (
"strconv"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
@ -52,10 +56,9 @@ type Graph struct {
func NewGraph() *Graph {
d := &Graph{}
d.Root = &Object{
Graph: d,
Parent: nil,
Children: make(map[string]*Object),
Attributes: &Attributes{},
Graph: d,
Parent: nil,
Children: make(map[string]*Object),
}
return d
}
@ -67,6 +70,8 @@ func (g *Graph) RootBoard() *Graph {
return g
}
type LayoutGraph func(context.Context, *Graph) error
// TODO consider having different Scalar types
// Right now we'll hold any types in Value and just convert, e.g. floats
type Scalar struct {
@ -84,16 +89,13 @@ type Object struct {
// IDVal: yes'"
//
// ID allows joining on . naively and construct a valid D2 key path
ID string `json:"id"`
IDVal string `json:"id_val"`
Map *d2ast.Map `json:"-"`
LabelDimensions d2target.TextDimensions `json:"label_dimensions"`
References []Reference `json:"references,omitempty"`
ID string `json:"id"`
IDVal string `json:"id_val"`
Map *d2ast.Map `json:"-"`
References []Reference `json:"references,omitempty"`
*geo.Box `json:"box,omitempty"`
LabelPosition *string `json:"labelPosition,omitempty"`
LabelWidth *int `json:"labelWidth,omitempty"`
LabelHeight *int `json:"labelHeight,omitempty"`
IconPosition *string `json:"iconPosition,omitempty"`
Class *d2target.Class `json:"class,omitempty"`
@ -102,20 +104,22 @@ type Object struct {
Children map[string]*Object `json:"-"`
ChildrenArray []*Object `json:"-"`
Attributes *Attributes `json:"attributes,omitempty"`
Attributes `json:"attributes"`
ZIndex int `json:"zIndex"`
}
type Attributes struct {
Label Scalar `json:"label"`
Label Scalar `json:"label"`
LabelDimensions d2target.TextDimensions `json:"labelDimensions"`
Style Style `json:"style"`
Icon *url.URL `json:"icon,omitempty"`
Tooltip *Scalar `json:"tooltip,omitempty"`
Link *Scalar `json:"link,omitempty"`
Width *Scalar `json:"width,omitempty"`
Height *Scalar `json:"height,omitempty"`
WidthAttr *Scalar `json:"width,omitempty"`
HeightAttr *Scalar `json:"height,omitempty"`
Top *Scalar `json:"top,omitempty"`
Left *Scalar `json:"left,omitempty"`
@ -129,6 +133,35 @@ type Attributes struct {
Direction Scalar `json:"direction"`
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
@ -151,25 +184,33 @@ func (r Reference) InEdge() bool {
}
type Style struct {
Opacity *Scalar `json:"opacity,omitempty"`
Stroke *Scalar `json:"stroke,omitempty"`
Fill *Scalar `json:"fill,omitempty"`
FillPattern *Scalar `json:"fillPattern,omitempty"`
StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
StrokeDash *Scalar `json:"strokeDash,omitempty"`
BorderRadius *Scalar `json:"borderRadius,omitempty"`
Shadow *Scalar `json:"shadow,omitempty"`
ThreeDee *Scalar `json:"3d,omitempty"`
Multiple *Scalar `json:"multiple,omitempty"`
Font *Scalar `json:"font,omitempty"`
FontSize *Scalar `json:"fontSize,omitempty"`
FontColor *Scalar `json:"fontColor,omitempty"`
Animated *Scalar `json:"animated,omitempty"`
Bold *Scalar `json:"bold,omitempty"`
Italic *Scalar `json:"italic,omitempty"`
Underline *Scalar `json:"underline,omitempty"`
Filled *Scalar `json:"filled,omitempty"`
DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
Opacity *Scalar `json:"opacity,omitempty"`
Stroke *Scalar `json:"stroke,omitempty"`
Fill *Scalar `json:"fill,omitempty"`
FillPattern *Scalar `json:"fillPattern,omitempty"`
StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
StrokeDash *Scalar `json:"strokeDash,omitempty"`
BorderRadius *Scalar `json:"borderRadius,omitempty"`
Shadow *Scalar `json:"shadow,omitempty"`
ThreeDee *Scalar `json:"3d,omitempty"`
Multiple *Scalar `json:"multiple,omitempty"`
Font *Scalar `json:"font,omitempty"`
FontSize *Scalar `json:"fontSize,omitempty"`
FontColor *Scalar `json:"fontColor,omitempty"`
Animated *Scalar `json:"animated,omitempty"`
Bold *Scalar `json:"bold,omitempty"`
Italic *Scalar `json:"italic,omitempty"`
Underline *Scalar `json:"underline,omitempty"`
Filled *Scalar `json:"filled,omitempty"`
DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
TextTransform *Scalar `json:"textTransform,omitempty"`
}
// NoneTextTransform will return a boolean if the text should not have any
// transformation applied. This should overwrite theme specific transformations
// like `CapsLock` from the `terminal` theme.
func (s Style) NoneTextTransform() bool {
return s.TextTransform != nil && s.TextTransform.Value == "none"
}
func (s *Style) Apply(key, value string) error {
@ -340,6 +381,14 @@ func (s *Style) Apply(key, value string) error {
return errors.New(`expected "double-border" to be true or false`)
}
s.DoubleBorder.Value = value
case "text-transform":
if s.TextTransform == nil {
break
}
if !go2.Contains(textTransforms, strings.ToLower(value)) {
return fmt.Errorf(`expected "text-transform" to be one of (%s)`, strings.Join(textTransforms, ", "))
}
s.TextTransform.Value = value
default:
return fmt.Errorf("unknown style key: %s", key)
}
@ -363,7 +412,7 @@ func (l ContainerLevel) LabelSize() int {
func (obj *Object) GetFill() string {
level := int(obj.Level())
shape := obj.Attributes.Shape.Value
shape := obj.Shape.Value
if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
return color.N1
@ -442,7 +491,7 @@ func (obj *Object) GetFill() string {
}
func (obj *Object) GetStroke(dashGapSize interface{}) string {
shape := obj.Attributes.Shape.Value
shape := obj.Shape.Value
if strings.EqualFold(shape, d2target.ShapeCode) ||
strings.EqualFold(shape, d2target.ShapeText) {
return color.N1
@ -469,10 +518,10 @@ func (obj *Object) IsContainer() bool {
}
func (obj *Object) HasOutsideBottomLabel() bool {
if obj == nil || obj.Attributes == nil {
if obj == nil {
return false
}
switch obj.Attributes.Shape.Value {
switch obj.Shape.Value {
case d2target.ShapeImage, d2target.ShapePerson:
return true
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 {
if obj.Parent != nil && obj.Parent.ID != "" {
return obj.Parent.AbsID() + "." + obj.ID
@ -495,12 +556,12 @@ func (obj *Object) AbsIDArray() []string {
}
func (obj *Object) Text() *d2target.MText {
isBold := !obj.IsContainer() && obj.Attributes.Shape.Value != "text"
isBold := !obj.IsContainer() && obj.Shape.Value != "text"
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
}
if obj.Attributes.Style.Italic != nil && obj.Attributes.Style.Italic.Value == "true" {
if obj.Style.Italic != nil && obj.Style.Italic.Value == "true" {
isItalic = true
}
fontSize := d2fonts.FONT_SIZE_M
@ -510,14 +571,14 @@ func (obj *Object) Text() *d2target.MText {
}
if obj.OuterSequenceDiagram() == nil {
if obj.IsContainer() {
if obj.IsContainer() && obj.Shape.Value != "text" {
fontSize = obj.Level().LabelSize()
}
} else {
isBold = false
}
if obj.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
if obj.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
}
// Class and Table objects have Label set to header
if obj.Class != nil || obj.SQLTable != nil {
@ -527,12 +588,12 @@ func (obj *Object) Text() *d2target.MText {
isBold = false
}
return &d2target.MText{
Text: obj.Attributes.Label.Value,
Text: obj.Label.Value,
FontSize: fontSize,
IsBold: isBold,
IsItalic: isItalic,
Language: obj.Attributes.Language,
Shape: obj.Attributes.Shape.Value,
Language: obj.Language,
Shape: obj.Shape.Value,
Dimensions: obj.LabelDimensions,
}
@ -547,7 +608,7 @@ func (obj *Object) newObject(id string) *Object {
child := &Object{
ID: id,
IDVal: idval,
Attributes: &Attributes{
Attributes: Attributes{
Label: Scalar{
Value: idval,
},
@ -725,7 +786,7 @@ func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) {
func (obj *Object) ensureChildEdge(ida []string) *Object {
for i := range ida {
switch obj.Attributes.Shape.Value {
switch obj.Shape.Value {
case d2target.ShapeClass, d2target.ShapeSQLTable:
// This will only be called for connecting edges where we want to truncate to the
// 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) {
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
shapeType := strings.ToLower(obj.Shape.Value)
if obj.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[obj.Attributes.Style.Font.Value]
if obj.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[obj.Style.Font.Value]
fontFamily = &f
}
var dims *d2target.TextDimensions
switch shapeType {
case d2target.ShapeText:
if obj.Attributes.Language == "latex" {
if obj.Language == "latex" {
width, height, err := d2latex.Measure(obj.Text().Text)
if err != nil {
return nil, err
}
dims = d2target.NewTextDimensions(width, height)
} else if obj.Attributes.Language != "" {
} else if obj.Language != "" {
var err error
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
if err != nil {
@ -837,7 +898,7 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
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
placeholder := *obj.Text()
placeholder.Text = "Table"
@ -866,10 +927,21 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
labelDims.Height += INNER_LABEL_PADDING
}
switch strings.ToLower(obj.Attributes.Shape.Value) {
switch strings.ToLower(obj.Shape.Value) {
default:
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:
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)
fontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
if obj.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
}
for _, f := range obj.Class.Fields {
@ -923,8 +995,8 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
constraintWidth := 0
colFontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
if obj.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
}
for i := range obj.SQLTable.Columns {
@ -968,18 +1040,24 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
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 {
Index int `json:"index"`
MinWidth int `json:"minWidth"`
MinHeight int `json:"minHeight"`
SrcTableColumnIndex *int `json:"srcTableColumnIndex,omitempty"`
DstTableColumnIndex *int `json:"dstTableColumnIndex,omitempty"`
LabelDimensions d2target.TextDimensions `json:"label_dimensions"`
LabelPosition *string `json:"labelPosition,omitempty"`
LabelPercentage *float64 `json:"labelPercentage,omitempty"`
LabelPosition *string `json:"labelPosition,omitempty"`
LabelPercentage *float64 `json:"labelPercentage,omitempty"`
IsCurve bool `json:"isCurve"`
Route []*geo.Point `json:"route,omitempty"`
@ -993,7 +1071,7 @@ type Edge struct {
DstArrowhead *Attributes `json:"dstArrowhead,omitempty"`
References []EdgeReference `json:"references,omitempty"`
Attributes *Attributes `json:"attributes,omitempty"`
Attributes `json:"attributes,omitempty"`
ZIndex int `json:"zIndex"`
}
@ -1007,6 +1085,10 @@ type EdgeReference struct {
ScopeObj *Object `json:"-"`
}
func (e *Edge) GetAstEdge() *d2ast.Edge {
return e.References[0].Edge
}
func (e *Edge) GetStroke(dashGapSize interface{}) string {
if dashGapSize != 0.0 {
return color.B2
@ -1029,15 +1111,15 @@ func (e *Edge) ArrowString() string {
func (e *Edge) Text() *d2target.MText {
fontSize := d2fonts.FONT_SIZE_M
if e.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(e.Attributes.Style.FontSize.Value)
if e.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(e.Style.FontSize.Value)
}
isBold := false
if e.Attributes.Style.Bold != nil {
isBold, _ = strconv.ParseBool(e.Attributes.Style.Bold.Value)
if e.Style.Bold != nil {
isBold, _ = strconv.ParseBool(e.Style.Bold.Value)
}
return &d2target.MText{
Text: e.Attributes.Label.Value,
Text: e.Label.Value,
FontSize: fontSize,
IsBold: isBold,
IsItalic: true,
@ -1085,7 +1167,7 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
}
e := &Edge{
Attributes: &Attributes{
Attributes: Attributes{
Label: Scalar{
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) {
if src.Attributes.Shape.Value == d2target.ShapeSQLTable {
if src.Shape.Value == d2target.ShapeSQLTable {
if src == dst {
// Ignore edge to column inside table.
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()
dstAbsID := dst.AbsIDArray()
if len(objAbsID)+len(dstID) > len(dstAbsID) {
@ -1179,7 +1261,7 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
}
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 {
return nil, err
}
@ -1269,16 +1351,16 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
var desiredWidth int
var desiredHeight int
if obj.Attributes.Width != nil {
desiredWidth, _ = strconv.Atoi(obj.Attributes.Width.Value)
if obj.WidthAttr != nil {
desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
}
if obj.Attributes.Height != nil {
desiredHeight, _ = strconv.Atoi(obj.Attributes.Height.Value)
if obj.HeightAttr != nil {
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.ShapeSQLTable &&
dslShape != d2target.ShapeClass {
@ -1304,11 +1386,12 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) {
if obj.Attributes.Language != "latex" {
obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.Value)
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
obj.Label.Value = strings.ToUpper(obj.Label.Value)
}
}
obj.ApplyTextTransform()
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil {
@ -1316,19 +1399,9 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
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
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)
if err != nil {
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
if obj.Attributes.Icon != nil {
if obj.Icon != nil {
labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING)
// Evenly pad enough to fit label above icon
if desiredWidth == 0 {
@ -1374,10 +1447,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
switch shapeType {
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.IMAGE_TYPE:
default:
if obj.Attributes.Link != nil {
if obj.Link != nil {
paddingX += 32
}
if obj.Attributes.Tooltip != nil {
if obj.Tooltip != nil {
paddingX += 32
}
}
@ -1406,35 +1479,33 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
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 != "" {
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 != "" {
endpointLabels = append(endpointLabels, edge.DstArrowhead.Label.Value)
}
for _, label := range endpointLabels {
t := edge.Text()
t.Text = label
dims := GetTextDimensions(mtexts, ruler, t, fontFamily)
edge.MinWidth += dims.Width
// Some padding as it's not totally near the end
edge.MinHeight += dims.Height + 5
t.Text = edge.DstArrowhead.Label.Value
dims := GetTextDimensions(mtexts, ruler, t, usedFont)
edge.DstArrowhead.LabelDimensions = *dims
}
if edge.Attributes.Label.Value == "" {
if edge.Label.Value == "" {
continue
}
if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
edge.Attributes.Label.Value = strings.ToUpper(edge.Attributes.Label.Value)
}
usedFont := fontFamily
if edge.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[edge.Attributes.Style.Font.Value]
usedFont = &f
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !edge.Style.NoneTextTransform() {
edge.Label.Value = strings.ToUpper(edge.Label.Value)
}
edge.ApplyTextTransform()
dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont)
if dims == nil {
@ -1442,8 +1513,6 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
edge.LabelDimensions = *dims
edge.MinWidth += dims.Width
edge.MinHeight += dims.Height
}
return nil
}
@ -1454,10 +1523,11 @@ func (g *Graph) Texts() []*d2target.MText {
capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock
for _, obj := range g.Objects {
if obj.Attributes.Label.Value != "" {
if obj.Label.Value != "" {
obj.ApplyTextTransform()
text := obj.Text()
if capsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) {
if obj.Attributes.Language != "latex" {
if capsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
text.Text = strings.ToUpper(text.Text)
}
}
@ -1465,8 +1535,8 @@ func (g *Graph) Texts() []*d2target.MText {
}
if obj.Class != nil {
fontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
if obj.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
}
for _, field := range obj.Class.Fields {
texts = appendTextDedup(texts, field.Text(fontSize))
@ -1476,8 +1546,8 @@ func (g *Graph) Texts() []*d2target.MText {
}
} else if obj.SQLTable != nil {
colFontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
if obj.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
}
for _, column := range obj.SQLTable.Columns {
for _, t := range column.Texts(colFontSize) {
@ -1487,9 +1557,10 @@ func (g *Graph) Texts() []*d2target.MText {
}
}
for _, edge := range g.Edges {
if edge.Attributes.Label.Value != "" {
if edge.Label.Value != "" {
edge.ApplyTextTransform()
text := edge.Text()
if capsLock {
if capsLock && !edge.Style.NoneTextTransform() {
text.Text = strings.ToUpper(text.Text)
}
texts = appendTextDedup(texts, text)
@ -1521,19 +1592,26 @@ var ReservedKeywords2 map[string]struct{}
// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
"tooltip": {},
"link": {},
"near": {},
"width": {},
"height": {},
"direction": {},
"top": {},
"left": {},
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
"tooltip": {},
"link": {},
"near": {},
"width": {},
"height": {},
"direction": {},
"top": {},
"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
@ -1554,12 +1632,13 @@ var StyleKeywords = map[string]struct{}{
"border-radius": {},
// Only for text
"font": {},
"font-size": {},
"font-color": {},
"bold": {},
"italic": {},
"underline": {},
"font": {},
"font-size": {},
"font-color": {},
"bold": {},
"italic": {},
"underline": {},
"text-transform": {},
// Only for shapes
"shadow": {},
@ -1597,6 +1676,8 @@ var FillPatterns = []string{
"paper",
}
var textTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
// BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{
"layers": {},

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"
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 {

View file

@ -265,57 +265,67 @@ func CompareSerializedObject(obj, other *Object) error {
}
}
if obj.Attributes != nil && other.Attributes == nil {
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) {
if d2target.IsShape(obj.Shape.Value) != d2target.IsShape(other.Shape.Value) {
return fmt.Errorf(
"shapes differ: obj=%s, other=%s",
obj.Shape.Value,
other.Shape.Value,
)
}
if obj.Icon == nil && other.Icon != nil {
return fmt.Errorf("other does not have an icon")
} else if obj.Icon != nil && other.Icon == nil {
return fmt.Errorf("obj does not have an icon")
}
if obj.Direction.Value != other.Direction.Value {
return fmt.Errorf(
"directions differ: obj=%s, other=%s",
obj.Direction.Value,
other.Direction.Value,
)
}
if obj.Label.Value != other.Label.Value {
return fmt.Errorf(
"labels differ: obj=%s, other=%s",
obj.Label.Value,
other.Label.Value,
)
}
if obj.NearKey != nil {
if other.NearKey == nil {
return fmt.Errorf("other does not have near")
}
objKey := strings.Join(Key(obj.NearKey), ".")
deserKey := strings.Join(Key(other.NearKey), ".")
if objKey != deserKey {
return fmt.Errorf(
"shapes differ: obj=%s, other=%s",
obj.Attributes.Shape.Value,
other.Attributes.Shape.Value,
"near differs: obj=%s, other=%s",
objKey,
deserKey,
)
}
} else if other.NearKey != nil {
return fmt.Errorf("other should not have near")
}
if obj.Attributes.Icon == nil && other.Attributes.Icon != nil {
return fmt.Errorf("other does not have an icon")
} else if obj.Attributes.Icon != nil && other.Attributes.Icon == nil {
return fmt.Errorf("obj does not have an icon")
}
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.Attributes.Direction.Value != other.Attributes.Direction.Value {
return fmt.Errorf(
"directions differ: obj=%s, other=%s",
obj.Attributes.Direction.Value,
other.Attributes.Direction.Value,
)
}
if obj.Attributes.Label.Value != other.Attributes.Label.Value {
return fmt.Errorf(
"labels differ: obj=%s, other=%s",
obj.Attributes.Label.Value,
other.Attributes.Label.Value,
)
}
if obj.Attributes.NearKey != nil {
if other.Attributes.NearKey == nil {
return fmt.Errorf("other does not have near")
}
objKey := strings.Join(Key(obj.Attributes.NearKey), ".")
deserKey := strings.Join(Key(other.Attributes.NearKey), ".")
if objKey != deserKey {
return fmt.Errorf(
"near differs: obj=%s, other=%s",
objKey,
deserKey,
)
}
} else if other.Attributes.NearKey != nil {
return fmt.Errorf("other should not have near")
}
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 {
@ -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
}
@ -408,27 +388,11 @@ func CompareSerializedEdge(edge, other *Edge) error {
)
}
if edge.MinWidth != other.MinWidth {
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 {
if edge.Label.Value != other.Label.Value {
return fmt.Errorf(
"labels differ: edge=%s, other=%s",
edge.Attributes.Label.Value,
other.Attributes.Label.Value,
edge.Label.Value,
other.Label.Value,
)
}
@ -442,7 +406,7 @@ func CompareSerializedEdge(edge, other *Edge) error {
if edge.LabelDimensions.Height != other.LabelDimensions.Height {
return fmt.Errorf(
"label hieght differs: edge=%d, other=%d",
"label height differs: edge=%d, other=%d",
edge.LabelDimensions.Height,
other.LabelDimensions.Height,
)

View file

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

View file

@ -19,6 +19,7 @@ func TestCompile(t *testing.T) {
t.Parallel()
t.Run("fields", testCompileFields)
t.Run("classes", testCompileClasses)
t.Run("edges", testCompileEdges)
t.Run("layers", testCompileLayers)
t.Run("scenarios", testCompileScenarios)
@ -101,10 +102,12 @@ func makeScalar(v interface{}) *d2ir.Scalar {
bv := &big.Rat{}
bv.SetFloat64(v)
s.Value = &d2ast.Number{
Raw: fmt.Sprint(v),
Value: bv,
}
case int:
s.Value = &d2ast.Number{
Raw: fmt.Sprint(v),
Value: big.NewRat(int64(v), 1),
}
case string:
@ -379,6 +382,20 @@ scenarios: {
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)
}
@ -431,9 +448,8 @@ scenarios: {
shape: sql_table
hey: int {constraint: primary_key}
}`)
assert.ErrorString(t, err, `TestCompile/steps/steps_panic.d2:6:3: invalid scenario
TestCompile/steps/steps_panic.d2:7:3: invalid scenario
TestCompile/steps/steps_panic.d2:2:3: invalid step`)
assert.ErrorString(t, err, `TestCompile/steps/steps_panic.d2:3:3: invalid step
TestCompile/steps/steps_panic.d2:7:3: invalid scenario`)
},
},
{
@ -490,3 +506,154 @@ steps: {
}
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
}
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 {
for len(ida) > 0 && ida[0] == "_" {
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"`)
}
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) == "" {
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 {
for {
n = n.Parent()
@ -1154,3 +1177,27 @@ func (m *Map) Equal(n2 Node) bool {
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
switch g.Root.Attributes.Direction.Value {
switch g.Root.Direction.Value {
case "down":
rootAttrs.rankdir = "TB"
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 {
continue
}
if obj.LabelHeight != nil {
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight+label.PADDING)
if obj.HasLabel() {
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))
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)
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
// 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
height := obj.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil {
if obj.HasOutsideBottomLabel() || obj.Attributes.Icon != nil {
height += float64(*obj.LabelHeight) + label.PADDING
if obj.HasLabel() {
if obj.HasOutsideBottomLabel() || obj.Icon != nil {
height += float64(obj.LabelDimensions.Height) + label.PADDING
}
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))
@ -189,7 +189,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// We want to leave some gap between multiple edges
if numEdges > 1 {
switch g.Root.Attributes.Direction.Value {
switch g.Root.Direction.Value {
case "down", "up", "":
width += EDGE_LABEL_GAP
case "left", "right":
@ -235,20 +235,20 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
obj.Width = dn.Width
obj.Height = dn.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil {
if obj.HasLabel() {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
// remove the extra height we added to the node when passing to dagre
obj.Height -= float64(*obj.LabelHeight) + label.PADDING
} else if obj.Attributes.Icon != nil {
obj.Height -= float64(obj.LabelDimensions.Height) + label.PADDING
} else if obj.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if obj.Attributes.Icon != nil {
if obj.Icon != nil {
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
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 {
if obj.LabelHeight == nil || len(obj.ChildrenArray) == 0 {
if !obj.HasLabel() || len(obj.ChildrenArray) == 0 {
continue
}
// 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
// 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
@ -373,7 +373,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// 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) {
// 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
}
}
@ -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)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.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.Shape.Value)], edge.Dst.Box)
// trace the edge to the specific shape's border
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
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
labelWidth := float64(*edge.Dst.LabelWidth)
labelHeight := float64(*edge.Dst.LabelHeight)
labelWidth := float64(edge.Dst.LabelDimensions.Width)
labelHeight := float64(edge.Dst.LabelDimensions.Height)
labelTL := label.Position(*edge.Dst.LabelPosition).
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
// compile needs to assign edge label positions
if edge.Attributes.Label.Value != "" {
if edge.Label.Value != "" {
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}

View file

@ -109,6 +109,8 @@ type elkOpts struct {
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
SelfLoopDistribution string `json:"elk.layered.edgeRouting.selfLoopDistribution,omitempty"`
NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
ContentAlignment string `json:"elk.contentAlignment,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":
elkGraph.LayoutOptions.Direction = "DOWN"
case "up":
@ -198,7 +204,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
if incoming >= 2 || outgoing >= 2 {
switch g.Root.Attributes.Direction.Value {
switch g.Root.Direction.Value {
case "right", "left":
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
default:
@ -208,11 +214,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
height := obj.Height
width := obj.Width
if obj.LabelWidth != nil && obj.LabelHeight != nil {
if obj.HasOutsideBottomLabel() || obj.Attributes.Icon != nil {
height += float64(*obj.LabelHeight) + label.PADDING
if obj.HasLabel() {
if obj.HasOutsideBottomLabel() || obj.Icon != nil {
height += float64(obj.LabelDimensions.Height) + label.PADDING
}
width = go2.Max(width, float64(*obj.LabelWidth))
width = go2.Max(width, float64(obj.LabelDimensions.Width))
}
n := &ELKNode{
@ -239,6 +245,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
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 {
case "DOWN", "UP":
@ -249,14 +258,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if n.LayoutOptions.Padding == DefaultOpts.Padding {
labelHeight := 0
if obj.LabelHeight != nil {
labelHeight = *obj.LabelHeight + label.PADDING
if obj.HasLabel() {
labelHeight = obj.LabelDimensions.Height + label.PADDING
}
n.Height += 100 + float64(labelHeight)
n.Width += 100
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)
paddingTop := n.Height - s.GetInnerBox().Height
@ -264,7 +273,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
n.Width -= 100
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
}
@ -277,15 +286,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
} else {
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{
Text: obj.Attributes.Label.Value,
Width: float64(*obj.LabelWidth),
Height: float64(*obj.LabelHeight),
Text: obj.Label.Value,
Width: float64(obj.LabelDimensions.Width),
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()},
Targets: []string{edge.Dst.AbsID()},
}
if edge.Attributes.Label.Value != "" {
if edge.Label.Value != "" {
e.Labels = append(e.Labels, &ELKLabel{
Text: edge.Attributes.Label.Value,
Text: edge.Label.Value,
Width: float64(edge.LabelDimensions.Width),
Height: float64(edge.LabelDimensions.Height),
LayoutOptions: &elkOpts{
@ -391,19 +400,19 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
obj.Width = n.Width
obj.Height = n.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil {
if obj.HasLabel() {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
obj.Height -= float64(*obj.LabelHeight) + label.PADDING
} else if obj.Attributes.Icon != nil {
obj.Height -= float64(obj.LabelDimensions.Height) + label.PADDING
} else if obj.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if obj.Attributes.Icon != nil {
if obj.Icon != nil {
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
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
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.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.Shape.Value)], edge.Dst.Box)
// trace the edge to the specific shape's border
points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+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))
}
@ -519,7 +528,7 @@ func deleteBends(g *d2graph.Graph) {
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)
// 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
}
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
// 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
const NEAR_OBJECT = "near_object"

View file

@ -10,44 +10,62 @@ import (
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
)
const pad = 20
// 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 {
if len(constantNears) == 0 {
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
if len(constantNearGraphs) == 0 {
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.
// 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
for _, processCenters := range []bool{true, false} {
for _, obj := range constantNears {
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "-center") {
for _, tempGraph := range constantNearGraphs {
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))
}
}
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)
}
}
}
dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
// These shapes skipped core layout, which means they also skipped label placements
for _, obj := range constantNears {
if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
} else if obj.Attributes.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
for _, subObject := range tempGraph.Objects {
// `obj` already been replaced above by `place(obj)`
if subObject == obj {
continue
}
subObject.TopLeft.X += dx
subObject.TopLeft.Y += dy
}
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)
w := br.X - tl.X
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":
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":
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":
return br.X + pad, tl.Y - obj.Height - pad
x, y = br.X+pad, tl.Y-obj.Height-pad
break
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":
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":
return tl.X - obj.Width - pad, br.Y + pad
x, y = tl.X-obj.Width-pad, br.Y+pad
break
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":
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
// 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++ {
obj := g.Objects[i]
if obj.Attributes.NearKey == nil {
if obj.NearKey == nil {
continue
}
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
_, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
if isKey {
continue
}
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]]
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isConst {
nears = append(nears, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
descendantObjects, edges := pluckObjAndEdges(g, obj)
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--
delete(obj.Parent.Children, strings.ToLower(obj.ID))
for i := 0; i < len(obj.Parent.ChildrenArray); i++ {
@ -104,9 +170,38 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2gra
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
@ -122,10 +217,10 @@ func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
y2 := math.Inf(-1)
for _, obj := range g.Objects {
if obj.Attributes.NearKey != nil {
if obj.NearKey != nil {
// 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
switch d2graph.Key(obj.Attributes.NearKey)[0] {
switch d2graph.Key(obj.NearKey)[0] {
case "top-center", "bottom-center":
x1 = math.Min(x1, obj.TopLeft.X)
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)
}
} else {
if obj.OuterNearContainer() != nil {
continue
}
x1 = math.Min(x1, obj.TopLeft.X)
y1 = math.Min(y1, obj.TopLeft.Y)
x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
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)
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)
y1 = math.Min(y1, labelTL.Y)
x2 = math.Max(x2, labelTL.X+float64(*obj.LabelWidth))
y2 = math.Max(y2, labelTL.Y+float64(*obj.LabelHeight))
x2 = math.Max(x2, labelTL.X+float64(obj.LabelDimensions.Width))
y2 = math.Max(y2, labelTL.Y+float64(obj.LabelDimensions.Height))
}
}
}

View file

@ -13,6 +13,33 @@ import (
"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) {
objectsToRemove := make(map[*d2graph.Object]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 {
continue
}
if obj.Attributes.Shape.Value != d2target.ShapeSequenceDiagram {
if obj.Shape.Value != d2target.ShapeSequenceDiagram {
queue = append(queue, obj.ChildrenArray...)
continue
}
@ -69,33 +96,6 @@ func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]
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
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
var edges []*d2graph.Edge
@ -154,11 +154,11 @@ func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, obj
objects = g.Objects
}
for _, obj := range objects {
if _, exists := sequenceDiagrams[obj.AbsID()]; !exists {
sd, exists := sequenceDiagrams[obj.AbsID()]
if !exists {
continue
}
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
sd.shift(
@ -171,22 +171,22 @@ func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, obj
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range sd.actors {
obj.Children[child.ID] = child
obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
for _, child := range sd.groups {
if child.Parent.AbsID() == obj.AbsID() {
obj.Children[child.ID] = child
obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
}
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].messages...)
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].lifelines...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].actors...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].notes...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].groups...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].spans...)
g.Edges = append(g.Edges, sd.messages...)
g.Edges = append(g.Edges, sd.lifelines...)
g.Objects = append(g.Objects, sd.actors...)
g.Objects = append(g.Objects, sd.notes...)
g.Objects = append(g.Objects, sd.groups...)
g.Objects = append(g.Objects, sd.spans...)
}
// 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)
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"})
assert.True(t, has)
@ -217,14 +217,14 @@ b -> a.t2`
})
// 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 != "" {
t.Fatalf("expected no label for span, got %s", a_t1.Attributes.Label.Value)
if a_t1.Label.Value != "" {
t.Fatalf("expected no label for span, got %s", a_t1.Label.Value)
}
if a_t1.Attributes.Shape.Value != shape.SQUARE_TYPE {
t.Fatalf("expected square shape for span, got %s", a_t1.Attributes.Shape.Value)
if a_t1.Shape.Value != shape.SQUARE_TYPE {
t.Fatalf("expected square shape for span, got %s", a_t1.Shape.Value)
}
if a_t1.Height != b_t1.Height {
@ -323,7 +323,7 @@ container -> c: edge 1
c := g.Root.EnsureChild([]string{"c"})
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 {
if len(g.Objects) != 2 {
@ -378,7 +378,7 @@ container -> c: edge 1
func TestSelfEdges(t *testing.T) {
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.Box = geo.NewBox(nil, 100, 100)
@ -387,7 +387,7 @@ func TestSelfEdges(t *testing.T) {
Src: n1,
Dst: n1,
Index: 0,
Attributes: &d2graph.Attributes{
Attributes: d2graph.Attributes{
Label: d2graph.Scalar{Value: "left to right"},
},
},
@ -414,10 +414,10 @@ func TestSelfEdges(t *testing.T) {
func TestSequenceToDescendant(t *testing.T) {
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.Box = geo.NewBox(nil, 100, 100)
a.Attributes = &d2graph.Attributes{
a.Attributes = d2graph.Attributes{
Shape: d2graph.Scalar{Value: shape.PERSON_TYPE},
}
a_t1 := a.EnsureChild([]string{"t1"})
@ -425,15 +425,13 @@ func TestSequenceToDescendant(t *testing.T) {
g.Edges = []*d2graph.Edge{
{
Src: a,
Dst: a_t1,
Index: 0,
Attributes: &d2graph.Attributes{},
Src: a,
Dst: a_t1,
Index: 0,
}, {
Src: a_t1,
Dst: a,
Index: 0,
Attributes: &d2graph.Attributes{},
Src: a_t1,
Dst: a,
Index: 0,
},
}

View file

@ -110,7 +110,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
sd.objectRank[actor] = rank
if actor.Width < MIN_ACTOR_WIDTH {
dslShape := strings.ToLower(actor.Attributes.Shape.Value)
dslShape := strings.ToLower(actor.Shape.Value)
switch dslShape {
case d2target.ShapePerson, d2target.ShapeOval, d2target.ShapeSquare, d2target.ShapeCircle:
// 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
if child.IsSequenceDiagramNote() {
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.objectRank[child] = rank
child.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
@ -139,8 +139,8 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
} else {
// spans have no labels
// TODO why not? Spans should be able to
child.Attributes.Label = d2graph.Scalar{Value: ""}
child.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
child.Label = d2graph.Scalar{Value: ""}
child.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
sd.spans = append(sd.spans, child)
sd.objectRank[child] = rank
}
@ -186,8 +186,8 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
sd.yStep += VERTICAL_PAD
sd.maxActorHeight += VERTICAL_PAD
if sd.root.LabelHeight != nil {
sd.maxActorHeight += float64(*sd.root.LabelHeight)
if sd.root.HasLabel() {
sd.maxActorHeight += float64(sd.root.LabelDimensions.Height)
}
return sd, nil
@ -282,11 +282,11 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
}
func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) {
if group.LabelHeight == nil {
if !group.HasLabel() {
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 {
return
}
@ -339,8 +339,8 @@ func (sd *sequenceDiagram) placeActors() {
if actor.HasOutsideBottomLabel() {
actor.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
yOffset = sd.maxActorHeight - actor.Height
if actor.LabelHeight != nil {
yOffset -= float64(*actor.LabelHeight)
if actor.HasLabel() {
yOffset -= float64(actor.LabelDimensions.Height)
}
} else {
actor.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
@ -381,20 +381,26 @@ func (sd *sequenceDiagram) addLifelineEdges() {
for _, actor := range sd.actors {
actorBottom := actor.Center()
actorBottom.Y = actor.TopLeft.Y + actor.Height
if *actor.LabelPosition == string(label.OutsideBottomCenter) && actor.LabelHeight != nil {
actorBottom.Y += float64(*actor.LabelHeight) + LIFELINE_LABEL_PAD
if *actor.LabelPosition == string(label.OutsideBottomCenter) && actor.HasLabel() {
actorBottom.Y += float64(actor.LabelDimensions.Height) + LIFELINE_LABEL_PAD
}
actorLifelineEnd := actor.Center()
actorLifelineEnd.Y = endY
style := d2graph.Style{
StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)},
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: d2graph.Style{
StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)},
StrokeWidth: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_WIDTH)},
},
},
Src: actor,
SrcArrow: false,
Attributes: d2graph.Attributes{Style: style},
Src: actor,
SrcArrow: false,
Dst: &d2graph.Object{
ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
},
@ -575,7 +581,7 @@ func (sd *sequenceDiagram) routeMessages() error {
}
messageOffset += sd.yStep
if message.Attributes.Label.Value != "" {
if message.Label.Value != "" {
message.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}

View file

@ -10,6 +10,7 @@ import (
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
@ -68,14 +69,23 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
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 {
return nil, err
}
err = d2near.Layout(ctx, g, constantNears)
err = d2near.Layout(ctx, g, constantNearGraphs)
if err != nil {
return nil, err
}
@ -110,7 +120,7 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
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 {
return opts.Layout, nil
} else if os.Getenv("D2_LAYOUT") == "dagre" {

View file

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

View file

@ -50,11 +50,11 @@ func TestCreate(t *testing.T) {
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.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)
if g.Objects[0].Label.MapKey.Value.Unbox() != nil {
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" {
t.Fatalf("expected g.Objects[0].Attributes.Label.Node.Key to be square: %#v", g.Objects[0].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[0].Label.MapKey.Key) != "square" {
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" {
t.Fatalf("bad absolute ID: %#v", g.Objects[2].AbsID())
}
if d2format.Format(g.Objects[2].Attributes.Label.MapKey.Key) != "b.c.square" {
t.Fatalf("bad mapkey: %#v", g.Objects[2].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[2].Label.MapKey.Key) != "b.c.square" {
t.Fatalf("bad mapkey: %#v", g.Objects[2].Label.MapKey.Key)
}
if g.Objects[2].Attributes.Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected nil mapkey value: %#v", g.Objects[2].Attributes.Label.MapKey.Value)
if g.Objects[2].Label.MapKey.Value.Unbox() != nil {
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" {
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 {
t.Fatalf("expected g.Objects[1].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[1].Attributes.Label.MapKey.Value)
if g.Objects[1].Label.MapKey.Value.Unbox() != nil {
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" {
t.Fatalf("expected g.Objects[1].Attributes.Label.Node.Key to be square 2: %#v", g.Objects[1].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[1].Label.MapKey.Key) != "square 2" {
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" {
t.Fatalf("expected g.Objects[3].ID to be square: %#v", g.Objects[3])
}
if g.Objects[3].Attributes.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)
if g.Objects[3].Label.MapKey.Value.Unbox() != nil {
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" {
t.Fatalf("expected g.Objects[3].Attributes.Label.Node.Key to be square: %#v", g.Objects[3].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[3].Label.MapKey.Key) != "square" {
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" {
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 {
t.Fatalf("expected g.Objects[4].Attributes.Label.Node.Value.Unbox() == nil: %#v", g.Objects[4].Attributes.Label.MapKey.Value)
if g.Objects[4].Label.MapKey.Value.Unbox() != nil {
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" {
t.Fatalf("expected g.Objects[4].Attributes.Label.Node.Key to be square 2: %#v", g.Objects[4].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[4].Label.MapKey.Key) != "square 2" {
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" {
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" {
t.Fatalf("expected g.Objects[13].Attributes.Label.Node.Key to be square 11: %#v", g.Objects[13].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[13].Label.MapKey.Key) != "square 11" {
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" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.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)
if g.Objects[0].Label.MapKey.Value.Unbox() != nil {
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" {
t.Fatalf("expected g.Objects[0].Attributes.Label.Node.Key to be square: %#v", g.Objects[0].Attributes.Label.MapKey.Key)
if d2format.Format(g.Objects[0].Label.MapKey.Key) != "square" {
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" {
t.Fatalf("expected g.Edges[0].Dst.ID == y: %#v", g.Edges[0].Dst.ID)
}
if g.Edges[0].Attributes.Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label.Value == two: %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "two" {
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" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == square: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value != d2target.ShapeSquare {
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" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == circle: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value != d2target.ShapeCircle {
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 {
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 {
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 {
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 {
t.Fatal(err, f)
}
@ -670,7 +670,7 @@ func TestSet(t *testing.T) {
if len(g.AST.Nodes) != 1 {
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 {
t.Fatal(err, f)
}
@ -689,7 +689,7 @@ square.style.opacity: 0.2
if len(g.AST.Nodes) != 2 {
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 {
t.Fatal(err, f)
}
@ -859,6 +859,85 @@ square.style.opacity: 0.2
exp: `square: {
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" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value == d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == square: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value == d2target.ShapeSquare {
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" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Attributes.Shape.Value == d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value == square: %#v", g.Objects[0].Attributes.Shape.Value)
if g.Objects[0].Shape.Value == d2target.ShapeSquare {
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" {
t.Fatal(g.Objects[0])
}
if g.Objects[0].Attributes.Label.Value == "I am deeply CONCERNED and I want something GOOD for BREAKFAST!" {
t.Fatal(g.Objects[0].Attributes.Label.Value)
if g.Objects[0].Label.Value == "I am deeply CONCERNED and I want something GOOD for BREAKFAST!" {
t.Fatal(g.Objects[0].Label.Value)
}
},
},
@ -1036,8 +1115,8 @@ z: {
if len(g.Edges) != 2 {
t.Fatalf("expected 2 edges: %#v", g.Edges)
}
if g.Edges[0].Attributes.Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Attributes.Label.Value == two: %#v", g.Edges[0].Attributes.Label.Value)
if g.Edges[0].Label.Value != "two" {
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 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Icon.String() != "https://icons.terrastruct.com/essentials/087-menu.svg" {
t.Fatal(g.Objects[0].Attributes.Icon.String())
if g.Objects[0].Icon.String() != "https://icons.terrastruct.com/essentials/087-menu.svg" {
t.Fatal(g.Objects[0].Icon.String())
}
},
},
@ -1133,7 +1212,7 @@ z: {
assert.JSON(t, 3, len(g.Objects))
assert.JSON(t, 1, len(g.Edges))
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" {
t.Fatal(g.Edges[0].Src.ID)
}
if g.Edges[0].Attributes.Style.Opacity.Value != "0.4" {
t.Fatal(g.Edges[0].Attributes.Style.Opacity.Value)
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Edges[0].Style.Opacity.Value)
}
},
},

View file

@ -8,7 +8,7 @@ import (
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
const NEAR_OBJECT PluginFeature = "near_object"
@ -33,19 +33,19 @@ func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error {
}
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 {
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 {
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 {
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
if obj.NearKey != nil {
_, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
if isKey {
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)

View file

@ -1,7 +1,7 @@
# d2fonts
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:
- `./ttf/Helvetica-Bold.ttf`

View file

@ -64,9 +64,10 @@ const (
FONT_SIZE_XXL = 28
FONT_SIZE_XXXL = 32
FONT_STYLE_REGULAR FontStyle = "regular"
FONT_STYLE_BOLD FontStyle = "bold"
FONT_STYLE_ITALIC FontStyle = "italic"
FONT_STYLE_REGULAR FontStyle = "regular"
FONT_STYLE_BOLD FontStyle = "bold"
FONT_STYLE_SEMIBOLD FontStyle = "semibold"
FONT_STYLE_ITALIC FontStyle = "italic"
SourceSansPro FontFamily = "SourceSansPro"
SourceCodePro FontFamily = "SourceCodePro"
@ -86,6 +87,7 @@ var FontSizes = []int{
var FontStyles = []FontStyle{
FONT_STYLE_REGULAR,
FONT_STYLE_BOLD,
FONT_STYLE_SEMIBOLD,
FONT_STYLE_ITALIC,
}
@ -101,6 +103,9 @@ var sourceSansProRegularBase64 string
//go:embed encoded/SourceSansPro-Bold.txt
var sourceSansProBoldBase64 string
//go:embed encoded/SourceSansPro-Semibold.txt
var sourceSansProSemiboldBase64 string
//go:embed encoded/SourceSansPro-Italic.txt
var sourceSansProItalicBase64 string
@ -110,6 +115,9 @@ var sourceCodeProRegularBase64 string
//go:embed encoded/SourceCodePro-Bold.txt
var sourceCodeProBoldBase64 string
//go:embed encoded/SourceCodePro-Semibold.txt
var sourceCodeProSemiboldBase64 string
//go:embed encoded/SourceCodePro-Italic.txt
var sourceCodeProItalicBase64 string
@ -135,6 +143,10 @@ func init() {
Family: SourceSansPro,
Style: FONT_STYLE_BOLD,
}: sourceSansProBoldBase64,
{
Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD,
}: sourceSansProSemiboldBase64,
{
Family: SourceSansPro,
Style: FONT_STYLE_ITALIC,
@ -147,6 +159,10 @@ func init() {
Family: SourceCodePro,
Style: FONT_STYLE_BOLD,
}: sourceCodeProBoldBase64,
{
Family: SourceCodePro,
Style: FONT_STYLE_SEMIBOLD,
}: sourceCodeProSemiboldBase64,
{
Family: SourceCodePro,
Style: FONT_STYLE_ITALIC,
@ -164,6 +180,11 @@ func init() {
Family: HandDrawn,
Style: FONT_STYLE_BOLD,
}: fuzzyBubblesBoldBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_SEMIBOLD,
// This font has no semibold, so just reuse bold
}: fuzzyBubblesBoldBase64,
}
for k, v := range FontEncodings {
@ -195,6 +216,14 @@ func init() {
Family: SourceCodePro,
Style: FONT_STYLE_BOLD,
}] = 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")
if err != nil {
panic(err)
@ -211,6 +240,14 @@ func init() {
Family: SourceSansPro,
Style: FONT_STYLE_BOLD,
}] = 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")
if err != nil {
panic(err)
@ -239,6 +276,10 @@ func init() {
Family: HandDrawn,
Style: FONT_STYLE_BOLD,
}] = b
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_SEMIBOLD,
}] = b
}
var D2_FONT_TO_FAMILY = map[string]FontFamily{
@ -259,7 +300,7 @@ func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
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)
regularFont := Font{
@ -316,6 +357,24 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF []byte) (*FontFam
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)
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
diamond -> oval -> circle
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 {
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 {
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 {
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 {
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 {
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 {
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 {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
}
.d2-3205202238 .text-bold {
font-family: "d2-3205202238-font-bold";
.d2-916646398 .text-bold {
font-family: "d2-916646398-font-bold";
}
@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==");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
@ -21,78 +21,78 @@
opacity: 0.5;
}
.d2-3205202238 .fill-N1{fill:#0A0F25;}
.d2-3205202238 .fill-N2{fill:#676C7E;}
.d2-3205202238 .fill-N3{fill:#9499AB;}
.d2-3205202238 .fill-N4{fill:#CFD2DD;}
.d2-3205202238 .fill-N5{fill:#DEE1EB;}
.d2-3205202238 .fill-N6{fill:#EEF1F8;}
.d2-3205202238 .fill-N7{fill:#FFFFFF;}
.d2-3205202238 .fill-B1{fill:#0D32B2;}
.d2-3205202238 .fill-B2{fill:#0D32B2;}
.d2-3205202238 .fill-B3{fill:#E3E9FD;}
.d2-3205202238 .fill-B4{fill:#E3E9FD;}
.d2-3205202238 .fill-B5{fill:#EDF0FD;}
.d2-3205202238 .fill-B6{fill:#F7F8FE;}
.d2-3205202238 .fill-AA2{fill:#4A6FF3;}
.d2-3205202238 .fill-AA4{fill:#EDF0FD;}
.d2-3205202238 .fill-AA5{fill:#F7F8FE;}
.d2-3205202238 .fill-AB4{fill:#EDF0FD;}
.d2-3205202238 .fill-AB5{fill:#F7F8FE;}
.d2-3205202238 .stroke-N1{stroke:#0A0F25;}
.d2-3205202238 .stroke-N2{stroke:#676C7E;}
.d2-3205202238 .stroke-N3{stroke:#9499AB;}
.d2-3205202238 .stroke-N4{stroke:#CFD2DD;}
.d2-3205202238 .stroke-N5{stroke:#DEE1EB;}
.d2-3205202238 .stroke-N6{stroke:#EEF1F8;}
.d2-3205202238 .stroke-N7{stroke:#FFFFFF;}
.d2-3205202238 .stroke-B1{stroke:#0D32B2;}
.d2-3205202238 .stroke-B2{stroke:#0D32B2;}
.d2-3205202238 .stroke-B3{stroke:#E3E9FD;}
.d2-3205202238 .stroke-B4{stroke:#E3E9FD;}
.d2-3205202238 .stroke-B5{stroke:#EDF0FD;}
.d2-3205202238 .stroke-B6{stroke:#F7F8FE;}
.d2-3205202238 .stroke-AA2{stroke:#4A6FF3;}
.d2-3205202238 .stroke-AA4{stroke:#EDF0FD;}
.d2-3205202238 .stroke-AA5{stroke:#F7F8FE;}
.d2-3205202238 .stroke-AB4{stroke:#EDF0FD;}
.d2-3205202238 .stroke-AB5{stroke:#F7F8FE;}
.d2-3205202238 .background-color-N1{background-color:#0A0F25;}
.d2-3205202238 .background-color-N2{background-color:#676C7E;}
.d2-3205202238 .background-color-N3{background-color:#9499AB;}
.d2-3205202238 .background-color-N4{background-color:#CFD2DD;}
.d2-3205202238 .background-color-N5{background-color:#DEE1EB;}
.d2-3205202238 .background-color-N6{background-color:#EEF1F8;}
.d2-3205202238 .background-color-N7{background-color:#FFFFFF;}
.d2-3205202238 .background-color-B1{background-color:#0D32B2;}
.d2-3205202238 .background-color-B2{background-color:#0D32B2;}
.d2-3205202238 .background-color-B3{background-color:#E3E9FD;}
.d2-3205202238 .background-color-B4{background-color:#E3E9FD;}
.d2-3205202238 .background-color-B5{background-color:#EDF0FD;}
.d2-3205202238 .background-color-B6{background-color:#F7F8FE;}
.d2-3205202238 .background-color-AA2{background-color:#4A6FF3;}
.d2-3205202238 .background-color-AA4{background-color:#EDF0FD;}
.d2-3205202238 .background-color-AA5{background-color:#F7F8FE;}
.d2-3205202238 .background-color-AB4{background-color:#EDF0FD;}
.d2-3205202238 .background-color-AB5{background-color:#F7F8FE;}
.d2-3205202238 .color-N1{color:#0A0F25;}
.d2-3205202238 .color-N2{color:#676C7E;}
.d2-3205202238 .color-N3{color:#9499AB;}
.d2-3205202238 .color-N4{color:#CFD2DD;}
.d2-3205202238 .color-N5{color:#DEE1EB;}
.d2-3205202238 .color-N6{color:#EEF1F8;}
.d2-3205202238 .color-N7{color:#FFFFFF;}
.d2-3205202238 .color-B1{color:#0D32B2;}
.d2-3205202238 .color-B2{color:#0D32B2;}
.d2-3205202238 .color-B3{color:#E3E9FD;}
.d2-3205202238 .color-B4{color:#E3E9FD;}
.d2-3205202238 .color-B5{color:#EDF0FD;}
.d2-3205202238 .color-B6{color:#F7F8FE;}
.d2-3205202238 .color-AA2{color:#4A6FF3;}
.d2-3205202238 .color-AA4{color:#EDF0FD;}
.d2-3205202238 .color-AA5{color:#F7F8FE;}
.d2-3205202238 .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 .fill-N1{fill:#0A0F25;}
.d2-916646398 .fill-N2{fill:#676C7E;}
.d2-916646398 .fill-N3{fill:#9499AB;}
.d2-916646398 .fill-N4{fill:#CFD2DD;}
.d2-916646398 .fill-N5{fill:#DEE1EB;}
.d2-916646398 .fill-N6{fill:#EEF1F8;}
.d2-916646398 .fill-N7{fill:#FFFFFF;}
.d2-916646398 .fill-B1{fill:#0D32B2;}
.d2-916646398 .fill-B2{fill:#0D32B2;}
.d2-916646398 .fill-B3{fill:#E3E9FD;}
.d2-916646398 .fill-B4{fill:#E3E9FD;}
.d2-916646398 .fill-B5{fill:#EDF0FD;}
.d2-916646398 .fill-B6{fill:#F7F8FE;}
.d2-916646398 .fill-AA2{fill:#4A6FF3;}
.d2-916646398 .fill-AA4{fill:#EDF0FD;}
.d2-916646398 .fill-AA5{fill:#F7F8FE;}
.d2-916646398 .fill-AB4{fill:#EDF0FD;}
.d2-916646398 .fill-AB5{fill:#F7F8FE;}
.d2-916646398 .stroke-N1{stroke:#0A0F25;}
.d2-916646398 .stroke-N2{stroke:#676C7E;}
.d2-916646398 .stroke-N3{stroke:#9499AB;}
.d2-916646398 .stroke-N4{stroke:#CFD2DD;}
.d2-916646398 .stroke-N5{stroke:#DEE1EB;}
.d2-916646398 .stroke-N6{stroke:#EEF1F8;}
.d2-916646398 .stroke-N7{stroke:#FFFFFF;}
.d2-916646398 .stroke-B1{stroke:#0D32B2;}
.d2-916646398 .stroke-B2{stroke:#0D32B2;}
.d2-916646398 .stroke-B3{stroke:#E3E9FD;}
.d2-916646398 .stroke-B4{stroke:#E3E9FD;}
.d2-916646398 .stroke-B5{stroke:#EDF0FD;}
.d2-916646398 .stroke-B6{stroke:#F7F8FE;}
.d2-916646398 .stroke-AA2{stroke:#4A6FF3;}
.d2-916646398 .stroke-AA4{stroke:#EDF0FD;}
.d2-916646398 .stroke-AA5{stroke:#F7F8FE;}
.d2-916646398 .stroke-AB4{stroke:#EDF0FD;}
.d2-916646398 .stroke-AB5{stroke:#F7F8FE;}
.d2-916646398 .background-color-N1{background-color:#0A0F25;}
.d2-916646398 .background-color-N2{background-color:#676C7E;}
.d2-916646398 .background-color-N3{background-color:#9499AB;}
.d2-916646398 .background-color-N4{background-color:#CFD2DD;}
.d2-916646398 .background-color-N5{background-color:#DEE1EB;}
.d2-916646398 .background-color-N6{background-color:#EEF1F8;}
.d2-916646398 .background-color-N7{background-color:#FFFFFF;}
.d2-916646398 .background-color-B1{background-color:#0D32B2;}
.d2-916646398 .background-color-B2{background-color:#0D32B2;}
.d2-916646398 .background-color-B3{background-color:#E3E9FD;}
.d2-916646398 .background-color-B4{background-color:#E3E9FD;}
.d2-916646398 .background-color-B5{background-color:#EDF0FD;}
.d2-916646398 .background-color-B6{background-color:#F7F8FE;}
.d2-916646398 .background-color-AA2{background-color:#4A6FF3;}
.d2-916646398 .background-color-AA4{background-color:#EDF0FD;}
.d2-916646398 .background-color-AA5{background-color:#F7F8FE;}
.d2-916646398 .background-color-AB4{background-color:#EDF0FD;}
.d2-916646398 .background-color-AB5{background-color:#F7F8FE;}
.d2-916646398 .color-N1{color:#0A0F25;}
.d2-916646398 .color-N2{color:#676C7E;}
.d2-916646398 .color-N3{color:#9499AB;}
.d2-916646398 .color-N4{color:#CFD2DD;}
.d2-916646398 .color-N5{color:#DEE1EB;}
.d2-916646398 .color-N6{color:#EEF1F8;}
.d2-916646398 .color-N7{color:#FFFFFF;}
.d2-916646398 .color-B1{color:#0D32B2;}
.d2-916646398 .color-B2{color:#0D32B2;}
.d2-916646398 .color-B3{color:#E3E9FD;}
.d2-916646398 .color-B4{color:#E3E9FD;}
.d2-916646398 .color-B5{color:#EDF0FD;}
.d2-916646398 .color-B6{color:#F7F8FE;}
.d2-916646398 .color-AA2{color:#4A6FF3;}
.d2-916646398 .color-AA4{color:#EDF0FD;}
.d2-916646398 .color-AA5{color:#F7F8FE;}
.d2-916646398 .color-AB4{color:#EDF0FD;}
.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>
</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/styles"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
@ -39,8 +37,7 @@ import (
)
const (
DEFAULT_PADDING = 100
MIN_ARROWHEAD_STROKE_WIDTH = 2
DEFAULT_PADDING = 100
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 {
arrowhead := connection.DstArrow
if !isTarget {
arrowhead = connection.SrcArrow
}
strokeWidth := float64(connection.StrokeWidth)
width, height := arrowheadDimensions(arrowhead, strokeWidth)
width, height := arrowhead.Dimensions(strokeWidth)
var path string
switch arrowhead {
@ -515,7 +469,12 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
if connection.Opacity != 1.0 {
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
if connection.SrcArrow != d2target.NoArrowhead {
id := arrowheadMarkerID(false, connection)
@ -615,38 +574,40 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
fmt.Fprint(writer, textEl.Render())
}
length := geo.Route(connection.Route).Length()
if connection.SrcLabel != "" {
// 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.SrcLabel != nil && connection.SrcLabel.Label != "" {
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.SrcLabel.Label, false))
}
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))
if connection.DstLabel != nil && connection.DstLabel.Label != "" {
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel.Label, true))
}
fmt.Fprintf(writer, `</g>`)
return
}
func renderArrowheadLabel(connection d2target.Connection, text string, position, width, height float64) string {
labelTL := label.UnlockedTop.GetPointOnRoute(connection.Route, float64(connection.StrokeWidth), position, width, height)
func renderArrowheadLabel(connection d2target.Connection, text string, isDst bool) string {
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.X = labelTL.X + width/2
textEl.Y = labelTL.Y + float64(connection.FontSize)
textEl.X = baselineCenter.X
textEl.Y = baselineCenter.Y
textEl.Fill = d2target.FG_COLOR
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)
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) {
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))
width := float64(targetShape.Width)
height := float64(targetShape.Height)
@ -1322,6 +1287,9 @@ func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape,
mdEl := d2themes.NewThemableElement("div")
mdEl.ClassName = "md"
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, `</foreignObject></g>`)
} 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(
buf,
source,
@ -1753,10 +1735,12 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
if hasMarkdown {
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-bold", fmt.Sprintf("%s-font-bold", 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-semibold", fmt.Sprintf("%s-font-semibold", diagramHash))
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)))
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 {
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 {
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 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
@ -240,35 +239,30 @@
.md h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
font-weight: 400;
line-height: 1.25;
font-family: "font-regular";
font-family: "font-semibold";
}
.md h2 {
font-weight: 600;
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.md h3 {
font-weight: 600;
font-size: 1.25em;
}
.md h4 {
font-weight: 600;
font-size: 1em;
}
.md h5 {
font-weight: 600;
font-size: 0.875em;
}
.md h6 {
font-weight: 600;
font-size: 0.85em;
color: var(--color-fg-muted);
}
@ -465,7 +459,7 @@
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
font-family: "font-semibold";
}
.md dl dd {
@ -474,7 +468,7 @@
}
.md table th {
font-weight: 600;
font-family: "font-semibold";
}
.md table th,
@ -677,7 +671,7 @@
}
.md .csv-data th {
font-weight: 600;
font-family: "font-semibold";
background: var(--color-canvas-subtle);
border-top: 0;
}

View file

@ -29,6 +29,9 @@ const (
BG_COLOR = color.N7
FG_COLOR = color.N1
MIN_ARROWHEAD_STROKE_WIDTH = 2
ARROWHEAD_PADDING = 2.
)
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)
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}
@ -286,8 +303,12 @@ func (diagram Diagram) GetCorpus() string {
}
for _, c := range diagram.Connections {
corpus += c.Label
corpus += c.SrcLabel
corpus += c.DstLabel
if c.SrcLabel != nil {
corpus += c.SrcLabel.Label
}
if c.DstLabel != nil {
corpus += c.DstLabel.Label
}
}
return corpus
@ -305,6 +326,8 @@ type Shape struct {
ID string `json:"id"`
Type string `json:"type"`
Classes []string `json:"classes,omitempty"`
Pos Point `json:"pos"`
Width int `json:"width"`
Height int `json:"height"`
@ -425,13 +448,15 @@ func BaseShape() *Shape {
type Connection struct {
ID string `json:"id"`
Classes []string `json:"classes,omitempty"`
Src string `json:"src"`
SrcArrow Arrowhead `json:"srcArrow"`
SrcLabel string `json:"srcLabel"`
SrcLabel *Text `json:"srcLabel,omitempty"`
Dst string `json:"dst"`
DstArrow Arrowhead `json:"dstArrow"`
DstLabel string `json:"dstLabel"`
DstLabel *Text `json:"dstLabel,omitempty"`
Opacity float64 `json:"opacity"`
StrokeDash float64 `json:"strokeDash"`
@ -502,13 +527,85 @@ func (c Connection) CSSStyle() string {
}
func (c *Connection) GetLabelTopLeft() *geo.Point {
return label.Position(c.LabelPosition).GetPointOnRoute(
point, _ := label.Position(c.LabelPosition).GetPointOnRoute(
c.Route,
float64(c.StrokeWidth),
c.LabelPercentage,
float64(c.LabelWidth),
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 {
@ -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 {
X int `json:"x"`
Y int `json:"y"`

View file

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

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