merge with master
2
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/daily.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/project.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
41
ci/release/changelogs/v0.4.0.md
Normal 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.
|
||||
35
ci/release/changelogs/v0.4.1.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
#### 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
|
|
@ -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
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
413
d2cli/main.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
156
d2graph/serde.go
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
47
d2ir/d2ir.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
116
d2layouts/d2grid/grid_diagram.go
Normal 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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
d2lib/d2.go
|
|
@ -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" {
|
||||
|
|
|
|||
126
d2oracle/edit.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
d2renderers/d2fonts/encoded/SourceCodePro-Semibold.txt
Normal file
1
d2renderers/d2fonts/encoded/SourceSansPro-Semibold.txt
Normal file
BIN
d2renderers/d2fonts/ttf/SourceCodePro-Semibold.ttf
Normal file
BIN
d2renderers/d2fonts/ttf/SourceSansPro-Semibold.ttf
Normal 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"
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
154
d2renderers/d2sketch/testdata/basic/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
110
d2renderers/d2sketch/testdata/long_arrowhead_label/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 63 KiB |
572
d2renderers/d2sketch/testdata/opacity/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 94 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
572
d2renderers/d2sketch/testdata/twitter/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 676 KiB After Width: | Height: | Size: 676 KiB |
|
|
@ -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 > x</text></g>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 657 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 660 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 660 KiB |
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 62 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 102 KiB |
16
d2renderers/d2svg/github-markdown.css
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||