Merge branch 'terrastruct:master' into master

This commit is contained in:
Barry Nolte 2023-11-03 14:47:26 -07:00 committed by GitHub
commit 702a0945d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
841 changed files with 155974 additions and 18341 deletions

View file

@ -31,3 +31,12 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
signed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: git submodule update --init
- run: COLOR=1 ./ci/sub/bin/ensure_signed.sh
env:
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

View file

@ -229,6 +229,7 @@ let us know and we'll be happy to include it here!
- **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter) - **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter)
- **Logseq-D2**: [https://github.com/b-yp/logseq-d2](https://github.com/b-yp/logseq-d2) - **Logseq-D2**: [https://github.com/b-yp/logseq-d2](https://github.com/b-yp/logseq-d2)
- **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2) - **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2)
- **MkDocs Plugin**: [https://github.com/landmaj/mkdocs-d2-plugin](https://github.com/landmaj/mkdocs-d2-plugin)
### Misc ### Misc

View file

@ -4,7 +4,7 @@ cd -- "$(dirname "$0")/../.."
. ./ci/sub/lib.sh . ./ci/sub/lib.sh
tag="$(sh_c docker build \ tag="$(sh_c docker build \
--build-arg GOVERSION="1.19.3.linux-$ARCH" \ --build-arg GOVERSION="1.20.8.linux-$ARCH" \
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)" -qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
docker_run \ docker_run \
-e DRY_RUN \ -e DRY_RUN \

View file

@ -1,9 +1,21 @@
#### Features 🚀 #### Features 🚀
- UTF-16 files are automatically detected and supported [#1525](https://github.com/terrastruct/d2/pull/1525) - ELK now routes `sql_table` edges to the exact columns (ty @landmaj) [#1681](https://github.com/terrastruct/d2/pull/1681)
#### Improvements 🧹 #### Improvements 🧹
- Grid cells can now contain nested edges [#1629](https://github.com/terrastruct/d2/pull/1629)
- Edges can now go across constant nears, sequence diagrams, and grids including nested ones. [#1631](https://github.com/terrastruct/d2/pull/1631)
- All vars defined in a scope are accessible everywhere in that scope, i.e., an object can use a var defined after itself. [#1695](https://github.com/terrastruct/d2/pull/1695)
#### Bugfixes ⛑️ #### Bugfixes ⛑️
- Fixes `d2 fmt` to format all files passed as arguments rather than first non-formatted only [#1523](https://github.com/terrastruct/d2/issues/1523) - Fixes a bug calculating grid height with only grid-rows and different horizontal-gap and vertical-gap values. [#1646](https://github.com/terrastruct/d2/pull/1646)
- Grid layout now accounts for each cell's outside labels and icons [#1624](https://github.com/terrastruct/d2/pull/1624)
- Grid layout now accounts for labels wider or taller than the shape and fixes default label positions for image grid cells. [#1670](https://github.com/terrastruct/d2/pull/1670)
- Fixes a panic with a spread substitution in a glob map [#1643](https://github.com/terrastruct/d2/pull/1643)
- Fixes use of `null` in `sql_table` constraints (ty @landmaj) [#1660](https://github.com/terrastruct/d2/pull/1660)
- Fixes elk growing shapes with width/height set [#1679](https://github.com/terrastruct/d2/pull/1679)
- Adds a compiler error when accidentally using an arrowhead on a shape [#1686](https://github.com/terrastruct/d2/pull/1686)
- Correctly reports errors from invalid values set by globs. [#1691](https://github.com/terrastruct/d2/pull/1691)
- Fixes panic when spread substitution referenced a nonexistant var. [#1695](https://github.com/terrastruct/d2/pull/1695)

View file

@ -0,0 +1,52 @@
The globs feature underwent a major rewrite and is now almost finalized.
### Before
Previously, globs would evaluate once on all the shapes and connections declared above it. So if you wanted to set everything red, you had to add the line at the bottom.
```d2
x
y
*.style.fill: red
```
### Now
```d2
*.style.fill: red
x
y
```
We still have one more release in 0.6 series to add filters to globs, so stay tuned.
You might also be interested to know that grid cells can now have connections between them! Source code for this diagram [here](https://github.com/terrastruct/d2/blob/master/e2etests/testdata/files/simple_grid_edges.d2).
![267854495-bc0a5456-3618-4d46-84db-f211ffb5246a](https://github.com/terrastruct/d2/assets/3120367/bb7b01a5-5473-401d-baf7-9faf2e7cfbe8)
#### Features 🚀
- UTF-16 files are automatically detected and supported [#1525](https://github.com/terrastruct/d2/pull/1525)
- Grid diagrams can now have simple connections between top-level cells [#1586](https://github.com/terrastruct/d2/pull/1586)
#### Improvements 🧹
- Globs are lazily-evaluated [#1552](https://github.com/terrastruct/d2/pull/1552)
- Latex blocks includes Mathjax's ASM extension [#1544](https://github.com/terrastruct/d2/pull/1544)
- `font-color` works on Markdown [#1546](https://github.com/terrastruct/d2/pull/1546)
- `font-color` works on arrowheads [#1582](https://github.com/terrastruct/d2/pull/1582)
- CLI failure message includes input path [#1617](https://github.com/terrastruct/d2/pull/1617)
#### Bugfixes ⛑️
- `d2 fmt` formats all files passed as arguments rather than just the first non-formatted (thank you @maxbrunet) [#1523](https://github.com/terrastruct/d2/issues/1523)
- Fixes Markdown cropping last element in mixed-element blocks (e.g. em and strong) [#1543](https://github.com/terrastruct/d2/issues/1543)
- Adds compiler error for non-blockstring empty labels [#1590](https://github.com/terrastruct/d2/issues/1590)
- Prevents multiple constant nears overlapping in some cases [#1591](https://github.com/terrastruct/d2/issues/1591)
- Fixes crash from empty nested grid [#1594](https://github.com/terrastruct/d2/issues/1594)
- `d2fmt` with variable substitution mid-string is formatted correctly [#1611](https://github.com/terrastruct/d2/issues/1611)
- Fixes certain shape IDs not working with dagre [#1610](https://github.com/terrastruct/d2/issues/1610)
- Fixes font-size adjustments missing from rendered code shape [#1614](https://github.com/terrastruct/d2/issues/1614)

2
ci/sub

@ -1 +1 @@
Subproject commit 5f8f9b6858a96583654d14397f7ea5ad7f0aea51 Subproject commit 7a2914b504ed0dfca6d2dcd923b660052217cccb

View file

@ -606,6 +606,15 @@ func (m *Map) IsFileMap() bool {
return m.Range.Start.Line == 0 && m.Range.Start.Column == 0 return m.Range.Start.Line == 0 && m.Range.Start.Column == 0
} }
func (m *Map) HasFilter() bool {
for _, n := range m.Nodes {
if n.MapKey != nil && (n.MapKey.Ampersand || n.MapKey.NotAmpersand) {
return true
}
}
return false
}
// TODO: require @ on import values for readability // TODO: require @ on import values for readability
type Key struct { type Key struct {
Range Range `json:"range"` Range Range `json:"range"`
@ -613,6 +622,9 @@ type Key struct {
// Indicates this MapKey is a filter selector. // Indicates this MapKey is a filter selector.
Ampersand bool `json:"ampersand,omitempty"` Ampersand bool `json:"ampersand,omitempty"`
// Indicates this MapKey is a not filter selector.
NotAmpersand bool `json:"not_ampersand,omitempty"`
// At least one of Key and Edges will be set but all four can also be set. // At least one of Key and Edges will be set but all four can also be set.
// The following are all valid MapKeys: // The following are all valid MapKeys:
// Key: // Key:
@ -638,8 +650,13 @@ type Key struct {
Value ValueBox `json:"value"` Value ValueBox `json:"value"`
} }
// TODO maybe need to compare Primary func (mk1 *Key) D2OracleEquals(mk2 *Key) bool {
func (mk1 *Key) Equals(mk2 *Key) bool { if mk1 == nil && mk2 == nil {
return true
}
if (mk1 == nil) || (mk2 == nil) {
return false
}
if mk1.Ampersand != mk2.Ampersand { if mk1.Ampersand != mk2.Ampersand {
return false return false
} }
@ -712,6 +729,104 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
return true return true
} }
func (mk1 *Key) Equals(mk2 *Key) bool {
if mk1 == nil && mk2 == nil {
return true
}
if (mk1 == nil) || (mk2 == nil) {
return false
}
if mk1.Ampersand != mk2.Ampersand {
return false
}
if (mk1.Key == nil) != (mk2.Key == nil) {
return false
}
if (mk1.EdgeIndex == nil) != (mk2.EdgeIndex == nil) {
return false
}
if mk1.EdgeIndex != nil {
if !mk1.EdgeIndex.Equals(mk2.EdgeIndex) {
return false
}
}
if (mk1.EdgeKey == nil) != (mk2.EdgeKey == nil) {
return false
}
if len(mk1.Edges) != len(mk2.Edges) {
return false
}
for i := range mk1.Edges {
if !mk1.Edges[i].Equals(mk2.Edges[i]) {
return false
}
}
if (mk1.Value.Map == nil) != (mk2.Value.Map == nil) {
if mk1.Value.Map != nil && len(mk1.Value.Map.Nodes) > 0 {
return false
}
if mk2.Value.Map != nil && len(mk2.Value.Map.Nodes) > 0 {
return false
}
} else if (mk1.Value.Unbox() == nil) != (mk2.Value.Unbox() == nil) {
return false
}
if mk1.Key != nil {
if len(mk1.Key.Path) != len(mk2.Key.Path) {
return false
}
for i, id := range mk1.Key.Path {
if id.Unbox().ScalarString() != mk2.Key.Path[i].Unbox().ScalarString() {
return false
}
}
}
if mk1.EdgeKey != nil {
if len(mk1.EdgeKey.Path) != len(mk2.EdgeKey.Path) {
return false
}
for i, id := range mk1.EdgeKey.Path {
if id.Unbox().ScalarString() != mk2.EdgeKey.Path[i].Unbox().ScalarString() {
return false
}
}
}
if mk1.Value.Map != nil && len(mk1.Value.Map.Nodes) > 0 {
if len(mk1.Value.Map.Nodes) != len(mk2.Value.Map.Nodes) {
return false
}
for i := range mk1.Value.Map.Nodes {
if !mk1.Value.Map.Nodes[i].MapKey.Equals(mk2.Value.Map.Nodes[i].MapKey) {
return false
}
}
}
if mk1.Value.Unbox() != nil {
if (mk1.Value.ScalarBox().Unbox() == nil) != (mk2.Value.ScalarBox().Unbox() == nil) {
return false
}
if mk1.Value.ScalarBox().Unbox() != nil {
if mk1.Value.ScalarBox().Unbox().ScalarString() != mk2.Value.ScalarBox().Unbox().ScalarString() {
return false
}
}
}
if mk1.Primary.Unbox() != nil {
if (mk1.Primary.Unbox() == nil) != (mk2.Primary.Unbox() == nil) {
return false
}
if mk1.Primary.ScalarString() != mk2.Primary.ScalarString() {
return false
}
}
return true
}
func (mk *Key) SetScalar(scalar ScalarBox) { func (mk *Key) SetScalar(scalar ScalarBox) {
if mk.Value.Unbox() != nil && mk.Value.ScalarBox().Unbox() == nil { if mk.Value.Unbox() != nil && mk.Value.ScalarBox().Unbox() == nil {
mk.Primary = scalar mk.Primary = scalar
@ -720,7 +835,43 @@ func (mk *Key) SetScalar(scalar ScalarBox) {
} }
} }
func (mk *Key) HasQueryGlob() bool { func (mk *Key) HasGlob() bool {
if mk.Key.HasGlob() {
return true
}
for _, e := range mk.Edges {
if e.Src.HasGlob() || e.Dst.HasGlob() {
return true
}
}
if mk.EdgeIndex != nil && mk.EdgeIndex.Glob {
return true
}
if mk.EdgeKey.HasGlob() {
return true
}
return false
}
func (mk *Key) HasTripleGlob() bool {
if mk.Key.HasTripleGlob() {
return true
}
for _, e := range mk.Edges {
if e.Src.HasTripleGlob() || e.Dst.HasTripleGlob() {
return true
}
}
if mk.EdgeIndex != nil && mk.EdgeIndex.Glob {
return true
}
if mk.EdgeKey.HasTripleGlob() {
return true
}
return false
}
func (mk *Key) SupportsGlobFilters() bool {
if mk.Key.HasGlob() && len(mk.Edges) == 0 { if mk.Key.HasGlob() && len(mk.Edges) == 0 {
return true return true
} }
@ -733,6 +884,11 @@ func (mk *Key) HasQueryGlob() bool {
return false return false
} }
func (mk *Key) Copy() *Key {
mk2 := *mk
return &mk2
}
type KeyPath struct { type KeyPath struct {
Range Range `json:"range"` Range Range `json:"range"`
Path []*StringBox `json:"path"` Path []*StringBox `json:"path"`
@ -760,16 +916,16 @@ func (kp *KeyPath) Copy() *KeyPath {
return &kp2 return &kp2
} }
func (kp *KeyPath) HasDoubleGlob() bool { func (kp *KeyPath) Last() *StringBox {
if kp == nil { return kp.Path[len(kp.Path)-1]
return false }
}
for _, el := range kp.Path { func IsDoubleGlob(pattern []string) bool {
if el.UnquotedString != nil && el.ScalarString() == "**" { return len(pattern) == 3 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*"
return true }
}
} func IsTripleGlob(pattern []string) bool {
return false return len(pattern) == 5 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*" && pattern[3] == "" && pattern[4] == "*"
} }
func (kp *KeyPath) HasGlob() bool { func (kp *KeyPath) HasGlob() bool {
@ -784,6 +940,54 @@ func (kp *KeyPath) HasGlob() bool {
return false return false
} }
func (kp *KeyPath) FirstGlob() int {
if kp == nil {
return -1
}
for i, el := range kp.Path {
if el.UnquotedString != nil && len(el.UnquotedString.Pattern) > 0 {
return i
}
}
return -1
}
func (kp *KeyPath) HasTripleGlob() bool {
if kp == nil {
return false
}
for _, el := range kp.Path {
if el.UnquotedString != nil && IsTripleGlob(el.UnquotedString.Pattern) {
return true
}
}
return false
}
func (kp *KeyPath) HasMultiGlob() bool {
if kp == nil {
return false
}
for _, el := range kp.Path {
if el.UnquotedString != nil && (IsDoubleGlob(el.UnquotedString.Pattern) || IsTripleGlob(el.UnquotedString.Pattern)) {
return true
}
}
return false
}
func (kp1 *KeyPath) Equals(kp2 *KeyPath) bool {
if len(kp1.Path) != len(kp2.Path) {
return false
}
for i, id := range kp1.Path {
if id.Unbox().ScalarString() != kp2.Path[i].Unbox().ScalarString() {
return false
}
}
return true
}
type Edge struct { type Edge struct {
Range Range `json:"range"` Range Range `json:"range"`
@ -796,6 +1000,22 @@ type Edge struct {
DstArrow string `json:"dst_arrow"` DstArrow string `json:"dst_arrow"`
} }
func (e1 *Edge) Equals(e2 *Edge) bool {
if !e1.Src.Equals(e2.Src) {
return false
}
if e1.SrcArrow != e2.SrcArrow {
return false
}
if !e1.Dst.Equals(e2.Dst) {
return false
}
if e1.DstArrow != e2.DstArrow {
return false
}
return true
}
type EdgeIndex struct { type EdgeIndex struct {
Range Range `json:"range"` Range Range `json:"range"`
@ -804,6 +1024,16 @@ type EdgeIndex struct {
Glob bool `json:"glob"` Glob bool `json:"glob"`
} }
func (ei1 *EdgeIndex) Equals(ei2 *EdgeIndex) bool {
if ei1.Int != ei2.Int {
return false
}
if ei1.Glob != ei2.Glob {
return false
}
return true
}
type Substitution struct { type Substitution struct {
Range Range `json:"range"` Range Range `json:"range"`
@ -1079,6 +1309,10 @@ func (sb ScalarBox) Unbox() Scalar {
} }
} }
func (sb ScalarBox) ScalarString() string {
return sb.Unbox().ScalarString()
}
// StringBox is used to box String for JSON persistence. // StringBox is used to box String for JSON persistence.
type StringBox struct { type StringBox struct {
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"` UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`

View file

@ -37,6 +37,7 @@ import (
"oss.terrastruct.com/d2/lib/pdf" "oss.terrastruct.com/d2/lib/pdf"
"oss.terrastruct.com/d2/lib/png" "oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/pptx" "oss.terrastruct.com/d2/lib/pptx"
"oss.terrastruct.com/d2/lib/simplelog"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
timelib "oss.terrastruct.com/d2/lib/time" timelib "oss.terrastruct.com/d2/lib/time"
"oss.terrastruct.com/d2/lib/version" "oss.terrastruct.com/d2/lib/version"
@ -334,9 +335,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
_, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, "", *bundleFlag, *forceAppendixFlag, pw.Page) _, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, "", *bundleFlag, *forceAppendixFlag, pw.Page)
if err != nil { if err != nil {
if written { if written {
return fmt.Errorf("failed to fully compile (partial render written): %w", err) return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
} }
return fmt.Errorf("failed to compile: %w", err) return fmt.Errorf("failed to compile %s: %w", ms.HumanPath(inputPath), err)
} }
return nil return nil
} }
@ -434,7 +435,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, la
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
out, err := xgif.AnimatePNGs(ms, pngs, int(animateInterval)) out, err := AnimatePNGs(ms, pngs, int(animateInterval))
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@ -748,10 +749,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
return svg, err return svg, err
} }
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg) cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
l := simplelog.FromCmdLog(ms.Log)
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
if bundle { if bundle {
var bundleErr2 error var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg) svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2) bundleErr = multierr.Combine(bundleErr, bundleErr2)
} }
if forceAppendix && !toPNG { if forceAppendix && !toPNG {
@ -764,11 +767,11 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
if !bundle { if !bundle {
var bundleErr2 error var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg) svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2) bundleErr = multierr.Combine(bundleErr, bundleErr2)
} }
out, err = png.ConvertSVG(ms, page, svg) out, err = ConvertSVG(ms, page, svg)
if err != nil { if err != nil {
return svg, err return svg, err
} }
@ -833,15 +836,17 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
return svg, err return svg, err
} }
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg) cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg) l := simplelog.FromCmdLog(ms.Log)
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2) bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil { if bundleErr != nil {
return svg, bundleErr return svg, bundleErr
} }
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg) pngImg, err := ConvertSVG(ms, page, svg)
if err != nil { if err != nil {
return svg, err return svg, err
} }
@ -933,8 +938,10 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
return nil, err return nil, err
} }
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg) cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg) l := simplelog.FromCmdLog(ms.Log)
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2) bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil { if bundleErr != nil {
return nil, bundleErr return nil, bundleErr
@ -942,7 +949,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg) pngImg, err := ConvertSVG(ms, page, svg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1178,8 +1185,10 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
return nil, nil, err return nil, nil, err
} }
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg) cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg) l := simplelog.FromCmdLog(ms.Log)
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2) bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil { if bundleErr != nil {
return nil, nil, bundleErr return nil, nil, bundleErr
@ -1187,7 +1196,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg) pngImg, err := ConvertSVG(ms, page, svg)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -1218,3 +1227,21 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
return svg, pngs, nil return svg, pngs, nil
} }
func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
cancel := background.Repeat(func() {
ms.Log.Info.Printf("converting to PNG...")
}, time.Second*5)
defer cancel()
return png.ConvertSVG(page, svg)
}
func AnimatePNGs(ms *xmain.State, pngs [][]byte, animIntervalMs int) ([]byte, error) {
cancel := background.Repeat(func() {
ms.Log.Info.Printf("generating GIF...")
}, time.Second*5)
defer cancel()
return xgif.AnimatePNGs(pngs, animIntervalMs)
}

View file

@ -51,6 +51,7 @@ func Compile(p string, r io.Reader, opts *CompileOptions) (*d2graph.Graph, *d2ta
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
g.FS = opts.FS
g.SortObjectsByAST() g.SortObjectsByAST()
g.SortEdgesByAST() g.SortEdgesByAST()
return g, compileConfig(ir), nil return g, compileConfig(ir), nil
@ -81,6 +82,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
if len(c.err.Errors) == 0 { if len(c.err.Errors) == 0 {
c.validateKeys(g.Root, ir) c.validateKeys(g.Root, ir)
} }
c.validateLabels(g)
c.validateNear(g) c.validateNear(g)
c.validateEdges(g) c.validateEdges(g)
@ -175,7 +177,14 @@ type compiler struct {
} }
func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
c.err.Errors = append(c.err.Errors, d2parser.Errorf(n, f, v...).(d2ast.Error)) err := d2parser.Errorf(n, f, v...).(d2ast.Error)
if c.err.ErrorsLookup == nil {
c.err.ErrorsLookup = make(map[d2ast.Error]struct{})
}
if _, ok := c.err.ErrorsLookup[err]; !ok {
c.err.Errors = append(c.err.Errors, err)
c.err.ErrorsLookup[err] = struct{}{}
}
} }
func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) { func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
@ -282,6 +291,10 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
return return
} else if f.Name == "vars" { } else if f.Name == "vars" {
return return
} else if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
c.errorf(f.LastRef().AST(), `%#v can only be used on connections`, f.Name)
return
} else if isReserved { } else if isReserved {
c.compileReserved(&obj.Attributes, f) c.compileReserved(&obj.Attributes, f)
return return
@ -321,21 +334,21 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
} }
for _, fr := range f.References { for _, fr := range f.References {
if fr.Primary() { if fr.Primary() {
if fr.Context.Key.Value.Map != nil { if fr.Context_.Key.Value.Map != nil {
obj.Map = fr.Context.Key.Value.Map obj.Map = fr.Context_.Key.Value.Map
} }
} }
r := d2graph.Reference{ r := d2graph.Reference{
Key: fr.KeyPath, Key: fr.KeyPath,
KeyPathIndex: fr.KeyPathIndex(), KeyPathIndex: fr.KeyPathIndex(),
MapKey: fr.Context.Key, MapKey: fr.Context_.Key,
MapKeyEdgeIndex: fr.Context.EdgeIndex(), MapKeyEdgeIndex: fr.Context_.EdgeIndex(),
Scope: fr.Context.Scope, Scope: fr.Context_.Scope,
ScopeAST: fr.Context.ScopeAST, ScopeAST: fr.Context_.ScopeAST,
} }
if fr.Context.ScopeMap != nil && !d2ir.IsVar(fr.Context.ScopeMap) { if fr.Context_.ScopeMap != nil && !d2ir.IsVar(fr.Context_.ScopeMap) {
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context.ScopeMap)) scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context_.ScopeMap))
r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA) r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA)
} }
obj.References = append(obj.References, r) obj.References = append(obj.References, r)
@ -441,10 +454,15 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
if arr, ok := f.Composite.(*d2ir.Array); ok { if arr, ok := f.Composite.(*d2ir.Array); ok {
for _, constraint := range arr.Values { for _, constraint := range arr.Values {
if scalar, ok := constraint.(*d2ir.Scalar); ok { if scalar, ok := constraint.(*d2ir.Scalar); ok {
switch scalar.Value.(type) {
case *d2ast.Null:
attrs.Constraint = append(attrs.Constraint, "null")
default:
attrs.Constraint = append(attrs.Constraint, scalar.Value.ScalarString()) attrs.Constraint = append(attrs.Constraint, scalar.Value.ScalarString())
} }
} }
} }
}
case "label", "icon": case "label", "icon":
c.compilePosition(attrs, f) c.compilePosition(attrs, f)
default: default:
@ -724,14 +742,14 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
edge.Label.MapKey = e.LastPrimaryKey() edge.Label.MapKey = e.LastPrimaryKey()
for _, er := range e.References { for _, er := range e.References {
r := d2graph.EdgeReference{ r := d2graph.EdgeReference{
Edge: er.Context.Edge, Edge: er.Context_.Edge,
MapKey: er.Context.Key, MapKey: er.Context_.Key,
MapKeyEdgeIndex: er.Context.EdgeIndex(), MapKeyEdgeIndex: er.Context_.EdgeIndex(),
Scope: er.Context.Scope, Scope: er.Context_.Scope,
ScopeAST: er.Context.ScopeAST, ScopeAST: er.Context_.ScopeAST,
} }
if er.Context.ScopeMap != nil && !d2ir.IsVar(er.Context.ScopeMap) { if er.Context_.ScopeMap != nil && !d2ir.IsVar(er.Context_.ScopeMap) {
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context.ScopeMap)) scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context_.ScopeMap))
r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA) r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA)
} }
edge.References = append(edge.References, r) edge.References = append(edge.References, r)
@ -997,6 +1015,22 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
} }
} }
func (c *compiler) validateLabels(g *d2graph.Graph) {
for _, obj := range g.Objects {
if obj.Shape.Value != d2target.ShapeText {
continue
}
if obj.Attributes.Language != "" {
// blockstrings have already been validated
continue
}
if strings.TrimSpace(obj.Label.Value) == "" {
c.errorf(obj.Label.MapKey, "shape text must have a non-empty label")
continue
}
}
}
func (c *compiler) validateNear(g *d2graph.Graph) { func (c *compiler) validateNear(g *d2graph.Graph) {
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.NearKey != nil { if obj.NearKey != nil {
@ -1050,20 +1084,13 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
} }
for _, edge := range g.Edges { for _, edge := range g.Edges {
srcNearContainer := edge.Src.OuterNearContainer() if edge.Src.IsConstantNear() && edge.Dst.IsDescendantOf(edge.Src) {
dstNearContainer := edge.Dst.OuterNearContainer() c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Src.AbsID())
continue
var isSrcNearConst, isDstNearConst bool
if srcNearContainer != nil {
_, isSrcNearConst = d2graph.NearConstants[d2graph.Key(srcNearContainer.NearKey)[0]]
} }
if dstNearContainer != nil { if edge.Dst.IsConstantNear() && edge.Src.IsDescendantOf(edge.Dst) {
_, isDstNearConst = d2graph.NearConstants[d2graph.Key(dstNearContainer.NearKey)[0]] c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Dst.AbsID())
} continue
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")
} }
} }
@ -1071,12 +1098,32 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
func (c *compiler) validateEdges(g *d2graph.Graph) { func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges { for _, edge := range g.Edges {
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil { // edges from a grid to something outside is ok
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet") // grid -> outside : ok
// grid -> grid.cell : not ok
// grid -> grid.cell.inner : not ok
if edge.Src.IsGridDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
c.errorf(edge.GetAstEdge(), "edge from grid diagram %#v cannot enter itself", edge.Src.AbsID())
continue continue
} }
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil { if edge.Dst.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet") c.errorf(edge.GetAstEdge(), "edge from grid diagram %#v cannot enter itself", edge.Dst.AbsID())
continue
}
if edge.Src.Parent.IsGridDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
c.errorf(edge.GetAstEdge(), "edge from grid cell %#v cannot enter itself", edge.Src.AbsID())
continue
}
if edge.Dst.Parent.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
c.errorf(edge.GetAstEdge(), "edge from grid cell %#v cannot enter itself", edge.Dst.AbsID())
continue
}
if edge.Src.IsSequenceDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
c.errorf(edge.GetAstEdge(), "edge from sequence diagram %#v cannot enter itself", edge.Src.AbsID())
continue
}
if edge.Dst.IsSequenceDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
c.errorf(edge.GetAstEdge(), "edge from sequence diagram %#v cannot enter itself", edge.Dst.AbsID())
continue continue
} }
} }

View file

@ -1616,7 +1616,7 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
} }
x -> y 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`, expErr: ``,
}, },
{ {
name: "near_descendant_connect_to_outside", name: "near_descendant_connect_to_outside",
@ -1627,7 +1627,7 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
} }
x.y -> z 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", expErr: "",
}, },
{ {
name: "nested_near_constant", name: "nested_near_constant",
@ -2040,7 +2040,7 @@ b
} }
b -> x.a b -> x.a
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/leaky_sequence.d2:5:1: connections within sequence diagrams can connect only to other objects within the same sequence diagram`, expErr: ``,
}, },
{ {
name: "sequence_scoping", name: "sequence_scoping",
@ -2199,6 +2199,19 @@ ok: {
tassert.Equal(t, []string{"primary_key", "foreign_key"}, table.Columns[1].Constraint) tassert.Equal(t, []string{"primary_key", "foreign_key"}, table.Columns[1].Constraint)
}, },
}, },
{
name: "sql-null-constraint",
text: `x: {
shape: sql_table
a: int {constraint: null}
b: int {constraint: [null]}
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
table := g.Objects[0].SQLTable
tassert.Nil(t, table.Columns[0].Constraint)
tassert.Equal(t, []string{"null"}, table.Columns[1].Constraint)
},
},
{ {
name: "wrong_column_index", name: "wrong_column_index",
text: `Chinchillas: { text: `Chinchillas: {
@ -2476,16 +2489,75 @@ d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must
name: "grid_edge", name: "grid_edge",
text: `hey: { text: `hey: {
grid-rows: 1 grid-rows: 1
a -> b a -> b: ok
} }
c -> hey.b c -> hey.b
hey.a -> c hey.a -> c
hey -> hey.a
hey -> c: ok hey -> c: ok
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:7:1: edge from grid diagram "hey" cannot enter itself`,
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_deeper_edge",
text: `hey: {
grid-rows: 1
a -> b: ok
b: {
c -> d: ok now
c.e -> c.f.g: ok
c.e -> d.h: ok
c -> d.h: ok
}
a: {
grid-columns: 1
e -> f: also ok now
e: {
g -> h: ok
g -> h.h: ok
}
e -> f.i: ok now
e.g -> f.i: ok now
}
a -> b.c: ok now
a.e -> b.c: ok now
a -> a.e: not ok
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_deeper_edge.d2:22:2: edge from grid diagram "hey.a" cannot enter itself`,
},
{
name: "parent_graph_edge_to_descendant",
text: `tl: {
near: top-left
a.b
}
grid: {
grid-rows: 1
cell.c.d
}
seq: {
shape: sequence_diagram
e.f
}
tl -> tl.a: no
tl -> tl.a.b: no
grid-> grid.cell: no
grid-> grid.cell.c: no
grid.cell -> grid.cell.c: no
grid.cell -> grid.cell.c.d: no
seq -> seq.e: no
seq -> seq.e.f: no
`,
expErr: `d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:13:1: edge from constant near "tl" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:14:1: edge from constant near "tl" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:17:1: edge from grid cell "grid.cell" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:18:1: edge from grid cell "grid.cell" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:15:1: edge from grid diagram "grid" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:16:1: edge from grid diagram "grid" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:19:1: edge from sequence diagram "seq" cannot enter itself
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:20:1: edge from sequence diagram "seq" cannot enter itself`,
}, },
{ {
name: "grid_nested", name: "grid_nested",
@ -2625,6 +2697,28 @@ a -> b: { class: [association; one target] }
tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value) tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value)
}, },
}, },
{
name: "var_in_glob",
text: `vars: {
v: {
ok
}
}
x1 -> x2
x*: {
...${v}
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 4, len(g.Objects))
tassert.Equal(t, "x1.ok", g.Objects[0].AbsID())
tassert.Equal(t, "x2.ok", g.Objects[1].AbsID())
tassert.Equal(t, "x1", g.Objects[2].AbsID())
tassert.Equal(t, "x2", g.Objects[3].AbsID())
},
},
{ {
name: "class-shape-class", name: "class-shape-class",
text: `classes: { text: `classes: {
@ -2694,6 +2788,40 @@ object: {
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`, expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`,
}, },
{
name: "text_no_label",
text: `a: "ok" {
shape: text
}
b: " \n " {
shape: text
}
c: "" {
shape: text
}
d: "" {
shape: circle
}
e: " \n "
f: |md |
g: |md
|
`,
expErr: `d2/testdata/d2compiler/TestCompile/text_no_label.d2:14:1: block string cannot be empty
d2/testdata/d2compiler/TestCompile/text_no_label.d2:15:1: block string cannot be empty
d2/testdata/d2compiler/TestCompile/text_no_label.d2:4:1: shape text must have a non-empty label
d2/testdata/d2compiler/TestCompile/text_no_label.d2:7:1: shape text must have a non-empty label`,
},
{
name: "no_arrowheads_in_shape",
text: `x.target-arrowhead.shape: cf-one
y.source-arrowhead.shape: cf-one
`,
expErr: `d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:1:3: "target-arrowhead" can only be used on connections
d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:2:3: "source-arrowhead" can only be used on connections`,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -2745,6 +2873,7 @@ func TestCompile2(t *testing.T) {
t.Run("seqdiagrams", testSeqDiagrams) t.Run("seqdiagrams", testSeqDiagrams)
t.Run("nulls", testNulls) t.Run("nulls", testNulls)
t.Run("vars", testVars) t.Run("vars", testVars)
t.Run("globs", testGlobs)
} }
func testBoards(t *testing.T) { func testBoards(t *testing.T) {
@ -4017,6 +4146,44 @@ z: {
`, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-solo.d2:8:2: cannot substitute composite variable "x" as part of a string`) `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-solo.d2:8:2: cannot substitute composite variable "x" as part of a string`)
}, },
}, },
{
name: "spread-mid-string",
run: func(t *testing.T) {
assertCompile(t, `
vars: {
test: hello
}
mybox: {
label: prefix${test}suffix
}
`, "")
},
},
{
name: "undeclared-var-usage",
run: func(t *testing.T) {
assertCompile(t, `
x: { ...${v} }
`, `d2/testdata/d2compiler/TestCompile2/vars/errors/undeclared-var-usage.d2:2:4: could not resolve variable "v"`)
},
},
{
name: "split-var-usage",
run: func(t *testing.T) {
assertCompile(t, `
x1
vars: {
v: {
style.fill: green
}
}
x1: { ...${v} }
`, ``)
},
},
} }
for _, tc := range tca { for _, tc := range tca {
@ -4032,6 +4199,118 @@ z: {
}) })
} }
func testGlobs(t *testing.T) {
t.Parallel()
tca := []struct {
name string
skip bool
run func(t *testing.T)
}{
{
name: "alixander-lazy-globs-review/1",
run: func(t *testing.T) {
assertCompile(t, `
***.style.fill: yellow
**.shape: circle
*.style.multiple: true
x: {
y
}
layers: {
next: {
a
}
}
`, "")
},
},
{
name: "alixander-lazy-globs-review/2",
run: func(t *testing.T) {
assertCompile(t, `
**.style.fill: yellow
scenarios: {
b: {
a -> b
}
}
`, "")
},
},
{
name: "alixander-lazy-globs-review/3",
run: func(t *testing.T) {
assertCompile(t, `
***: {
c: d
}
***: {
style.fill: red
}
table: {
shape: sql_table
a: b
}
class: {
shape: class
a: b
}
`, "")
},
},
{
name: "double-glob-err-val",
run: func(t *testing.T) {
assertCompile(t, `
**: {
label: hi
label.near: center
}
x: {
a -> b
}
`, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-err-val.d2:4:3: invalid "near" field`)
},
},
{
name: "double-glob-override-err-val",
run: func(t *testing.T) {
assertCompile(t, `
(** -> **)[*]: {
label.near: top-center
}
(** -> **)[*]: {
label.near: invalid
}
x: {
a -> b
}
`, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-override-err-val.d2:6:2: invalid "near" field`)
},
},
}
for _, tc := range tca {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.skip {
t.SkipNow()
}
tc.run(t)
})
}
}
func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) { func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) {
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil) g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil)

View file

@ -213,6 +213,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
LabelWidth: edge.SrcArrowhead.LabelDimensions.Width, LabelWidth: edge.SrcArrowhead.LabelDimensions.Width,
LabelHeight: edge.SrcArrowhead.LabelDimensions.Height, LabelHeight: edge.SrcArrowhead.LabelDimensions.Height,
} }
if edge.SrcArrowhead.Style.FontColor != nil {
connection.SrcLabel.Color = edge.SrcArrowhead.Style.FontColor.Value
}
} }
} }
if edge.DstArrow { if edge.DstArrow {
@ -228,6 +231,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
LabelWidth: edge.DstArrowhead.LabelDimensions.Width, LabelWidth: edge.DstArrowhead.LabelDimensions.Width,
LabelHeight: edge.DstArrowhead.LabelDimensions.Height, LabelHeight: edge.DstArrowhead.LabelDimensions.Height,
} }
if edge.DstArrowhead.Style.FontColor != nil {
connection.DstLabel.Color = edge.DstArrowhead.Style.FontColor.Value
}
} }
} }
if theme != nil && theme.SpecialRules.NoCornerRadius { if theme != nil && theme.SpecialRules.NoCornerRadius {

View file

@ -17,9 +17,8 @@ import (
"oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter" "oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
@ -235,7 +234,8 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil) err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err) assert.JSON(t, nil, err)
err = d2sequence.Layout(ctx, g, d2grid.Layout(ctx, g, d2dagrelayout.DefaultLayout)) graphInfo := d2layouts.NestedGraphInfo(g.Root)
err = d2layouts.LayoutNested(ctx, g, graphInfo, d2dagrelayout.DefaultLayout)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -810,6 +810,25 @@ steps: {
step-1-content step-1-content
} }
} }
`,
},
{
name: "substitution_mid_string",
in: `vars: {
test: hello
}
mybox: {
label: prefix${test}suffix
}
`,
exp: `vars: {
test: hello
}
mybox: {
label: prefix${test}suffix
}
`, `,
}, },
} }

View file

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"math" "math"
"net/url" "net/url"
"sort" "sort"
@ -36,6 +37,7 @@ const DEFAULT_SHAPE_SIZE = 100.
const MIN_SHAPE_SIZE = 5 const MIN_SHAPE_SIZE = 5
type Graph struct { type Graph struct {
FS fs.FS `json:"-"`
Parent *Graph `json:"-"` Parent *Graph `json:"-"`
Name string `json:"name"` Name string `json:"name"`
// IsFolderOnly indicates a board or scenario itself makes no modifications from its // IsFolderOnly indicates a board or scenario itself makes no modifications from its
@ -55,6 +57,9 @@ type Graph struct {
Steps []*Graph `json:"steps,omitempty"` Steps []*Graph `json:"steps,omitempty"`
Theme *d2themes.Theme `json:"theme,omitempty"` Theme *d2themes.Theme `json:"theme,omitempty"`
// Object.Level uses the location of a nested graph
RootLevel int `json:"rootLevel,omitempty"`
} }
func NewGraph() *Graph { func NewGraph() *Graph {
@ -527,7 +532,7 @@ func (obj *Object) GetStroke(dashGapSize interface{}) string {
func (obj *Object) Level() ContainerLevel { func (obj *Object) Level() ContainerLevel {
if obj.Parent == nil { if obj.Parent == nil {
return 0 return ContainerLevel(obj.Graph.RootLevel)
} }
return 1 + obj.Parent.Level() return 1 + obj.Parent.Level()
} }
@ -1085,6 +1090,21 @@ func (obj *Object) OuterNearContainer() *Object {
return nil return nil
} }
func (obj *Object) IsConstantNear() bool {
if obj.NearKey == nil {
return false
}
keyPath := Key(obj.NearKey)
// interesting if there is a shape with id=top-left, then top-left isn't treated a constant near
_, isKey := obj.Graph.Root.HasChild(keyPath)
if isKey {
return false
}
_, isConst := NearConstants[keyPath[0]]
return isConst
}
type Edge struct { type Edge struct {
Index int `json:"index"` Index int `json:"index"`
@ -1164,6 +1184,13 @@ func (e *Edge) Text() *d2target.MText {
} }
} }
func (e *Edge) Move(dx, dy float64) {
for _, p := range e.Route {
p.X += dx
p.Y += dy
}
}
func (e *Edge) AbsID() string { func (e *Edge) AbsID() string {
srcIDA := e.Src.AbsIDArray() srcIDA := e.Src.AbsIDArray()
dstIDA := e.Dst.AbsIDArray() dstIDA := e.Dst.AbsIDArray()
@ -1198,10 +1225,6 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
src := obj.ensureChildEdge(srcID) src := obj.ensureChildEdge(srcID)
dst := obj.ensureChildEdge(dstID) dst := obj.ensureChildEdge(dstID)
if src.OuterSequenceDiagram() != dst.OuterSequenceDiagram() {
return nil, errors.New("connections within sequence diagrams can connect only to other objects within the same sequence diagram")
}
e := &Edge{ e := &Edge{
Attributes: Attributes{ Attributes: Attributes{
Label: Scalar{ Label: Scalar{
@ -1898,7 +1921,7 @@ func (g *Graph) PrintString() string {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
fmt.Fprint(buf, "Objects: [") fmt.Fprint(buf, "Objects: [")
for _, obj := range g.Objects { for _, obj := range g.Objects {
fmt.Fprintf(buf, "%#v @(%v)", obj.AbsID(), obj.TopLeft.ToString()) fmt.Fprintf(buf, "%v, ", obj.AbsID())
} }
fmt.Fprint(buf, "]") fmt.Fprint(buf, "]")
return buf.String() return buf.String()

View file

@ -14,3 +14,33 @@ func (obj *Object) ClosestGridDiagram() *Object {
} }
return obj.Parent.ClosestGridDiagram() return obj.Parent.ClosestGridDiagram()
} }
func (obj *Object) ClosestGridCell() *Object {
if obj == nil {
return nil
}
// grid cells can be a nested grid diagram
if obj.Parent.IsGridDiagram() {
return obj
}
return obj.Parent.ClosestGridCell()
}
// TopGridDiagram returns the least nested (outermost) grid diagram
func (obj *Object) TopGridDiagram() *Object {
if obj == nil {
return nil
}
var gd *Object
if obj.IsGridDiagram() {
gd = obj
}
curr := obj.Parent
for curr != nil {
if curr.IsGridDiagram() {
gd = curr
}
curr = curr.Parent
}
return gd
}

View file

@ -1,6 +1,7 @@
package d2graph package d2graph
import ( import (
"math"
"sort" "sort"
"strings" "strings"
@ -26,7 +27,7 @@ func (obj *Object) MoveWithDescendantsTo(x, y float64) {
obj.MoveWithDescendants(dx, dy) obj.MoveWithDescendants(dx, dy)
} }
func (parent *Object) removeChild(child *Object) { func (parent *Object) RemoveChild(child *Object) {
delete(parent.Children, strings.ToLower(child.ID)) delete(parent.Children, strings.ToLower(child.ID))
for i := 0; i < len(parent.ChildrenArray); i++ { for i := 0; i < len(parent.ChildrenArray); i++ {
if parent.ChildrenArray[i] == child { if parent.ChildrenArray[i] == child {
@ -41,6 +42,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
descendantObjects, edges := pluckObjAndEdges(g, obj) descendantObjects, edges := pluckObjAndEdges(g, obj)
tempGraph := NewGraph() tempGraph := NewGraph()
tempGraph.RootLevel = int(obj.Level()) - 1
tempGraph.Root.ChildrenArray = []*Object{obj} tempGraph.Root.ChildrenArray = []*Object{obj}
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
@ -50,7 +52,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
tempGraph.Objects = descendantObjects tempGraph.Objects = descendantObjects
tempGraph.Edges = edges tempGraph.Edges = edges
obj.Parent.removeChild(obj) obj.Parent.RemoveChild(obj)
obj.Parent = tempGraph.Root obj.Parent = tempGraph.Root
return tempGraph return tempGraph
@ -59,7 +61,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) { func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) {
for i := 0; i < len(g.Edges); i++ { for i := 0; i < len(g.Edges); i++ {
edge := g.Edges[i] edge := g.Edges[i]
if edge.Src == obj || edge.Dst == obj { if edge.Src.IsDescendantOf(obj) && edge.Dst.IsDescendantOf(obj) {
edges = append(edges, edge) edges = append(edges, edge)
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...) g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
i-- i--
@ -68,15 +70,10 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge
for i := 0; i < len(g.Objects); i++ { for i := 0; i < len(g.Objects); i++ {
temp := g.Objects[i] temp := g.Objects[i]
if temp.AbsID() == obj.AbsID() { if temp.IsDescendantOf(obj) {
descendantsObjects = append(descendantsObjects, obj) descendantsObjects = append(descendantsObjects, temp)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...) g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
for _, child := range obj.ChildrenArray { i--
subObjects, subEdges := pluckObjAndEdges(g, child)
descendantsObjects = append(descendantsObjects, subObjects...)
edges = append(edges, subEdges...)
}
break
} }
} }
@ -85,7 +82,12 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) { func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
obj := tempGraph.Root.ChildrenArray[0] obj := tempGraph.Root.ChildrenArray[0]
obj.MoveWithDescendantsTo(0, 0) dx := 0 - obj.TopLeft.X
dy := 0 - obj.TopLeft.Y
obj.MoveWithDescendants(dx, dy)
for _, e := range tempGraph.Edges {
e.Move(dx, dy)
}
obj.Parent = parent obj.Parent = parent
for _, obj := range tempGraph.Objects { for _, obj := range tempGraph.Objects {
obj.Graph = g obj.Graph = g
@ -284,6 +286,76 @@ func (obj *Object) GetModifierElementAdjustments() (dx, dy float64) {
return dx, dy return dx, dy
} }
func (obj *Object) GetMargin() geo.Spacing {
margin := geo.Spacing{}
if obj.HasLabel() && obj.LabelPosition != nil {
position := label.Position(*obj.LabelPosition)
labelWidth := float64(obj.LabelDimensions.Width + label.PADDING)
labelHeight := float64(obj.LabelDimensions.Height + label.PADDING)
switch position {
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
margin.Top = labelHeight
case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
margin.Bottom = labelHeight
case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom:
margin.Left = labelWidth
case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
margin.Right = labelWidth
}
// if an outside label is larger than the object add margin accordingly
if labelWidth > obj.Width {
dx := labelWidth - obj.Width
switch position {
case label.OutsideTopLeft, label.OutsideBottomLeft:
// label fixed at left will overflow on right
margin.Right = dx
case label.OutsideTopCenter, label.OutsideBottomCenter:
margin.Left = math.Ceil(dx / 2)
margin.Right = math.Ceil(dx / 2)
case label.OutsideTopRight, label.OutsideBottomRight:
margin.Left = dx
}
}
if labelHeight > obj.Height {
dy := labelHeight - obj.Height
switch position {
case label.OutsideLeftTop, label.OutsideRightTop:
margin.Bottom = dy
case label.OutsideLeftMiddle, label.OutsideRightMiddle:
margin.Top = math.Ceil(dy / 2)
margin.Bottom = math.Ceil(dy / 2)
case label.OutsideLeftBottom, label.OutsideRightBottom:
margin.Top = dy
}
}
}
if obj.Icon != nil && obj.IconPosition != nil && obj.Shape.Value != d2target.ShapeImage {
position := label.Position(*obj.IconPosition)
iconSize := float64(d2target.MAX_ICON_SIZE + label.PADDING)
switch position {
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
margin.Top = math.Max(margin.Top, iconSize)
case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
margin.Bottom = math.Max(margin.Bottom, iconSize)
case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom:
margin.Left = math.Max(margin.Left, iconSize)
case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
margin.Right = math.Max(margin.Right, iconSize)
}
}
dx, dy := obj.GetModifierElementAdjustments()
margin.Right += dx
margin.Top += dy
return margin
}
func (obj *Object) ToShape() shape.Shape { func (obj *Object) ToShape() shape.Shape {
tl := obj.TopLeft tl := obj.TopLeft
if tl == nil { if tl == nil {

View file

@ -13,6 +13,7 @@ type SerializedGraph struct {
Root SerializedObject `json:"root"` Root SerializedObject `json:"root"`
Edges []SerializedEdge `json:"edges"` Edges []SerializedEdge `json:"edges"`
Objects []SerializedObject `json:"objects"` Objects []SerializedObject `json:"objects"`
RootLevel int `json:"rootLevel"`
} }
type SerializedObject map[string]interface{} type SerializedObject map[string]interface{}
@ -30,6 +31,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
convert(sg.Root, &root) convert(sg.Root, &root)
g.Root = &root g.Root = &root
root.Graph = g root.Graph = g
g.RootLevel = sg.RootLevel
idToObj := make(map[string]*Object) idToObj := make(map[string]*Object)
idToObj[""] = g.Root idToObj[""] = g.Root
@ -39,6 +41,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
if err := convert(so, &o); err != nil { if err := convert(so, &o); err != nil {
return err return err
} }
o.Graph = g
objects = append(objects, &o) objects = append(objects, &o)
idToObj[so["AbsID"].(string)] = &o idToObj[so["AbsID"].(string)] = &o
} }
@ -91,6 +94,7 @@ func SerializeGraph(g *Graph) ([]byte, error) {
return nil, err return nil, err
} }
sg.Root = root sg.Root = root
sg.RootLevel = g.RootLevel
var sobjects []SerializedObject var sobjects []SerializedObject
for _, o := range g.Objects { for _, o := range g.Objects {

View file

@ -5,14 +5,25 @@ import (
"strconv" "strconv"
"strings" "strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/util-go/go2"
) )
type globContext struct {
root *globContext
refctx *RefContext
// Set of BoardIDA that this glob has already applied to.
appliedFields map[string]struct{}
// Set of Edge IDs that this glob has already applied to.
appliedEdges map[string]struct{}
}
type compiler struct { type compiler struct {
err *d2parser.ParseError err *d2parser.ParseError
@ -23,7 +34,13 @@ type compiler struct {
importCache map[string]*Map importCache map[string]*Map
utf16Pos bool utf16Pos bool
globStack []bool // Stack of globs that must be recomputed at each new object in and below the current scope.
globContextStack [][]*globContext
// Used to prevent field globs causing infinite loops.
globRefContextStack []*RefContext
// Used to check whether ampersands are allowed in the current map.
mapRefContextStack []*RefContext
lazyGlobBeingApplied bool
} }
type CompileOptions struct { type CompileOptions struct {
@ -49,8 +66,8 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, error) {
} }
m := &Map{} m := &Map{}
m.initRoot() m.initRoot()
m.parent.(*Field).References[0].Context.Scope = ast m.parent.(*Field).References[0].Context_.Scope = ast
m.parent.(*Field).References[0].Context.ScopeAST = ast m.parent.(*Field).References[0].Context_.ScopeAST = ast
c.pushImportStack(&d2ast.Import{ c.pushImportStack(&d2ast.Import{
Path: []*d2ast.StringBox{d2ast.RawStringBox(ast.GetRange().Path, true)}, Path: []*d2ast.StringBox{d2ast.RawStringBox(ast.GetRange().Path, true)},
@ -83,7 +100,7 @@ func (c *compiler) overlayClasses(m *Map) {
for _, lf := range layers.Fields { for _, lf := range layers.Fields {
if lf.Map() == nil || lf.Primary() != nil { if lf.Map() == nil || lf.Primary() != nil {
c.errorf(lf.References[0].Context.Key, "invalid layer") c.errorf(lf.References[0].Context_.Key, "invalid layer")
continue continue
} }
l := lf.Map() l := lf.Map()
@ -108,6 +125,8 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
if f.Name == "vars" && f.Map() != nil { if f.Name == "vars" && f.Map() != nil {
varsStack = append([]*Map{f.Map()}, varsStack...) varsStack = append([]*Map{f.Map()}, varsStack...)
} }
}
for _, f := range m.Fields {
if f.Primary() != nil { if f.Primary() != nil {
c.resolveSubstitutions(varsStack, f) c.resolveSubstitutions(varsStack, f)
} }
@ -337,7 +356,7 @@ func (c *compiler) resolveSubstitution(vars *Map, substitution *d2ast.Substituti
func (c *compiler) overlay(base *Map, f *Field) { func (c *compiler) overlay(base *Map, f *Field) {
if f.Map() == nil || f.Primary() != nil { if f.Map() == nil || f.Primary() != nil {
c.errorf(f.References[0].Context.Key, "invalid %s", NodeBoardKind(f)) c.errorf(f.References[0].Context_.Key, "invalid %s", NodeBoardKind(f))
return return
} }
base = base.CopyBase(f) base = base.CopyBase(f)
@ -345,7 +364,26 @@ func (c *compiler) overlay(base *Map, f *Field) {
f.Composite = base f.Composite = base
} }
func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) { func (g *globContext) copy() *globContext {
g2 := *g
g2.refctx = g.root.refctx.Copy()
return &g2
}
func (g *globContext) prefixed(dst *Map) *globContext {
g2 := g.copy()
prefix := d2ast.MakeKeyPath(RelIDA(g2.refctx.ScopeMap, dst))
g2.refctx.Key = g2.refctx.Key.Copy()
if g2.refctx.Key.Key != nil {
prefix.Path = append(prefix.Path, g2.refctx.Key.Key.Path...)
}
if len(prefix.Path) > 0 {
g2.refctx.Key.Key = prefix
}
return g2
}
func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
for _, n := range ast.Nodes { for _, n := range ast.Nodes {
switch { switch {
case n.MapKey != nil: case n.MapKey != nil:
@ -355,11 +393,56 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
ScopeMap: dst, ScopeMap: dst,
ScopeAST: scopeAST, ScopeAST: scopeAST,
}) })
if !ok {
// Unapply glob if appropriate.
gctx := c.getGlobContext(c.mapRefContextStack[len(c.mapRefContextStack)-1])
if gctx == nil {
return false
}
var ks string
if gctx.refctx.Key.HasTripleGlob() {
ks = d2format.Format(d2ast.MakeKeyPath(IDA(dst)))
} else {
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst)))
}
delete(gctx.appliedFields, ks)
return false
}
}
}
return true
}
func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
var globs []*globContext
if len(c.globContextStack) > 0 {
previousGlobs := c.globContextStack[len(c.globContextStack)-1]
if NodeBoardKind(dst) == BoardLayer {
for _, g := range previousGlobs {
if g.refctx.Key.HasTripleGlob() {
globs = append(globs, g.prefixed(dst))
}
}
} else if NodeBoardKind(dst) != "" {
// Make all globs relative to the scenario or step.
for _, g := range previousGlobs {
globs = append(globs, g.prefixed(dst))
}
} else {
globs = append(globs, previousGlobs...)
}
}
c.globContextStack = append(c.globContextStack, globs)
defer func() {
dst.globs = c.globContexts()
c.globContextStack = c.globContextStack[:len(c.globContextStack)-1]
}()
ok := c.ampersandFilterMap(dst, ast, scopeAST)
if !ok { if !ok {
return return
} }
}
}
for _, n := range ast.Nodes { for _, n := range ast.Nodes {
switch { switch {
case n.MapKey != nil: case n.MapKey != nil:
@ -378,6 +461,13 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
Value: []d2ast.InterpolationBox{{Substitution: n.Substitution}}, Value: []d2ast.InterpolationBox{{Substitution: n.Substitution}},
}, },
}, },
References: []*FieldReference{{
Context_: &RefContext{
Scope: ast,
ScopeMap: dst,
ScopeAST: scopeAST,
},
}},
} }
dst.Fields = append(dst.Fields, f) dst.Fields = append(dst.Fields, f)
case n.Import != nil: case n.Import != nil:
@ -389,6 +479,17 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
c.errorf(n.Import, "cannot spread import non map into map") c.errorf(n.Import, "cannot spread import non map into map")
continue continue
} }
for _, gctx := range impn.Map().globs {
if !gctx.refctx.Key.HasTripleGlob() {
continue
}
gctx2 := gctx.copy()
gctx2.refctx.ScopeMap = dst
c.compileKey(gctx2.refctx)
c.ensureGlobContext(gctx2.refctx)
}
OverlayMap(dst, impn.Map()) OverlayMap(dst, impn.Map())
if impnf, ok := impn.(*Field); ok { if impnf, ok := impn.(*Field); ok {
@ -403,12 +504,68 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
} }
} }
func (c *compiler) globContexts() []*globContext {
return c.globContextStack[len(c.globContextStack)-1]
}
func (c *compiler) getGlobContext(refctx *RefContext) *globContext {
for _, gctx := range c.globContexts() {
if gctx.refctx.Equal(refctx) {
return gctx
}
}
return nil
}
func (c *compiler) ensureGlobContext(refctx *RefContext) *globContext {
gctx := c.getGlobContext(refctx)
if gctx != nil {
return gctx
}
gctx = &globContext{
refctx: refctx,
appliedFields: make(map[string]struct{}),
appliedEdges: make(map[string]struct{}),
}
gctx.root = gctx
c.globContextStack[len(c.globContextStack)-1] = append(c.globContexts(), gctx)
return gctx
}
func (c *compiler) compileKey(refctx *RefContext) { func (c *compiler) compileKey(refctx *RefContext) {
if refctx.Key.HasGlob() {
// These printlns are for debugging infinite loops.
// println("og", refctx.Edge, refctx.Key, refctx.Scope, refctx.ScopeMap, refctx.ScopeAST)
for _, refctx2 := range c.globRefContextStack {
// println("st", refctx2.Edge, refctx2.Key, refctx2.Scope, refctx2.ScopeMap, refctx2.ScopeAST)
if refctx.Equal(refctx2) {
// Break the infinite loop.
return
}
// println("keys", d2format.Format(refctx2.Key), d2format.Format(refctx.Key))
}
c.globRefContextStack = append(c.globRefContextStack, refctx)
defer func() {
c.globRefContextStack = c.globRefContextStack[:len(c.globRefContextStack)-1]
}()
c.ensureGlobContext(refctx)
}
oldFields := refctx.ScopeMap.FieldCountRecursive()
oldEdges := refctx.ScopeMap.EdgeCountRecursive()
if len(refctx.Key.Edges) == 0 { if len(refctx.Key.Edges) == 0 {
c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx) c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx)
} else { } else {
c.compileEdges(refctx) c.compileEdges(refctx)
} }
if oldFields != refctx.ScopeMap.FieldCountRecursive() || oldEdges != refctx.ScopeMap.EdgeCountRecursive() {
for _, gctx2 := range c.globContexts() {
// println(d2format.Format(gctx2.refctx.Key), d2format.Format(refctx.Key))
old := c.lazyGlobBeingApplied
c.lazyGlobBeingApplied = true
c.compileKey(gctx2.refctx)
c.lazyGlobBeingApplied = old
}
}
} }
func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) { func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) {
@ -416,7 +573,7 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
return return
} }
fa, err := dst.EnsureField(kp, refctx, true) fa, err := dst.EnsureField(kp, refctx, true, c)
if err != nil { if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return return
@ -431,7 +588,7 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
if !refctx.Key.Ampersand { if !refctx.Key.Ampersand {
return true return true
} }
if len(c.globStack) == 0 || !c.globStack[len(c.globStack)-1] { if len(c.mapRefContextStack) == 0 || !c.mapRefContextStack[len(c.mapRefContextStack)-1].Key.SupportsGlobFilters() {
c.errorf(refctx.Key, "glob filters cannot be used outside globs") c.errorf(refctx.Key, "glob filters cannot be used outside globs")
return false return false
} }
@ -439,14 +596,51 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
return true return true
} }
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, false) fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, false, c)
if err != nil { if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return false return false
} }
if len(fa) == 0 { if len(fa) == 0 {
if refctx.Key.Key.Last().ScalarString() != "label" {
return false return false
} }
kp := refctx.Key.Key.Copy()
kp.Path = kp.Path[:len(kp.Path)-1]
if len(kp.Path) == 0 {
n := refctx.ScopeMap.Parent()
switch n := n.(type) {
case *Field:
fa = append(fa, n)
case *Edge:
if n.Primary_ == nil {
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "" {
return true
}
return false
}
if n.Primary_.Value.ScalarString() != refctx.Key.Value.ScalarBox().Unbox().ScalarString() {
return false
}
}
} else {
fa, err = refctx.ScopeMap.EnsureField(kp, refctx, false, c)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return false
}
}
for _, f := range fa {
label := f.Name
if f.Primary_ != nil {
label = f.Primary_.Value.ScalarString()
}
if label != refctx.Key.Value.ScalarBox().Unbox().ScalarString() {
return false
}
}
return true
}
for _, f := range fa { for _, f := range fa {
ok := c._ampersandFilter(f, refctx) ok := c._ampersandFilter(f, refctx)
if !ok { if !ok {
@ -484,7 +678,22 @@ func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
} }
func (c *compiler) _compileField(f *Field, refctx *RefContext) { func (c *compiler) _compileField(f *Field, refctx *RefContext) {
if len(refctx.Key.Edges) == 0 && refctx.Key.Value.Null != nil { // In case of filters, we need to pass filters before continuing
if refctx.Key.Value.Map != nil && refctx.Key.Value.Map.HasFilter() {
if f.Map() == nil {
f.Composite = &Map{
parent: f,
}
}
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
ok := c.ampersandFilterMap(f.Map(), refctx.Key.Value.Map, refctx.ScopeAST)
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
if !ok {
return
}
}
if len(refctx.Key.Edges) == 0 && (refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil) {
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name // For vars, if we delete the field, it may just resolve to an outer scope var of the same name
// Instead we keep it around, so that resolveSubstitutions can find it // Instead we keep it around, so that resolveSubstitutions can find it
if !IsVar(ParentMap(f)) { if !IsVar(ParentMap(f)) {
@ -494,11 +703,15 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
} }
if refctx.Key.Primary.Unbox() != nil { if refctx.Key.Primary.Unbox() != nil {
if c.ignoreLazyGlob(f) {
return
}
f.Primary_ = &Scalar{ f.Primary_ = &Scalar{
parent: f, parent: f,
Value: refctx.Key.Primary.Unbox(), Value: refctx.Key.Primary.Unbox(),
} }
} }
if refctx.Key.Value.Array != nil { if refctx.Key.Value.Array != nil {
a := &Array{ a := &Array{
parent: f, parent: f,
@ -506,12 +719,11 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
c.compileArray(a, refctx.Key.Value.Array, refctx.ScopeAST) c.compileArray(a, refctx.Key.Value.Array, refctx.ScopeAST)
f.Composite = a f.Composite = a
} else if refctx.Key.Value.Map != nil { } else if refctx.Key.Value.Map != nil {
scopeAST := refctx.Key.Value.Map
if f.Map() == nil { if f.Map() == nil {
f.Composite = &Map{ f.Composite = &Map{
parent: f, parent: f,
} }
}
scopeAST := refctx.Key.Value.Map
switch NodeBoardKind(f) { switch NodeBoardKind(f) {
case BoardScenario: case BoardScenario:
c.overlay(ParentBoard(f).Map(), f) c.overlay(ParentBoard(f).Map(), f)
@ -532,9 +744,12 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
// If new board type, use that as the new scope AST, otherwise, carry on // If new board type, use that as the new scope AST, otherwise, carry on
scopeAST = refctx.ScopeAST scopeAST = refctx.ScopeAST
} }
c.globStack = append(c.globStack, refctx.Key.HasQueryGlob()) } else {
scopeAST = refctx.ScopeAST
}
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
c.compileMap(f.Map(), refctx.Key.Value.Map, scopeAST) c.compileMap(f.Map(), refctx.Key.Value.Map, scopeAST)
c.globStack = c.globStack[:len(c.globStack)-1] c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
switch NodeBoardKind(f) { switch NodeBoardKind(f) {
case BoardScenario, BoardStep: case BoardScenario, BoardStep:
c.overlayClasses(f.Map()) c.overlayClasses(f.Map())
@ -580,15 +795,30 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
} }
} }
} else if refctx.Key.Value.ScalarBox().Unbox() != nil { } else if refctx.Key.Value.ScalarBox().Unbox() != nil {
// If the link is a board, we need to transform it into an absolute path. if c.ignoreLazyGlob(f) {
if f.Name == "link" { return
c.compileLink(refctx)
} }
f.Primary_ = &Scalar{ f.Primary_ = &Scalar{
parent: f, parent: f,
Value: refctx.Key.Value.ScalarBox().Unbox(), Value: refctx.Key.Value.ScalarBox().Unbox(),
} }
// If the link is a board, we need to transform it into an absolute path.
if f.Name == "link" {
c.compileLink(f, refctx)
} }
}
}
// Whether the current lazy glob being applied should not override the field
// if already set by a non glob key.
func (c *compiler) ignoreLazyGlob(n Node) bool {
if c.lazyGlobBeingApplied && n.Primary() != nil {
lastPrimaryRef := n.LastPrimaryRef()
if lastPrimaryRef != nil && !lastPrimaryRef.DueToLazyGlob() {
return true
}
}
return false
} }
func (c *compiler) updateLinks(m *Map) { func (c *compiler) updateLinks(m *Map) {
@ -613,8 +843,35 @@ func (c *compiler) updateLinks(m *Map) {
aida := IDA(f) aida := IDA(f)
if len(bida) != len(aida) { if len(bida) != len(aida) {
prependIDA := aida[:len(aida)-len(bida)] prependIDA := aida[:len(aida)-len(bida)]
kp := d2ast.MakeKeyPath(prependIDA) fullIDA := []string{"root"}
s := d2format.Format(kp) + strings.TrimPrefix(f.Primary_.Value.ScalarString(), "root") // With nested imports, a value may already have been updated with part of the absolute path
// E.g.,
// The import prepends path a b c
// The existing path is b c d
// So the new path is
// a b c
// b c d
// -------
// a b c d
OUTER:
for i := 1; i < len(prependIDA); i += 2 {
for j := 0; i+j < len(prependIDA); j++ {
if prependIDA[i+j] != linkIDA[1+j] {
break
}
// Reached the end and all common
if i+j == len(prependIDA)-1 {
break OUTER
}
}
fullIDA = append(fullIDA, prependIDA[i])
fullIDA = append(fullIDA, prependIDA[i+1])
}
// Chop off "root"
fullIDA = append(fullIDA, linkIDA[1:]...)
kp := d2ast.MakeKeyPath(fullIDA)
s := d2format.Format(kp)
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox() f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox()
} }
} }
@ -624,7 +881,7 @@ func (c *compiler) updateLinks(m *Map) {
} }
} }
func (c *compiler) compileLink(refctx *RefContext) { func (c *compiler) compileLink(f *Field, refctx *RefContext) {
val := refctx.Key.Value.ScalarBox().Unbox().ScalarString() val := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
link, err := d2parser.ParseKey(val) link, err := d2parser.ParseKey(val)
if err != nil { if err != nil {
@ -683,7 +940,7 @@ func (c *compiler) compileLink(refctx *RefContext) {
// Create the absolute path by appending scope path with value specified // Create the absolute path by appending scope path with value specified
scopeIDA = append(scopeIDA, linkIDA...) scopeIDA = append(scopeIDA, linkIDA...)
kp := d2ast.MakeKeyPath(scopeIDA) kp := d2ast.MakeKeyPath(scopeIDA)
refctx.Key.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(d2format.Format(kp))) f.Primary_.Value = d2ast.FlatUnquotedString(d2format.Format(kp))
} }
func (c *compiler) compileEdges(refctx *RefContext) { func (c *compiler) compileEdges(refctx *RefContext) {
@ -692,7 +949,7 @@ func (c *compiler) compileEdges(refctx *RefContext) {
return return
} }
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, true) fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, true, c)
if err != nil { if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return return
@ -726,21 +983,25 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
var ea []*Edge var ea []*Edge
if eid.Index != nil || eid.Glob { if eid.Index != nil || eid.Glob {
ea = refctx.ScopeMap.GetEdges(eid, refctx) ea = refctx.ScopeMap.GetEdges(eid, refctx, c)
if len(ea) == 0 { if len(ea) == 0 {
if !eid.Glob {
c.errorf(refctx.Edge, "indexed edge does not exist") c.errorf(refctx.Edge, "indexed edge does not exist")
}
continue continue
} }
for _, e := range ea { for _, e := range ea {
e.References = append(e.References, &EdgeReference{ e.References = append(e.References, &EdgeReference{
Context: refctx, Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
}) })
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx) refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx, c)
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx) refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx, c)
} }
} else { } else {
var err error var err error
ea, err = refctx.ScopeMap.CreateEdge(eid, refctx) ea, err = refctx.ScopeMap.CreateEdge(eid, refctx, c)
if err != nil { if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error)) c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue continue
@ -757,6 +1018,9 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx) c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
} else { } else {
if refctx.Key.Primary.Unbox() != nil { if refctx.Key.Primary.Unbox() != nil {
if c.ignoreLazyGlob(e) {
return
}
e.Primary_ = &Scalar{ e.Primary_ = &Scalar{
parent: e, parent: e,
Value: refctx.Key.Primary.Unbox(), Value: refctx.Key.Primary.Unbox(),
@ -771,10 +1035,13 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
parent: e, parent: e,
} }
} }
c.globStack = append(c.globStack, refctx.Key.HasQueryGlob()) c.mapRefContextStack = append(c.mapRefContextStack, refctx)
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST) c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
c.globStack = c.globStack[:len(c.globStack)-1] c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
} else if refctx.Key.Value.ScalarBox().Unbox() != nil { } else if refctx.Key.Value.ScalarBox().Unbox() != nil {
if c.ignoreLazyGlob(e) {
return
}
e.Primary_ = &Scalar{ e.Primary_ = &Scalar{
parent: e, parent: e,
Value: refctx.Key.Value.ScalarBox().Unbox(), Value: refctx.Key.Value.ScalarBox().Unbox(),

View file

@ -107,7 +107,7 @@ func assertQuery(t testing.TB, n d2ir.Node, nfields, nedges int, primary interfa
} }
if len(na) == 0 { if len(na) == 0 {
return nil t.Fatalf("query didn't match anything")
} }
return na[0] return na[0]
@ -416,10 +416,27 @@ scenarios: {
} }
}`) }`)
assert.Success(t, err) assert.Success(t, err)
assertQuery(t, m, 8, 2, nil, "")
assertQuery(t, m, 0, 0, nil, "(a -> b)[0]") assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
}, },
}, },
{
name: "multiple-scenario-map",
run: func(t testing.TB) {
m, err := compile(t, `a -> b: { style.opacity: 0.3 }
scenarios: {
1: {
(a -> b)[0].style.opacity: 0.1
}
1: {
z
}
}`)
assert.Success(t, err)
assertQuery(t, m, 11, 2, nil, "")
assertQuery(t, m, 0, 0, 0.1, "scenarios.1.(a -> b)[0].style.opacity")
},
},
} }
runa(t, tca) runa(t, tca)
} }

View file

@ -13,6 +13,7 @@ import (
"oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target"
) )
// Most errors returned by a node should be created with d2parser.Errorf // Most errors returned by a node should be created with d2parser.Errorf
@ -29,6 +30,7 @@ type Node interface {
fmt.Stringer fmt.Stringer
LastRef() Reference LastRef() Reference
LastPrimaryRef() Reference
LastPrimaryKey() *d2ast.Key LastPrimaryKey() *d2ast.Key
} }
@ -110,6 +112,10 @@ func (n *Scalar) LastRef() Reference { return parentRef(n) }
func (n *Map) LastRef() Reference { return parentRef(n) } func (n *Map) LastRef() Reference { return parentRef(n) }
func (n *Array) LastRef() Reference { return parentRef(n) } func (n *Array) LastRef() Reference { return parentRef(n) }
func (n *Scalar) LastPrimaryRef() Reference { return parentPrimaryRef(n) }
func (n *Map) LastPrimaryRef() Reference { return parentPrimaryRef(n) }
func (n *Array) LastPrimaryRef() Reference { return parentPrimaryRef(n) }
func (n *Scalar) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) } func (n *Scalar) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
func (n *Map) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) } func (n *Map) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
func (n *Array) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) } func (n *Array) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
@ -119,6 +125,10 @@ type Reference interface {
// Most specific AST node for the reference. // Most specific AST node for the reference.
AST() d2ast.Node AST() d2ast.Node
Primary() bool Primary() bool
Context() *RefContext
// Result of a glob in Context or from above.
DueToGlob() bool
DueToLazyGlob() bool
} }
var _ Reference = &FieldReference{} var _ Reference = &FieldReference{}
@ -126,6 +136,12 @@ var _ Reference = &EdgeReference{}
func (r *FieldReference) reference() {} func (r *FieldReference) reference() {}
func (r *EdgeReference) reference() {} func (r *EdgeReference) reference() {}
func (r *FieldReference) Context() *RefContext { return r.Context_ }
func (r *EdgeReference) Context() *RefContext { return r.Context_ }
func (r *FieldReference) DueToGlob() bool { return r.DueToGlob_ }
func (r *EdgeReference) DueToGlob() bool { return r.DueToGlob_ }
func (r *FieldReference) DueToLazyGlob() bool { return r.DueToLazyGlob_ }
func (r *EdgeReference) DueToLazyGlob() bool { return r.DueToLazyGlob_ }
type Scalar struct { type Scalar struct {
parent Node parent Node
@ -154,13 +170,15 @@ type Map struct {
parent Node parent Node
Fields []*Field `json:"fields"` Fields []*Field `json:"fields"`
Edges []*Edge `json:"edges"` Edges []*Edge `json:"edges"`
globs []*globContext
} }
func (m *Map) initRoot() { func (m *Map) initRoot() {
m.parent = &Field{ m.parent = &Field{
Name: "root", Name: "root",
References: []*FieldReference{{ References: []*FieldReference{{
Context: &RefContext{ Context_: &RefContext{
ScopeMap: m, ScopeMap: m,
}, },
}}, }},
@ -245,7 +263,11 @@ func NodeBoardKind(n Node) BoardKind {
} }
f = ParentField(n) f = ParentField(n)
case *Map: case *Map:
f = ParentField(n) var ok bool
f, ok = n.parent.(*Field)
if !ok {
return ""
}
if f.Root() { if f.Root() {
return BoardLayer return BoardLayer
} }
@ -295,7 +317,7 @@ func (f *Field) Copy(newParent Node) Node {
return f return f
} }
func (f *Field) lastPrimaryRef() *FieldReference { func (f *Field) LastPrimaryRef() Reference {
for i := len(f.References) - 1; i >= 0; i-- { for i := len(f.References) - 1; i >= 0; i-- {
if f.References[i].Primary() { if f.References[i].Primary() {
return f.References[i] return f.References[i]
@ -305,11 +327,11 @@ func (f *Field) lastPrimaryRef() *FieldReference {
} }
func (f *Field) LastPrimaryKey() *d2ast.Key { func (f *Field) LastPrimaryKey() *d2ast.Key {
fr := f.lastPrimaryRef() fr := f.LastPrimaryRef()
if fr == nil { if fr == nil {
return nil return nil
} }
return fr.Context.Key return fr.(*FieldReference).Context_.Key
} }
func (f *Field) LastRef() Reference { func (f *Field) LastRef() Reference {
@ -451,10 +473,10 @@ func (e *Edge) Copy(newParent Node) Node {
return e return e
} }
func (e *Edge) lastPrimaryRef() *EdgeReference { func (e *Edge) LastPrimaryRef() Reference {
for i := len(e.References) - 1; i >= 0; i-- { for i := len(e.References) - 1; i >= 0; i-- {
fr := e.References[i] fr := e.References[i]
if fr.Context.Key.EdgeKey == nil { if fr.Context_.Key.EdgeKey == nil && !fr.DueToLazyGlob() {
return fr return fr
} }
} }
@ -462,11 +484,11 @@ func (e *Edge) lastPrimaryRef() *EdgeReference {
} }
func (e *Edge) LastPrimaryKey() *d2ast.Key { func (e *Edge) LastPrimaryKey() *d2ast.Key {
er := e.lastPrimaryRef() er := e.LastPrimaryRef()
if er == nil { if er == nil {
return nil return nil
} }
return er.Context.Key return er.(*EdgeReference).Context_.Key
} }
func (e *Edge) LastRef() Reference { func (e *Edge) LastRef() Reference {
@ -494,16 +516,18 @@ type FieldReference struct {
String d2ast.String `json:"string"` String d2ast.String `json:"string"`
KeyPath *d2ast.KeyPath `json:"key_path"` KeyPath *d2ast.KeyPath `json:"key_path"`
Context *RefContext `json:"context"` Context_ *RefContext `json:"context"`
DueToGlob_ bool `json:"due_to_glob"`
DueToLazyGlob_ bool `json:"due_to_lazy_glob"`
} }
// Primary returns true if the Value in Context.Key.Value corresponds to the Field // Primary returns true if the Value in Context.Key.Value corresponds to the Field
// represented by String. // represented by String.
func (fr *FieldReference) Primary() bool { func (fr *FieldReference) Primary() bool {
if fr.KeyPath == fr.Context.Key.Key { if fr.KeyPath == fr.Context_.Key.Key {
return len(fr.Context.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1 return len(fr.Context_.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
} else if fr.KeyPath == fr.Context.Key.EdgeKey { } else if fr.KeyPath == fr.Context_.Key.EdgeKey {
return len(fr.Context.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1 return len(fr.Context_.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
} }
return false return false
} }
@ -518,33 +542,35 @@ func (fr *FieldReference) KeyPathIndex() int {
} }
func (fr *FieldReference) EdgeDest() bool { func (fr *FieldReference) EdgeDest() bool {
return fr.KeyPath == fr.Context.Edge.Dst return fr.KeyPath == fr.Context_.Edge.Dst
} }
func (fr *FieldReference) InEdge() bool { func (fr *FieldReference) InEdge() bool {
return fr.Context.Edge != nil return fr.Context_.Edge != nil
} }
func (fr *FieldReference) AST() d2ast.Node { func (fr *FieldReference) AST() d2ast.Node {
if fr.String == nil { if fr.String == nil {
// Root map. // Root map.
return fr.Context.Scope return fr.Context_.Scope
} }
return fr.String return fr.String
} }
type EdgeReference struct { type EdgeReference struct {
Context *RefContext `json:"context"` Context_ *RefContext `json:"context"`
DueToGlob_ bool `json:"due_to_glob"`
DueToLazyGlob_ bool `json:"due_to_lazy_glob"`
} }
func (er *EdgeReference) AST() d2ast.Node { func (er *EdgeReference) AST() d2ast.Node {
return er.Context.Edge return er.Context_.Edge
} }
// Primary returns true if the Value in Context.Key.Value corresponds to the *Edge // Primary returns true if the Value in Context.Key.Value corresponds to the *Edge
// represented by Context.Edge // represented by Context.Edge
func (er *EdgeReference) Primary() bool { func (er *EdgeReference) Primary() bool {
return len(er.Context.Key.Edges) == 1 && er.Context.Key.EdgeKey == nil return len(er.Context_.Key.Edges) == 1 && er.Context_.Key.EdgeKey == nil
} }
type RefContext struct { type RefContext struct {
@ -569,6 +595,12 @@ func (rc *RefContext) EdgeIndex() int {
return -1 return -1
} }
func (rc *RefContext) Equal(rc2 *RefContext) bool {
// We intentionally ignore edges here because the same glob can produce multiple RefContexts that should be treated the same with only the edge as the difference.
// Same with ScopeMap.
return rc.Key.Equals(rc2.Key) && rc.Scope == rc2.Scope && rc.ScopeAST == rc2.ScopeAST
}
func (m *Map) FieldCountRecursive() int { func (m *Map) FieldCountRecursive() int {
if m == nil { if m == nil {
return 0 return 0
@ -667,7 +699,7 @@ func (m *Map) getField(ida []string) *Field {
} }
// EnsureField is a bit of a misnomer. It's more of a Query/Ensure combination function at this point. // EnsureField is a bit of a misnomer. It's more of a Query/Ensure combination function at this point.
func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool) ([]*Field, error) { func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool, c *compiler) ([]*Field, error) {
i := 0 i := 0
for kp.Path[i].Unbox().ScalarString() == "_" { for kp.Path[i].Unbox().ScalarString() == "_" {
m = ParentMap(m) m = ParentMap(m)
@ -680,26 +712,77 @@ func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool) ([
i++ i++
} }
var gctx *globContext
if refctx != nil && refctx.Key.HasGlob() && c != nil {
gctx = c.getGlobContext(refctx)
}
var fa []*Field var fa []*Field
err := m.ensureField(i, kp, refctx, create, &fa) err := m.ensureField(i, kp, refctx, create, gctx, c, &fa)
if len(fa) > 0 && c != nil && len(c.globRefContextStack) == 0 {
for _, gctx2 := range c.globContexts() {
old := c.lazyGlobBeingApplied
c.lazyGlobBeingApplied = true
c.compileKey(gctx2.refctx)
c.lazyGlobBeingApplied = old
}
}
return fa, err return fa, err
} }
func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create bool, fa *[]*Field) error { func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create bool, gctx *globContext, c *compiler, fa *[]*Field) error {
filter := func(f *Field, passthrough bool) bool {
if gctx != nil {
var ks string
if refctx.Key.HasTripleGlob() {
ks = d2format.Format(d2ast.MakeKeyPath(IDA(f)))
} else {
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f)))
}
if !kp.HasGlob() {
if !passthrough {
gctx.appliedFields[ks] = struct{}{}
}
return true
}
// For globs with edges, we only ignore duplicate fields if the glob is not at the terminal of the keypath, the glob is on the common key or the glob is on the edge key. And only for globs with edge indexes.
lastEl := kp.Path[len(kp.Path)-1]
if len(refctx.Key.Edges) == 0 || lastEl.UnquotedString == nil || len(lastEl.UnquotedString.Pattern) == 0 || kp == refctx.Key.Key || kp == refctx.Key.EdgeKey {
if _, ok := gctx.appliedFields[ks]; ok {
return false
}
}
if !passthrough {
gctx.appliedFields[ks] = struct{}{}
}
}
return true
}
faAppend := func(fa2 ...*Field) {
for _, f := range fa2 {
if filter(f, false) {
*fa = append(*fa, f)
}
}
}
us, ok := kp.Path[i].Unbox().(*d2ast.UnquotedString) us, ok := kp.Path[i].Unbox().(*d2ast.UnquotedString)
if ok && us.Pattern != nil { if ok && us.Pattern != nil {
fa2, ok := m.doubleGlob(us.Pattern) fa2, ok := m.multiGlob(us.Pattern)
if ok { if ok {
if i == len(kp.Path)-1 { if i == len(kp.Path)-1 {
*fa = append(*fa, fa2...) faAppend(fa2...)
} else { } else {
for _, f := range fa2 { for _, f := range fa2 {
if !filter(f, true) {
continue
}
if f.Map() == nil { if f.Map() == nil {
f.Composite = &Map{ f.Composite = &Map{
parent: f, parent: f,
} }
} }
err := f.Map().ensureField(i+1, kp, refctx, create, fa) err := f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
if err != nil { if err != nil {
return err return err
} }
@ -710,14 +793,17 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
for _, f := range m.Fields { for _, f := range m.Fields {
if matchPattern(f.Name, us.Pattern) { if matchPattern(f.Name, us.Pattern) {
if i == len(kp.Path)-1 { if i == len(kp.Path)-1 {
*fa = append(*fa, f) faAppend(f)
} else { } else {
if !filter(f, true) {
continue
}
if f.Map() == nil { if f.Map() == nil {
f.Composite = &Map{ f.Composite = &Map{
parent: f, parent: f,
} }
} }
err := f.Map().ensureField(i+1, kp, refctx, create, fa) err := f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
if err != nil { if err != nil {
return err return err
} }
@ -758,12 +844,17 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
f.References = append(f.References, &FieldReference{ f.References = append(f.References, &FieldReference{
String: kp.Path[i].Unbox(), String: kp.Path[i].Unbox(),
KeyPath: kp, KeyPath: kp,
Context: refctx, Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
}) })
} }
if i+1 == len(kp.Path) { if i+1 == len(kp.Path) {
*fa = append(*fa, f) faAppend(f)
return nil
}
if !filter(f, true) {
return nil return nil
} }
if _, ok := f.Composite.(*Array); ok { if _, ok := f.Composite.(*Array); ok {
@ -774,33 +865,61 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
parent: f, parent: f,
} }
} }
return f.Map().ensureField(i+1, kp, refctx, create, fa) return f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
} }
if !create { if !create {
return nil return nil
} }
shape := ParentShape(m)
if _, ok := d2graph.ReservedKeywords[strings.ToLower(head)]; !ok && len(c.globRefContextStack) > 0 {
if shape == d2target.ShapeClass || shape == d2target.ShapeSQLTable {
return nil
}
}
f := &Field{ f := &Field{
parent: m, parent: m,
Name: head, Name: head,
} }
defer func() {
if i < kp.FirstGlob() {
return
}
for _, grefctx := range c.globRefContextStack {
var ks string
if grefctx.Key.HasTripleGlob() {
ks = d2format.Format(d2ast.MakeKeyPath(IDA(f)))
} else {
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f)))
}
gctx2 := c.getGlobContext(grefctx)
gctx2.appliedFields[ks] = struct{}{}
}
}()
// Don't add references for fake common KeyPath from trimCommon in CreateEdge. // Don't add references for fake common KeyPath from trimCommon in CreateEdge.
if refctx != nil { if refctx != nil {
f.References = append(f.References, &FieldReference{ f.References = append(f.References, &FieldReference{
String: kp.Path[i].Unbox(), String: kp.Path[i].Unbox(),
KeyPath: kp, KeyPath: kp,
Context: refctx, Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
}) })
} }
if !filter(f, true) {
return nil
}
m.Fields = append(m.Fields, f) m.Fields = append(m.Fields, f)
if i+1 == len(kp.Path) { if i+1 == len(kp.Path) {
*fa = append(*fa, f) faAppend(f)
return nil return nil
} }
if f.Composite == nil {
f.Composite = &Map{ f.Composite = &Map{
parent: f, parent: f,
} }
return f.Map().ensureField(i+1, kp, refctx, create, fa) }
return f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
} }
func (m *Map) DeleteEdge(eid *EdgeID) *Edge { func (m *Map) DeleteEdge(eid *EdgeID) *Edge {
@ -833,7 +952,7 @@ func (m *Map) DeleteField(ida ...string) *Field {
for _, fr := range f.References { for _, fr := range f.References {
for _, e := range m.Edges { for _, e := range m.Edges {
for _, er := range e.References { for _, er := range e.References {
if er.Context == fr.Context { if er.Context_ == fr.Context_ {
m.DeleteEdge(e.ID) m.DeleteEdge(e.ID)
break break
} }
@ -865,10 +984,14 @@ func (m *Map) DeleteField(ida ...string) *Field {
return nil return nil
} }
func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge { func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext, c *compiler) []*Edge {
if refctx != nil { if refctx != nil {
var gctx *globContext
if refctx.Key.HasGlob() && c != nil {
gctx = c.ensureGlobContext(refctx)
}
var ea []*Edge var ea []*Edge
m.getEdges(eid, refctx, &ea) m.getEdges(eid, refctx, gctx, &ea)
return ea return ea
} }
@ -882,7 +1005,7 @@ func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
return nil return nil
} }
if f.Map() != nil { if f.Map() != nil {
return f.Map().GetEdges(eid, nil) return f.Map().GetEdges(eid, nil, nil)
} }
return nil return nil
} }
@ -896,7 +1019,7 @@ func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
return ea return ea
} }
func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error { func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[]*Edge) error {
eid, m, common, err := eid.resolve(m) eid, m, common, err := eid.resolve(m)
if err != nil { if err != nil {
return err return err
@ -914,7 +1037,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
} }
} }
} }
fa, err := m.EnsureField(commonKP, nil, false) fa, err := m.EnsureField(commonKP, nil, false, nil)
if err != nil { if err != nil {
return nil return nil
} }
@ -927,7 +1050,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
parent: f, parent: f,
} }
} }
err = f.Map().getEdges(eid, refctx, ea) err = f.Map().getEdges(eid, refctx, gctx, ea)
if err != nil { if err != nil {
return err return err
} }
@ -935,11 +1058,11 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
return nil return nil
} }
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, nil, false) srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, nil, false, nil)
if err != nil { if err != nil {
return err return err
} }
dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, nil, false) dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, nil, false, nil)
if err != nil { if err != nil {
return err return err
} }
@ -950,19 +1073,46 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
eid2.SrcPath = RelIDA(m, src) eid2.SrcPath = RelIDA(m, src)
eid2.DstPath = RelIDA(m, dst) eid2.DstPath = RelIDA(m, dst)
ea2 := m.GetEdges(eid2, nil) ea2 := m.GetEdges(eid2, nil, nil)
for _, e := range ea2 {
if gctx != nil {
var ks string
if refctx.Key.HasTripleGlob() {
ks = d2format.Format(d2ast.MakeKeyPath(IDA(e)))
} else {
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e)))
}
if _, ok := gctx.appliedEdges[ks]; ok {
continue
}
gctx.appliedEdges[ks] = struct{}{}
}
*ea = append(*ea, ea2...) *ea = append(*ea, ea2...)
} }
} }
}
return nil return nil
} }
func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext) ([]*Edge, error) { func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext, c *compiler) ([]*Edge, error) {
var ea []*Edge var ea []*Edge
return ea, m.createEdge(eid, refctx, &ea) var gctx *globContext
if refctx != nil && refctx.Key.HasGlob() && c != nil {
gctx = c.ensureGlobContext(refctx)
}
err := m.createEdge(eid, refctx, gctx, c, &ea)
if len(ea) > 0 && c != nil && len(c.globRefContextStack) == 0 {
for _, gctx2 := range c.globContexts() {
old := c.lazyGlobBeingApplied
c.lazyGlobBeingApplied = true
c.compileKey(gctx2.refctx)
c.lazyGlobBeingApplied = old
}
}
return ea, err
} }
func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error { func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *compiler, ea *[]*Edge) error {
if ParentEdge(m) != nil { if ParentEdge(m) != nil {
return d2parser.Errorf(refctx.Edge, "cannot create edge inside edge") return d2parser.Errorf(refctx.Edge, "cannot create edge inside edge")
} }
@ -983,7 +1133,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
} }
} }
} }
fa, err := m.EnsureField(commonKP, nil, true) fa, err := m.EnsureField(commonKP, nil, true, c)
if err != nil { if err != nil {
return err return err
} }
@ -996,7 +1146,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
parent: f, parent: f,
} }
} }
err = f.Map().createEdge(eid, refctx, ea) err = f.Map().createEdge(eid, refctx, gctx, c, ea)
if err != nil { if err != nil {
return err return err
} }
@ -1022,11 +1172,11 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
return d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense") return d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense")
} }
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx, true) srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx, true, c)
if err != nil { if err != nil {
return err return err
} }
dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx, true) dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx, true, c)
if err != nil { if err != nil {
return err return err
} }
@ -1038,21 +1188,21 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
continue continue
} }
if refctx.Edge.Src.HasDoubleGlob() { if refctx.Edge.Src.HasMultiGlob() {
// If src has a double glob we only select leafs, those without children. // If src has a double glob we only select leafs, those without children.
if src.Map().IsContainer() { if src.Map().IsContainer() {
continue continue
} }
if ParentBoard(src) != ParentBoard(dst) { if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) {
continue continue
} }
} }
if refctx.Edge.Dst.HasDoubleGlob() { if refctx.Edge.Dst.HasMultiGlob() {
// If dst has a double glob we only select leafs, those without children. // If dst has a double glob we only select leafs, those without children.
if dst.Map().IsContainer() { if dst.Map().IsContainer() {
continue continue
} }
if ParentBoard(src) != ParentBoard(dst) { if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) {
continue continue
} }
} }
@ -1060,17 +1210,20 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
eid2 := eid.Copy() eid2 := eid.Copy()
eid2.SrcPath = RelIDA(m, src) eid2.SrcPath = RelIDA(m, src)
eid2.DstPath = RelIDA(m, dst) eid2.DstPath = RelIDA(m, dst)
e, err := m.createEdge2(eid2, refctx, src, dst)
e, err := m.createEdge2(eid2, refctx, gctx, c, src, dst)
if err != nil { if err != nil {
return err return err
} }
if e != nil {
*ea = append(*ea, e) *ea = append(*ea, e)
} }
} }
}
return nil return nil
} }
func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Edge, error) { func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c *compiler, src, dst *Field) (*Edge, error) {
if NodeBoardKind(src) != "" { if NodeBoardKind(src) != "" {
return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards") return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards")
} }
@ -1083,7 +1236,7 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Ed
eid.Index = nil eid.Index = nil
eid.Glob = true eid.Glob = true
ea := m.GetEdges(eid, nil) ea := m.GetEdges(eid, nil, nil)
index := len(ea) index := len(ea)
eid.Index = &index eid.Index = &index
eid.Glob = false eid.Glob = false
@ -1091,9 +1244,29 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Ed
parent: m, parent: m,
ID: eid, ID: eid,
References: []*EdgeReference{{ References: []*EdgeReference{{
Context: refctx, Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
}}, }},
} }
if gctx != nil {
var ks string
// We only ever want to create one of the edge per glob so we filter without the edge index.
e2 := e.Copy(e.Parent()).(*Edge)
e2.ID = e2.ID.Copy()
e2.ID.Index = nil
if refctx.Key.HasTripleGlob() {
ks = d2format.Format(d2ast.MakeKeyPath(IDA(e2)))
} else {
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e2)))
}
if _, ok := gctx.appliedEdges[ks]; ok {
return nil, nil
}
gctx.appliedEdges[ks] = struct{}{}
}
m.Edges = append(m.Edges, e) m.Edges = append(m.Edges, e)
return e, nil return e, nil
@ -1148,6 +1321,18 @@ func (e *Edge) AST() d2ast.Node {
return k return k
} }
func (e *Edge) IDString() string {
ast := e.AST().(*d2ast.Key)
if e.ID.Index != nil {
ast.EdgeIndex = &d2ast.EdgeIndex{
Int: e.ID.Index,
}
}
ast.Primary = d2ast.ScalarBox{}
ast.Value = d2ast.ValueBox{}
return d2format.Format(ast)
}
func (a *Array) AST() d2ast.Node { func (a *Array) AST() d2ast.Node {
if a == nil { if a == nil {
return nil return nil
@ -1178,7 +1363,7 @@ func (m *Map) AST() d2ast.Node {
return astMap return astMap
} }
func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext) { func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext, c *compiler) {
sb := kp.Path[i] sb := kp.Path[i]
f := m.GetField(sb.Unbox().ScalarString()) f := m.GetField(sb.Unbox().ScalarString())
if f == nil { if f == nil {
@ -1188,13 +1373,15 @@ func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext
f.References = append(f.References, &FieldReference{ f.References = append(f.References, &FieldReference{
String: sb.Unbox(), String: sb.Unbox(),
KeyPath: kp, KeyPath: kp,
Context: refctx, Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
}) })
if i+1 == len(kp.Path) { if i+1 == len(kp.Path) {
return return
} }
if f.Map() != nil { if f.Map() != nil {
f.Map().appendFieldReferences(i+1, kp, refctx) f.Map().appendFieldReferences(i+1, kp, refctx, c)
} }
} }
@ -1268,6 +1455,24 @@ func ParentEdge(n Node) *Edge {
} }
} }
func ParentShape(n Node) string {
for {
f, ok := n.(*Field)
if ok {
if f.Map() != nil {
shapef := f.Map().GetField("shape")
if shapef != nil && shapef.Primary() != nil {
return shapef.Primary().Value.ScalarString()
}
}
}
n = n.Parent()
if n == nil {
return ""
}
}
}
func countUnderscores(p []string) int { func countUnderscores(p []string) int {
for i, el := range p { for i, el := range p {
if el != "_" { if el != "_" {
@ -1310,6 +1515,18 @@ func parentRef(n Node) Reference {
return nil return nil
} }
func parentPrimaryRef(n Node) Reference {
f := ParentField(n)
if f != nil {
return f.LastPrimaryRef()
}
e := ParentEdge(n)
if e != nil {
return e.LastPrimaryRef()
}
return nil
}
func parentPrimaryKey(n Node) *d2ast.Key { func parentPrimaryKey(n Node) *d2ast.Key {
f := ParentField(n) f := ParentField(n)
if f != nil { if f != nil {
@ -1325,60 +1542,65 @@ func parentPrimaryKey(n Node) *d2ast.Key {
// BoardIDA returns the absolute path to n from the nearest board root. // BoardIDA returns the absolute path to n from the nearest board root.
func BoardIDA(n Node) (ida []string) { func BoardIDA(n Node) (ida []string) {
for { for {
f, ok := n.(*Field) switch n := n.(type) {
if ok { case *Field:
if f.Root() || NodeBoardKind(f) != "" { if n.Root() || NodeBoardKind(n) != "" {
reverseIDA(ida) reverseIDA(ida)
return ida return ida
} }
ida = append(ida, f.Name) ida = append(ida, n.Name)
case *Edge:
ida = append(ida, n.IDString())
} }
f = ParentField(n) n = n.Parent()
if f == nil { if n == nil {
reverseIDA(ida) reverseIDA(ida)
return ida return ida
} }
n = f
} }
} }
// IDA returns the absolute path to n. // IDA returns the absolute path to n.
func IDA(n Node) (ida []string) { func IDA(n Node) (ida []string) {
for { for {
f, ok := n.(*Field) switch n := n.(type) {
if ok { case *Field:
ida = append(ida, f.Name) ida = append(ida, n.Name)
if f.Root() { if n.Root() {
reverseIDA(ida) reverseIDA(ida)
return ida return ida
} }
case *Edge:
ida = append(ida, n.IDString())
} }
f = ParentField(n) n = n.Parent()
if f == nil { if n == nil {
reverseIDA(ida) reverseIDA(ida)
return ida return ida
} }
n = f
} }
} }
// RelIDA returns the path to n relative to p. // RelIDA returns the path to n relative to p.
func RelIDA(p, n Node) (ida []string) { func RelIDA(p, n Node) (ida []string) {
for { for {
f, ok := n.(*Field) switch n := n.(type) {
if ok { case *Field:
ida = append(ida, f.Name) ida = append(ida, n.Name)
if f.Root() { if n.Root() {
reverseIDA(ida) reverseIDA(ida)
return ida return ida
} }
case *Edge:
ida = append(ida, n.String())
} }
f = ParentField(n) n = n.Parent()
if f == nil || f.Root() || f == p || f.Composite == p { f, fok := n.(*Field)
e, eok := n.(*Edge)
if n == nil || (fok && (f.Root() || f == p || f.Composite == p)) || (eok && (e == p || e.Map_ == p)) {
reverseIDA(ida) reverseIDA(ida)
return ida return ida
} }
n = f
} }
} }
@ -1476,7 +1698,7 @@ func (m *Map) InClass(key *d2ast.Key) bool {
} }
for _, ref := range classF.References { for _, ref := range classF.References {
if ref.Context.Key == key { if ref.Context_.Key == key {
return true return true
} }
} }

View file

@ -96,6 +96,120 @@ x -> y
assertQuery(t, m, 0, 0, nil, "(x -> y)[1]") assertQuery(t, m, 0, 0, nil, "(x -> y)[1]")
}, },
}, },
{
name: "label-filter/1",
run: func(t testing.TB) {
m, err := compile(t, `
x
y
p: p
a -> z: delta
*.style.opacity: 0.1
*: {
&label: x
style.opacity: 1
}
*: {
&label: p
style.opacity: 0.5
}
(* -> *)[*]: {
&label: delta
target-arrowhead.shape: diamond
}
`)
assert.Success(t, err)
assertQuery(t, m, 17, 1, nil, "")
assertQuery(t, m, 0, 0, 1, "x.style.opacity")
assertQuery(t, m, 0, 0, 0.1, "y.style.opacity")
assertQuery(t, m, 0, 0, 0.5, "p.style.opacity")
assertQuery(t, m, 0, 0, 0.1, "a.style.opacity")
assertQuery(t, m, 0, 0, 0.1, "z.style.opacity")
assertQuery(t, m, 0, 0, "diamond", "(a -> z).target-arrowhead.shape")
},
},
{
name: "label-filter/2",
run: func(t testing.TB) {
m, err := compile(t, `
(* -> *)[*].style.opacity: 0.1
(* -> *)[*]: {
&label: hi
style.opacity: 1
}
x -> y: hi
x -> y
`)
assert.Success(t, err)
assertQuery(t, m, 6, 2, nil, "")
assertQuery(t, m, 2, 0, "hi", "(x -> y)[0]")
assertQuery(t, m, 0, 0, 1, "(x -> y)[0].style.opacity")
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[1].style.opacity")
},
},
{
name: "lazy-filter",
run: func(t testing.TB) {
m, err := compile(t, `
*: {
&label: a
style.fill: yellow
}
a
# if i remove this line, the glob applies as expected
b
b.label: a
`)
assert.Success(t, err)
assertQuery(t, m, 7, 0, nil, "")
assertQuery(t, m, 0, 0, "yellow", "a.style.fill")
assertQuery(t, m, 0, 0, "yellow", "b.style.fill")
},
},
{
name: "primary-filter",
run: func(t testing.TB) {
m, err := compile(t, `
parent: {
a -> b1
a -> b2
a -> b3
b1 -> c1
b1 -> c2
c1: {
c1-child.class: hidden
}
c2: {
C2-child.class: hidden
}
c2.class: hidden
b2.class: hidden
}
classes: {
hidden: {
style: {
fill: red
}
}
}
# Error
**: null {
&class: hidden
}
`)
assert.Success(t, err)
assertQuery(t, m, 9, 3, nil, "")
},
},
} }
runa(t, tca) runa(t, tca)

View file

@ -108,7 +108,7 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) {
ir = &Map{} ir = &Map{}
ir.initRoot() ir.initRoot()
ir.parent.(*Field).References[0].Context.Scope = ast ir.parent.(*Field).References[0].Context_.Scope = ast
c.compileMap(ir, ast, ast) c.compileMap(ir, ast, ast)
@ -128,14 +128,14 @@ func nilScopeMap(n Node) {
} }
case *Edge: case *Edge:
for _, r := range n.References { for _, r := range n.References {
r.Context.ScopeMap = nil r.Context_.ScopeMap = nil
} }
if n.Map() != nil { if n.Map() != nil {
nilScopeMap(n.Map()) nilScopeMap(n.Map())
} }
case *Field: case *Field:
for _, r := range n.References { for _, r := range n.References {
r.Context.ScopeMap = nil r.Context_.ScopeMap = nil
} }
if n.Map() != nil { if n.Map() != nil {
nilScopeMap(n.Map()) nilScopeMap(n.Map())

View file

@ -84,6 +84,19 @@ label: meow`,
assertQuery(t, m, 0, 0, "root.layers.x.layers.y", "layers.x.y.link") assertQuery(t, m, 0, 0, "root.layers.x.layers.y", "layers.x.y.link")
}, },
}, },
{
name: "boards-deep",
run: func(t testing.TB) {
m, err := compileFS(t, "index.d2", map[string]string{
"index.d2": `a.link: layers.b; layers: { b: @b }`,
"b.d2": `b.link: layers.c; layers: { c: @c }`,
"c.d2": `c.link: layers.d; layers: { d: @d }`,
"d.d2": `d`,
})
assert.Success(t, err)
assertQuery(t, m, 0, 0, "root.layers.b.layers.c.layers.d", "layers.b.layers.c.c.link")
},
},
{ {
name: "steps-inheritence", name: "steps-inheritence",
run: func(t testing.TB) { run: func(t testing.TB) {

View file

@ -11,7 +11,7 @@ func OverlayMap(base, overlay *Map) {
} }
for _, oe := range overlay.Edges { for _, oe := range overlay.Edges {
bea := base.GetEdges(oe.ID, nil) bea := base.GetEdges(oe.ID, nil, nil)
if len(bea) == 0 { if len(bea) == 0 {
base.Edges = append(base.Edges, oe.Copy(base).(*Edge)) base.Edges = append(base.Edges, oe.Copy(base).(*Edge))
continue continue

View file

@ -3,32 +3,68 @@ package d2ir
import ( import (
"strings" "strings"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
) )
func (m *Map) doubleGlob(pattern []string) ([]*Field, bool) { func (m *Map) multiGlob(pattern []string) ([]*Field, bool) {
if !(len(pattern) == 3 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*") {
return nil, false
}
var fa []*Field var fa []*Field
if d2ast.IsDoubleGlob(pattern) {
m._doubleGlob(&fa) m._doubleGlob(&fa)
return fa, true return fa, true
}
if d2ast.IsTripleGlob(pattern) {
m._tripleGlob(&fa)
return fa, true
}
return nil, false
} }
func (m *Map) _doubleGlob(fa *[]*Field) { func (m *Map) _doubleGlob(fa *[]*Field) {
for _, f := range m.Fields { for _, f := range m.Fields {
if _, ok := d2graph.ReservedKeywords[f.Name]; ok { if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
if f.Name == "layers" {
continue
}
if _, ok := d2graph.BoardKeywords[f.Name]; !ok { if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
continue continue
} }
// We don't ever want to append layers, scenarios or steps directly.
if f.Map() != nil {
f.Map()._tripleGlob(fa)
} }
continue
}
if NodeBoardKind(f) == "" {
*fa = append(*fa, f) *fa = append(*fa, f)
}
if f.Map() != nil { if f.Map() != nil {
f.Map()._doubleGlob(fa) f.Map()._doubleGlob(fa)
} }
} }
} }
func (m *Map) _tripleGlob(fa *[]*Field) {
for _, f := range m.Fields {
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
continue
}
// We don't ever want to append layers, scenarios or steps directly.
if f.Map() != nil {
f.Map()._tripleGlob(fa)
}
continue
}
if NodeBoardKind(f) == "" {
*fa = append(*fa, f)
}
if f.Map() != nil {
f.Map()._tripleGlob(fa)
}
}
}
func matchPattern(s string, pattern []string) bool { func matchPattern(s string, pattern []string) bool {
if len(pattern) == 0 { if len(pattern) == 0 {
return true return true

View file

@ -128,7 +128,7 @@ an* -> an*`)
assert.Success(t, err) assert.Success(t, err)
assertQuery(t, m, 2, 2, nil, "") assertQuery(t, m, 2, 2, nil, "")
assertQuery(t, m, 0, 0, nil, "(animate -> animal)[0]") assertQuery(t, m, 0, 0, nil, "(animate -> animal)[0]")
assertQuery(t, m, 0, 0, nil, "(animal -> animal)[0]") assertQuery(t, m, 0, 0, nil, "(animal -> animate)[0]")
}, },
}, },
{ {
@ -154,7 +154,7 @@ sh*.an* -> sh*.an*`)
assertQuery(t, m, 3, 2, nil, "") assertQuery(t, m, 3, 2, nil, "")
assertQuery(t, m, 2, 2, nil, "shared") assertQuery(t, m, 2, 2, nil, "shared")
assertQuery(t, m, 0, 0, nil, "shared.(animate -> animal)[0]") assertQuery(t, m, 0, 0, nil, "shared.(animate -> animal)[0]")
assertQuery(t, m, 0, 0, nil, "shared.(animal -> animal)[0]") assertQuery(t, m, 0, 0, nil, "shared.(animal -> animate)[0]")
}, },
}, },
{ {
@ -285,6 +285,31 @@ d
assertQuery(t, m, 0, 0, nil, "(* -> *)[*]") assertQuery(t, m, 0, 0, nil, "(* -> *)[*]")
}, },
}, },
{
name: "single-glob/defaults",
run: func(t testing.TB) {
m, err := compile(t, `wrapper.*: {
shape: page
}
wrapper.a
wrapper.b
wrapper.c
wrapper.d
scenarios.x: { wrapper.p }
layers.x: { wrapper.p }
`)
assert.Success(t, err)
assertQuery(t, m, 26, 0, nil, "")
assertQuery(t, m, 0, 0, "page", "wrapper.a.shape")
assertQuery(t, m, 0, 0, "page", "wrapper.b.shape")
assertQuery(t, m, 0, 0, "page", "wrapper.c.shape")
assertQuery(t, m, 0, 0, "page", "wrapper.d.shape")
assertQuery(t, m, 0, 0, "page", "scenarios.x.wrapper.p.shape")
assertQuery(t, m, 0, 0, nil, "layers.x.wrapper.p")
},
},
{ {
name: "double-glob/edge/1", name: "double-glob/edge/1",
run: func(t testing.TB) { run: func(t testing.TB) {
@ -314,21 +339,438 @@ task.** -> fast
assertQuery(t, m, 3, 1, nil, "") assertQuery(t, m, 3, 1, nil, "")
}, },
}, },
}
runa(t, tca)
t.Run("errors", func(t *testing.T) {
tca := []testCase{
{ {
name: "glob-edge-glob-index", name: "double-glob/defaults",
run: func(t testing.TB) { run: func(t testing.TB) {
_, err := compile(t, `(* -> b)[*].style.fill: red m, err := compile(t, `**: {
shape: page
}
a
b
c
d
scenarios.x: { p }
layers.x: { p }
`) `)
assert.ErrorString(t, err, `TestCompile/patterns/errors/glob-edge-glob-index.d2:1:2: indexed edge does not exist`) assert.Success(t, err)
assertQuery(t, m, 23, 0, nil, "")
assertQuery(t, m, 0, 0, "page", "a.shape")
assertQuery(t, m, 0, 0, "page", "b.shape")
assertQuery(t, m, 0, 0, "page", "c.shape")
assertQuery(t, m, 0, 0, "page", "d.shape")
assertQuery(t, m, 0, 0, "page", "scenarios.x.p.shape")
assertQuery(t, m, 0, 0, nil, "layers.x.p")
},
},
{
name: "triple-glob/defaults",
run: func(t testing.TB) {
m, err := compile(t, `***: {
shape: page
}
a
b
c
d
scenarios.x: { p }
layers.x: { p }
`)
assert.Success(t, err)
assertQuery(t, m, 24, 0, nil, "")
assertQuery(t, m, 0, 0, "page", "a.shape")
assertQuery(t, m, 0, 0, "page", "b.shape")
assertQuery(t, m, 0, 0, "page", "c.shape")
assertQuery(t, m, 0, 0, "page", "d.shape")
assertQuery(t, m, 0, 0, "page", "scenarios.x.p.shape")
assertQuery(t, m, 0, 0, "page", "layers.x.p.shape")
},
},
{
name: "triple-glob/edge-defaults",
run: func(t testing.TB) {
m, err := compile(t, `(*** -> ***)[*]: {
target-arrowhead.shape: diamond
}
a -> b
c -> d
scenarios.x: { p -> q }
layers.x: { j -> f }
`)
assert.Success(t, err)
assertQuery(t, m, 28, 6, nil, "")
assertQuery(t, m, 0, 0, "diamond", "(a -> b)[0].target-arrowhead.shape")
assertQuery(t, m, 0, 0, "diamond", "(c -> d)[0].target-arrowhead.shape")
assertQuery(t, m, 0, 0, "diamond", "scenarios.x.(a -> b)[0].target-arrowhead.shape")
assertQuery(t, m, 0, 0, "diamond", "scenarios.x.(c -> d)[0].target-arrowhead.shape")
assertQuery(t, m, 0, 0, "diamond", "scenarios.x.(p -> q)[0].target-arrowhead.shape")
assertQuery(t, m, 4, 1, nil, "layers.x")
assertQuery(t, m, 0, 0, "diamond", "layers.x.(j -> f)[0].target-arrowhead.shape")
},
},
{
name: "alixander-review/1",
run: func(t testing.TB) {
m, err := compile(t, `
***.style.fill: yellow
**.shape: circle
*.style.multiple: true
x: {
y
}
layers: {
next: {
a
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 14, 0, nil, "")
},
},
{
name: "alixander-review/2",
run: func(t testing.TB) {
m, err := compile(t, `
a
* -> y
b
c
`)
assert.Success(t, err)
assertQuery(t, m, 4, 3, nil, "")
},
},
{
name: "alixander-review/3",
run: func(t testing.TB) {
m, err := compile(t, `
a
***.b -> c
layers: {
z: {
d
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 8, 2, nil, "")
},
},
{
name: "alixander-review/4",
run: func(t testing.TB) {
m, err := compile(t, `
**.child
a
b
c
`)
assert.Success(t, err)
assertQuery(t, m, 6, 0, nil, "")
},
},
{
name: "alixander-review/5",
run: func(t testing.TB) {
m, err := compile(t, `
**.style.fill: red
scenarios: {
b: {
a -> b
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 8, 1, nil, "")
assertQuery(t, m, 0, 0, "red", "scenarios.b.a.style.fill")
assertQuery(t, m, 0, 0, "red", "scenarios.b.b.style.fill")
},
},
{
name: "alixander-review/6",
run: func(t testing.TB) {
m, err := compile(t, `
(* -> *)[*].style.opacity: 0.1
x -> y: hi
x -> y
`)
assert.Success(t, err)
assertQuery(t, m, 6, 2, nil, "")
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[0].style.opacity")
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[1].style.opacity")
},
},
{
name: "alixander-review/7",
run: func(t testing.TB) {
m, err := compile(t, `
*: {
style.fill: red
}
**: {
style.fill: red
}
table: {
style.fill: blue
shape: sql_table
a: b
}
`)
assert.Success(t, err)
assertQuery(t, m, 7, 0, nil, "")
assertQuery(t, m, 0, 0, "blue", "table.style.fill")
},
},
{
name: "alixander-review/8",
run: func(t testing.TB) {
m, err := compile(t, `
(a -> *)[*].style.stroke: red
(* -> *)[*].style.stroke: red
b -> c
`)
assert.Success(t, err)
assertQuery(t, m, 4, 1, nil, "")
assertQuery(t, m, 0, 0, "red", "(b -> c)[0].style.stroke")
},
},
{
name: "override/1",
run: func(t testing.TB) {
m, err := compile(t, `
**.style.fill: yellow
**.style.fill: red
a
`)
assert.Success(t, err)
assertQuery(t, m, 3, 0, nil, "")
assertQuery(t, m, 0, 0, "red", "a.style.fill")
},
},
{
name: "override/2",
run: func(t testing.TB) {
m, err := compile(t, `
***.style.fill: yellow
layers: {
hi: {
**.style.fill: red
# should be red, but it's yellow right now
a
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 5, 0, nil, "")
assertQuery(t, m, 0, 0, "red", "layers.hi.a.style.fill")
},
},
{
name: "override/3",
run: func(t testing.TB) {
m, err := compile(t, `
(*** -> ***)[*].label: hi
a -> b
layers: {
hi: {
(*** -> ***)[*].label: bye
scenarios: {
b: {
# This label is "hi", but it should be "bye"
a -> b
}
}
}
}
`)
assert.Success(t, err)
assertQuery(t, m, 10, 2, nil, "")
assertQuery(t, m, 0, 0, "hi", "(a -> b)[0].label")
assertQuery(t, m, 0, 0, "bye", "layers.hi.scenarios.b.(a -> b)[0].label")
},
},
{
name: "override/4",
run: func(t testing.TB) {
m, err := compile(t, `
(*** -> ***)[*].label: hi
a -> b: {
label: bye
}
`)
assert.Success(t, err)
assertQuery(t, m, 3, 1, nil, "")
assertQuery(t, m, 0, 0, "bye", "(a -> b)[0].label")
},
},
{
name: "override/5",
run: func(t testing.TB) {
m, err := compile(t, `
(*** -> ***)[*].label: hi
# This is "hey" right now but should be "hi"?
a -> b
(*** -> ***)[*].label: hey
`)
assert.Success(t, err)
assertQuery(t, m, 3, 1, nil, "")
assertQuery(t, m, 0, 0, "hey", "(a -> b)[0].label")
},
},
{
name: "override/6",
run: func(t testing.TB) {
m, err := compile(t, `
# Nulling glob doesn't work
a
*a.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
a.icon: null
# Regular icon nulling works
b.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
b.icon: null
# Shape nulling works
*.shape: circle
a.shape: null
b.shape: null
`)
assert.Success(t, err)
assertQuery(t, m, 2, 0, nil, "")
assertQuery(t, m, 0, 0, nil, "a")
assertQuery(t, m, 0, 0, nil, "b")
},
},
{
name: "override/7",
run: func(t testing.TB) {
m, err := compile(t, `
# Nulling glob doesn't work
*a.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
a.icon: null
# Regular icon nulling works
b.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
b.icon: null
# Shape nulling works
*.shape: circle
a.shape: null
b.shape: null
`)
assert.Success(t, err)
assertQuery(t, m, 2, 0, nil, "")
assertQuery(t, m, 0, 0, nil, "a")
assertQuery(t, m, 0, 0, nil, "b")
},
},
{
name: "table-class-exception",
run: func(t testing.TB) {
m, err := compile(t, `
***: {
c: d
}
***: {
style.fill: red
}
table: {
shape: sql_table
a: b
}
class: {
shape: class
a: b
}
`)
assert.Success(t, err)
assertQuery(t, m, 13, 0, nil, "")
},
},
{
name: "prevent-chain-recursion",
run: func(t testing.TB) {
m, err := compile(t, `
***: {
c: d
}
***: {
style.fill: red
}
one
two
`)
assert.Success(t, err)
assertQuery(t, m, 12, 0, nil, "")
},
},
{
name: "import-glob/1",
run: func(t testing.TB) {
m, err := compileFS(t, "index.d2", map[string]string{
"index.d2": "before; ...@globs.d2; after",
"globs.d2": `*: jingle
**: true
***: meow`,
})
assert.Success(t, err)
assertQuery(t, m, 2, 0, nil, "")
assertQuery(t, m, 0, 0, "meow", "before")
assertQuery(t, m, 0, 0, "meow", "after")
},
},
{
name: "import-glob/2",
run: func(t testing.TB) {
m, err := compileFS(t, "index.d2", map[string]string{
"index.d2": `...@rules.d2
hi
`,
"rules.d2": `***.style.fill: red
***: meow
x`,
})
assert.Success(t, err)
assertQuery(t, m, 6, 0, nil, "")
assertQuery(t, m, 2, 0, "meow", "hi")
assertQuery(t, m, 2, 0, "meow", "x")
assertQuery(t, m, 0, 0, "red", "hi.style.fill")
assertQuery(t, m, 0, 0, "red", "x.style.fill")
}, },
}, },
} }
runa(t, tca) runa(t, tca)
})
} }

View file

@ -14,10 +14,14 @@ func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
} }
if k.Key != nil { if k.Key != nil {
f := m.GetField(k.Key.IDA()...) fa, err := m.EnsureField(k.Key, nil, false, nil)
if f == nil { if err != nil {
return nil, err
}
if len(fa) == 0 {
return nil, nil return nil, nil
} }
for _, f := range fa {
if len(k.Edges) == 0 { if len(k.Edges) == 0 {
na = append(na, f) na = append(na, f)
return na, nil return na, nil
@ -27,6 +31,7 @@ func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
return nil, nil return nil, nil
} }
} }
}
eida := NewEdgeIDs(k) eida := NewEdgeIDs(k)
@ -36,7 +41,7 @@ func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
ScopeMap: m, ScopeMap: m,
Edge: k.Edges[i], Edge: k.Edges[i],
} }
ea := m.GetEdges(eid, refctx) ea := m.GetEdges(eid, refctx, nil)
for _, e := range ea { for _, e := range ea {
if k.EdgeKey == nil { if k.EdgeKey == nil {
na = append(na, e) na = append(na, e)

View file

@ -6,7 +6,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
"regexp"
"sort" "sort"
"strings" "strings"
@ -140,17 +139,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
return err return err
} }
loadScript := "" mapper := NewObjectMapper()
idToObj := make(map[string]*d2graph.Object)
for _, obj := range g.Objects { for _, obj := range g.Objects {
id := obj.AbsID() mapper.Register(obj)
idToObj[id] = obj }
loadScript := ""
width, height := obj.Width, obj.Height for _, obj := range g.Objects {
loadScript += mapper.generateAddNodeLine(obj, int(obj.Width), int(obj.Height))
loadScript += generateAddNodeLine(id, int(width), int(height))
if obj.Parent != g.Root { if obj.Parent != g.Root {
loadScript += generateAddParentLine(id, obj.Parent.AbsID()) loadScript += mapper.generateAddParentLine(obj, obj.Parent)
} }
} }
@ -178,7 +175,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
} }
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), width, height) loadScript += mapper.generateAddEdgeLine(src, dst, edge.AbsID(), width, height)
} }
if debugJS { if debugJS {
@ -209,7 +206,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
log.Debug(ctx, "graph", slog.F("json", dn)) log.Debug(ctx, "graph", slog.F("json", dn))
} }
obj := idToObj[dn.ID] obj := mapper.ToObj(dn.ID)
// dagre gives center of node // dagre gives center of node
obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2)) obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2))
@ -415,30 +412,6 @@ func setGraphAttrs(attrs dagreOpts) string {
) )
} }
func escapeID(id string) string {
// fixes \\
id = strings.ReplaceAll(id, "\\", `\\`)
// replaces \n with \\n whenever \n is not preceded by \ (does not replace \\n)
re := regexp.MustCompile(`[^\\]\n`)
id = re.ReplaceAllString(id, `\\n`)
// avoid an unescaped \r becoming a \n in the layout result
id = strings.ReplaceAll(id, "\r", `\r`)
return id
}
func generateAddNodeLine(id string, width, height int) string {
id = escapeID(id)
return fmt.Sprintf("g.setNode(`%s`, { id: `%s`, width: %d, height: %d });\n", id, id, width, height)
}
func generateAddParentLine(childID, parentID string) string {
return fmt.Sprintf("g.setParent(`%s`, `%s`);\n", escapeID(childID), escapeID(parentID))
}
func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string {
return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height)
}
// getLongestEdgeChainHead finds the longest chain in a container and gets its head // getLongestEdgeChainHead finds the longest chain in a container and gets its head
// If there are multiple chains of the same length, get the head closest to the center // If there are multiple chains of the same length, get the head closest to the center
func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object { func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {

View file

@ -0,0 +1,63 @@
package d2dagrelayout
import (
"fmt"
"regexp"
"strconv"
"strings"
"oss.terrastruct.com/d2/d2graph"
)
type objectMapper struct {
objToID map[*d2graph.Object]string
idToObj map[string]*d2graph.Object
}
func NewObjectMapper() *objectMapper {
return &objectMapper{
objToID: make(map[*d2graph.Object]string),
idToObj: make(map[string]*d2graph.Object),
}
}
func (c *objectMapper) Register(obj *d2graph.Object) {
id := strconv.Itoa(len(c.idToObj))
c.idToObj[id] = obj
c.objToID[obj] = id
}
func (c *objectMapper) ToID(obj *d2graph.Object) string {
return c.objToID[obj]
}
func (c *objectMapper) ToObj(id string) *d2graph.Object {
return c.idToObj[id]
}
func (c objectMapper) generateAddNodeLine(obj *d2graph.Object, width, height int) string {
id := c.ToID(obj)
return fmt.Sprintf("g.setNode(`%s`, { id: `%s`, width: %d, height: %d });\n", id, id, width, height)
}
func (c objectMapper) generateAddParentLine(child, parent *d2graph.Object) string {
return fmt.Sprintf("g.setParent(`%s`, `%s`);\n", c.ToID(child), c.ToID(parent))
}
func (c objectMapper) generateAddEdgeLine(from, to *d2graph.Object, edgeID string, width, height int) string {
return fmt.Sprintf(
"g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n",
c.ToID(from), c.ToID(to), escapeID(edgeID), width, height,
)
}
func escapeID(id string) string {
// fixes \\
id = strings.ReplaceAll(id, "\\", `\\`)
// replaces \n with \\n whenever \n is not preceded by \ (does not replace \\n)
re := regexp.MustCompile(`[^\\]\n`)
id = re.ReplaceAllString(id, `\\n`)
// avoid an unescaped \r becoming a \n in the layout result
id = strings.ReplaceAll(id, "\r", `\r`)
return id
}

View file

@ -41,10 +41,38 @@ type ELKNode struct {
Width float64 `json:"width"` Width float64 `json:"width"`
Height float64 `json:"height"` Height float64 `json:"height"`
Children []*ELKNode `json:"children,omitempty"` Children []*ELKNode `json:"children,omitempty"`
Ports []*ELKPort `json:"ports,omitempty"`
Labels []*ELKLabel `json:"labels,omitempty"` Labels []*ELKLabel `json:"labels,omitempty"`
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"` LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
} }
type PortSide string
const (
South PortSide = "SOUTH"
North PortSide = "NORTH"
East PortSide = "EAST"
West PortSide = "WEST"
)
type Direction string
const (
Down Direction = "DOWN"
Up Direction = "UP"
Right Direction = "RIGHT"
Left Direction = "LEFT"
)
type ELKPort struct {
ID string `json:"id"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
}
type ELKLabel struct { type ELKLabel struct {
Text string `json:"text"` Text string `json:"text"`
X float64 `json:"x"` X float64 `json:"x"`
@ -105,7 +133,7 @@ type elkOpts struct {
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"` FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
Thoroughness int `json:"elk.layered.thoroughness,omitempty"` Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"` EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
Direction string `json:"elk.direction"` Direction Direction `json:"elk.direction"`
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"` HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"` InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"` ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
@ -118,6 +146,9 @@ type elkOpts struct {
ContentAlignment string `json:"elk.contentAlignment,omitempty"` ContentAlignment string `json:"elk.contentAlignment,omitempty"`
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"` NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
PortSide PortSide `json:"elk.port.side,omitempty"`
PortConstraints string `json:"elk.portConstraints,omitempty"`
ConfigurableOpts ConfigurableOpts
} }
@ -171,15 +202,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
switch g.Root.Direction.Value { switch g.Root.Direction.Value {
case "down": case "down":
elkGraph.LayoutOptions.Direction = "DOWN" elkGraph.LayoutOptions.Direction = Down
case "up": case "up":
elkGraph.LayoutOptions.Direction = "UP" elkGraph.LayoutOptions.Direction = Up
case "right": case "right":
elkGraph.LayoutOptions.Direction = "RIGHT" elkGraph.LayoutOptions.Direction = Right
case "left": case "left":
elkGraph.LayoutOptions.Direction = "LEFT" elkGraph.LayoutOptions.Direction = Left
default: default:
elkGraph.LayoutOptions.Direction = "DOWN" elkGraph.LayoutOptions.Direction = Down
} }
// set label and icon positions for ELK // set label and icon positions for ELK
@ -215,11 +246,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if incoming >= 2 || outgoing >= 2 { if incoming >= 2 || outgoing >= 2 {
switch g.Root.Direction.Value { switch g.Root.Direction.Value {
case "right", "left": case "right", "left":
if obj.Attributes.HeightAttr == nil {
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing) obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
}
default: default:
if obj.Attributes.WidthAttr == nil {
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing) obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
} }
} }
}
width, height := adjustDimensions(obj) width, height := adjustDimensions(obj)
@ -253,9 +288,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
switch elkGraph.LayoutOptions.Direction { switch elkGraph.LayoutOptions.Direction {
case "DOWN", "UP": case Down, Up:
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width))) n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
case "RIGHT", "LEFT": case Right, Left:
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height))) n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
} }
} else { } else {
@ -283,6 +318,33 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} else { } else {
elkNodes[parent].Children = append(elkNodes[parent].Children, n) elkNodes[parent].Children = append(elkNodes[parent].Children, n)
} }
if obj.SQLTable != nil {
n.LayoutOptions.PortConstraints = "FIXED_POS"
columns := obj.SQLTable.Columns
colHeight := n.Height / float64(len(columns)+1)
n.Ports = make([]*ELKPort, 0, len(columns)*2)
var srcSide, dstSide PortSide
switch elkGraph.LayoutOptions.Direction {
case Left:
srcSide, dstSide = West, East
default:
srcSide, dstSide = East, West
}
for i, col := range columns {
n.Ports = append(n.Ports, &ELKPort{
ID: srcPortID(obj, col.Name.Label),
Y: float64(i+1)*colHeight + colHeight/2,
LayoutOptions: &elkOpts{PortSide: srcSide},
})
n.Ports = append(n.Ports, &ELKPort{
ID: dstPortID(obj, col.Name.Label),
Y: float64(i+1)*colHeight + colHeight/2,
LayoutOptions: &elkOpts{PortSide: dstSide},
})
}
}
elkNodes[obj] = n elkNodes[obj] = n
}) })
@ -321,11 +383,64 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} }
} }
for _, edge := range g.Edges { var srcSide, dstSide PortSide
switch elkGraph.LayoutOptions.Direction {
case Up:
srcSide, dstSide = North, South
default:
srcSide, dstSide = South, North
}
ports := map[struct {
obj *d2graph.Object
side PortSide
}][]*ELKPort{}
for ei, edge := range g.Edges {
var src, dst string
switch {
case edge.SrcTableColumnIndex != nil:
src = srcPortID(edge.Src, edge.Src.SQLTable.Columns[*edge.SrcTableColumnIndex].Name.Label)
case edge.Src.SQLTable != nil:
p := &ELKPort{
ID: fmt.Sprintf("%s.%d", srcPortID(edge.Src, "__root__"), ei),
LayoutOptions: &elkOpts{PortSide: srcSide},
}
src = p.ID
elkNodes[edge.Src].Ports = append(elkNodes[edge.Src].Ports, p)
k := struct {
obj *d2graph.Object
side PortSide
}{edge.Src, srcSide}
ports[k] = append(ports[k], p)
default:
src = edge.Src.AbsID()
}
switch {
case edge.DstTableColumnIndex != nil:
dst = dstPortID(edge.Dst, edge.Dst.SQLTable.Columns[*edge.DstTableColumnIndex].Name.Label)
case edge.Dst.SQLTable != nil:
p := &ELKPort{
ID: fmt.Sprintf("%s.%d", dstPortID(edge.Dst, "__root__"), ei),
LayoutOptions: &elkOpts{PortSide: dstSide},
}
dst = p.ID
elkNodes[edge.Dst].Ports = append(elkNodes[edge.Dst].Ports, p)
k := struct {
obj *d2graph.Object
side PortSide
}{edge.Dst, dstSide}
ports[k] = append(ports[k], p)
default:
dst = edge.Dst.AbsID()
}
e := &ELKEdge{ e := &ELKEdge{
ID: edge.AbsID(), ID: edge.AbsID(),
Sources: []string{edge.Src.AbsID()}, Sources: []string{src},
Targets: []string{edge.Dst.AbsID()}, Targets: []string{dst},
} }
if edge.Label.Value != "" { if edge.Label.Value != "" {
e.Labels = append(e.Labels, &ELKLabel{ e.Labels = append(e.Labels, &ELKLabel{
@ -341,6 +456,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
elkEdges[edge] = e elkEdges[edge] = e
} }
for k, ports := range ports {
width := elkNodes[k.obj].Width
spacing := width / float64(len(ports)+1)
for i, p := range ports {
p.X = float64(i+1) * spacing
}
}
raw, err := json.Marshal(elkGraph) raw, err := json.Marshal(elkGraph)
if err != nil { if err != nil {
return err return err
@ -503,6 +626,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
return nil return nil
} }
func srcPortID(obj *d2graph.Object, column string) string {
return fmt.Sprintf("%s.%s.src", obj.AbsID(), column)
}
func dstPortID(obj *d2graph.Object, column string) string {
return fmt.Sprintf("%s.%s.dst", obj.AbsID(), column)
}
// deleteBends is a shim for ELK to delete unnecessary bends // deleteBends is a shim for ELK to delete unnecessary bends
// see https://github.com/terrastruct/d2/issues/1030 // see https://github.com/terrastruct/d2/issues/1030
func deleteBends(g *d2graph.Graph) { func deleteBends(g *d2graph.Graph) {
@ -521,30 +652,42 @@ func deleteBends(g *d2graph.Graph) {
var corner *geo.Point var corner *geo.Point
var end *geo.Point var end *geo.Point
var columnIndex *int
if isSource { if isSource {
start = e.Route[0] start = e.Route[0]
corner = e.Route[1] corner = e.Route[1]
end = e.Route[2] end = e.Route[2]
endpoint = e.Src endpoint = e.Src
columnIndex = e.SrcTableColumnIndex
} else { } else {
start = e.Route[len(e.Route)-1] start = e.Route[len(e.Route)-1]
corner = e.Route[len(e.Route)-2] corner = e.Route[len(e.Route)-2]
end = e.Route[len(e.Route)-3] end = e.Route[len(e.Route)-3]
endpoint = e.Dst endpoint = e.Dst
columnIndex = e.DstTableColumnIndex
} }
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y) isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
dx, dy := endpoint.GetModifierElementAdjustments() dx, dy := endpoint.GetModifierElementAdjustments()
// Make sure it's still attached // Make sure it's still attached
if isHorizontal { switch {
case columnIndex != nil:
rowHeight := endpoint.Height / float64(len(endpoint.SQLTable.Columns)+1)
rowCenter := endpoint.TopLeft.Y + rowHeight*float64(*columnIndex+1) + rowHeight/2
// for row connections new Y coordinate should be within 1/3 row height from the row center
if math.Abs(end.Y-rowCenter) > rowHeight/3 {
continue
}
case isHorizontal:
if end.Y <= endpoint.TopLeft.Y+10-dy { if end.Y <= endpoint.TopLeft.Y+10-dy {
continue continue
} }
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 { if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
continue continue
} }
} else { default:
if end.X <= endpoint.TopLeft.X+10 { if end.X <= endpoint.TopLeft.X+10 {
continue continue
} }
@ -606,12 +749,21 @@ func deleteBends(g *d2graph.Graph) {
} }
} }
} }
// Get rid of ladders // Get rid of ladders
// ELK likes to do these for some reason // ELK likes to do these for some reason
// . ┌─ // . ┌─
// . ┌─┘ // . ┌─┘
// . │ // . │
// We want to transform these into L-shapes // We want to transform these into L-shapes
points := map[geo.Point]int{}
for _, e := range g.Edges {
for _, p := range e.Route {
points[*p]++
}
}
for ei, e := range g.Edges { for ei, e := range g.Edges {
if len(e.Route) < 6 { if len(e.Route) < 6 {
continue continue
@ -627,6 +779,11 @@ func deleteBends(g *d2graph.Graph) {
end := e.Route[i+2] end := e.Route[i+2]
after := e.Route[i+3] after := e.Route[i+3]
if c, _ := points[*corner]; c > 1 {
// If corner is shared with another edge, they merge
continue
}
// S-shape on sources only concerned one segment, since the other was just along the bound of endpoint // S-shape on sources only concerned one segment, since the other was just along the bound of endpoint
// These concern two segments // These concern two segments

View file

@ -2,7 +2,6 @@ package d2grid
import ( import (
"strconv" "strconv"
"strings"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
@ -11,6 +10,7 @@ import (
type gridDiagram struct { type gridDiagram struct {
root *d2graph.Object root *d2graph.Object
objects []*d2graph.Object objects []*d2graph.Object
edges []*d2graph.Edge
rows int rows int
columns int columns int
@ -107,19 +107,7 @@ func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.objects { for _, obj := range gd.objects {
obj.MoveWithDescendants(dx, dy) obj.MoveWithDescendants(dx, dy)
} }
} for _, e := range gd.edges {
e.Move(dx, dy)
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0)
restore := func(parent, child *d2graph.Object) {
parent.Children[strings.ToLower(child.ID)] = child
parent.ChildrenArray = append(parent.ChildrenArray, child)
graph.Objects = append(graph.Objects, child)
}
for _, child := range gd.objects {
restore(obj, child)
child.IterDescendants(restore)
} }
} }

View file

@ -5,7 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"math" "math"
"sort"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
@ -21,77 +20,15 @@ const (
// Layout runs the grid layout on containers with rows/columns // Layout runs the grid layout on containers with rows/columns
// Note: children are not allowed edges or descendants // Note: children are not allowed edges or descendants
// // 1. Run grid layout on the graph root
// 1. Traverse graph from root, skip objects with no rows/columns // 2. Set the resulting dimensions to the graph root
// 2. Construct a grid with the container children func Layout(ctx context.Context, g *d2graph.Graph) error {
// 3. Remove the children from the main graph obj := g.Root
// 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, layout)
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, layout d2graph.LayoutGraph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
toRemove := make(map[*d2graph.Object]struct{})
gridDiagrams = make(map[string]*gridDiagram)
objectOrder = make(map[string]int)
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
}
var processGrid func(obj *d2graph.Object) error
processGrid = func(obj *d2graph.Object) error {
for _, child := range obj.ChildrenArray {
if child.IsGridDiagram() {
if err := processGrid(child); err != nil {
return err
}
} else if len(child.ChildrenArray) > 0 {
tempGraph := g.ExtractAsNestedGraph(child)
if err := layout(ctx, tempGraph); err != nil {
return err
}
g.InjectNestedGraph(tempGraph, obj)
sort.SliceStable(g.Objects, func(i, j int) bool {
return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
})
sort.SliceStable(child.ChildrenArray, func(i, j int) bool {
return objectOrder[child.ChildrenArray[i].AbsID()] < objectOrder[child.ChildrenArray[j].AbsID()]
})
sort.SliceStable(obj.ChildrenArray, func(i, j int) bool {
return objectOrder[obj.ChildrenArray[i].AbsID()] < objectOrder[obj.ChildrenArray[j].AbsID()]
})
for _, o := range tempGraph.Objects {
toRemove[o] = struct{}{}
}
}
}
gd, err := layoutGrid(g, obj) gd, err := layoutGrid(g, obj)
if err != nil { if err != nil {
return err return err
} }
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
if obj.Box != nil { if obj.Box != nil {
// CONTAINER_PADDING is default, but use gap value if set // CONTAINER_PADDING is default, but use gap value if set
@ -140,43 +77,6 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
} }
} }
// also check for grid cells with outside top labels or icons
// the first grid object is at the top (and always exists)
topY := gd.objects[0].TopLeft.Y
highestOutside := topY
for _, o := range gd.objects {
// we only want to compute label positions for objects at the top of the grid
if o.TopLeft.Y > topY {
if gd.rowDirected {
// if the grid is rowDirected (row1, row2, etc) we can stop after finishing the first row
break
} else {
// otherwise we continue until the next column
continue
}
}
if o.LabelPosition != nil {
labelPosition := label.Position(*o.LabelPosition)
if labelPosition.IsOutside() {
labelTL := o.GetLabelTopLeft()
if labelTL.Y < highestOutside {
highestOutside = labelTL.Y
}
}
}
if o.IconPosition != nil {
switch label.Position(*o.IconPosition) {
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
iconSpace := float64(d2target.MAX_ICON_SIZE + label.PADDING)
if topY-iconSpace < highestOutside {
highestOutside = topY - iconSpace
}
}
}
}
if highestOutside < topY {
occupiedHeight += topY - highestOutside + 2*label.PADDING
}
if occupiedHeight > float64(verticalPadding) { if occupiedHeight > float64(verticalPadding) {
// if the label doesn't fit within the padding, we need to add more // if the label doesn't fit within the padding, we need to add more
dy = occupiedHeight - float64(verticalPadding) dy = occupiedHeight - float64(verticalPadding)
@ -195,70 +95,92 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
if obj.Icon != nil { if obj.Icon != nil {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft)) obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
} }
gridDiagrams[obj.AbsID()] = gd
for _, o := range gd.objects { // simple straight line edge routing between grid objects
toRemove[o] = struct{}{} for _, e := range g.Edges {
if !e.Src.Parent.IsDescendantOf(obj) && !e.Dst.Parent.IsDescendantOf(obj) {
continue
}
// if edge is within grid, remove it from outer layout
gd.edges = append(gd.edges, e)
if e.Src.Parent != obj || e.Dst.Parent != obj {
continue
}
// if edge is grid child, use simple routing
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
e.TraceToShape(e.Route, 0, 1)
if e.Label.Value != "" {
e.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if g.Root.IsGridDiagram() && len(g.Root.ChildrenArray) != 0 {
g.Root.TopLeft = geo.NewPoint(0, 0)
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
if g.RootLevel > 0 {
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
if obj.GridGap != nil || obj.HorizontalGap != nil {
horizontalPadding = gd.horizontalGap
}
if obj.GridGap != nil || obj.VerticalGap != nil {
verticalPadding = gd.verticalGap
}
// shift the grid from (0, 0)
gd.shift(
obj.TopLeft.X+float64(horizontalPadding),
obj.TopLeft.Y+float64(verticalPadding),
)
} }
return nil return nil
}
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
}
if err := processGrid(obj); err != nil {
return nil, nil, err
}
}
}
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
for _, obj := range g.Objects {
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) { func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
gd := newGridDiagram(obj) gd := newGridDiagram(obj)
// position labels and icons
for _, o := range gd.objects {
positionedLabel := false
if o.Icon != nil && o.IconPosition == nil {
if len(o.ChildrenArray) > 0 {
o.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
// don't overwrite position if nested graph layout positioned label/icon
if o.LabelPosition == nil {
o.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
positionedLabel = true
}
} else {
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if !positionedLabel && o.HasLabel() && o.LabelPosition == nil {
if len(o.ChildrenArray) > 0 {
o.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else if o.HasOutsideBottomLabel() {
o.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
} else if o.Icon != nil {
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else {
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
}
// to handle objects with outside labels, we adjust their dimensions before layout and
// after layout, we remove the label adjustment and reposition TopLeft if needed
revertAdjustments := gd.sizeForOutsideLabels()
if gd.rows != 0 && gd.columns != 0 { if gd.rows != 0 && gd.columns != 0 {
gd.layoutEvenly(g, obj) gd.layoutEvenly(g, obj)
} else { } else {
gd.layoutDynamic(g, obj) gd.layoutDynamic(g, obj)
} }
// position labels and icons revertAdjustments()
for _, o := range gd.objects {
if o.Icon != nil {
// don't overwrite position if nested graph layout positioned label/icon
if o.LabelPosition == nil {
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
}
if o.IconPosition == nil {
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
} else {
if o.LabelPosition == nil {
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
}
return gd, nil return gd, nil
} }
@ -486,7 +408,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
cursor.X = 0 cursor.X = 0
cursor.Y += rowHeight + verticalGap cursor.Y += rowHeight + verticalGap
} }
maxY = cursor.Y - horizontalGap maxY = cursor.Y - verticalGap
} else { } else {
// measure column heights // measure column heights
colHeights := []float64{} colHeights := []float64{}
@ -664,20 +586,14 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
// if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here // if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
if rowSize > okThreshold*targetSize { if rowSize > okThreshold*targetSize {
skipCount++ skipCount++
if skipCount >= SKIP_LIMIT {
// there may even be too many to skip // there may even be too many to skip
return true return skipCount >= SKIP_LIMIT
}
return false
} }
} }
// row is too small to be good overall // row is too small to be good overall
if rowSize < targetSize/okThreshold { if rowSize < targetSize/okThreshold {
skipCount++ skipCount++
if skipCount >= SKIP_LIMIT { return skipCount >= SKIP_LIMIT
return true
}
return false
} }
return true return true
} }
@ -936,53 +852,110 @@ func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalG
return totalDelta return totalDelta
} }
// cleanup restores the graph after the core layout engine finishes func (gd *gridDiagram) sizeForOutsideLabels() (revert func()) {
// - translating the grid to its position placed by the core layout engine margins := make(map[*d2graph.Object]geo.Spacing)
// - 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()]
})
}()
var restore func(obj *d2graph.Object) for _, o := range gd.objects {
restore = func(obj *d2graph.Object) { margin := o.GetMargin()
gd, exists := gridDiagrams[obj.AbsID()] margins[o] = margin
if !exists {
return
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING o.Height += margin.Top + margin.Bottom
if obj.GridGap != nil || obj.HorizontalGap != nil { o.Width += margin.Left + margin.Right
horizontalPadding = gd.horizontalGap
}
if obj.GridGap != nil || obj.VerticalGap != nil {
verticalPadding = gd.verticalGap
} }
// shift the grid from (0, 0) // Example: a single column with 3 shapes and
gd.shift( // `x.label: long label {near: outside-bottom-left}`
obj.TopLeft.X+float64(horizontalPadding), // `y.label: outsider {near: outside-right-center}`
obj.TopLeft.Y+float64(verticalPadding), // . ┌───────────────────┐
) // . │ widest shape here │
gd.cleanup(obj, graph) // . └───────────────────┘
// . ┌───┐
// . │ x │
// . └───┘
// . long label
// . ├─────────┤ x's new width
// . ├─mr──┤ margin.right added to width during layout
// . ┌───┐
// . │ y │ outsider
// . └───┘
// . ├─────────────┤ y's new width
// . ├───mr────┤ margin.right added to width during layout
for _, child := range obj.ChildrenArray { // BEFORE LAYOUT
restore(child) // . ┌───────────────────┐
// . │ widest shape here │
// . └───────────────────┘
// . ┌─────────┐
// . │ x │
// . └─────────┘
// . ┌─────────────┐
// . │ y │
// . └─────────────┘
// AFTER LAYOUT
// . ┌───────────────────┐
// . │ widest shape here │
// . └───────────────────┘
// . ┌───────────────────┐
// . │ x │
// . └───────────────────┘
// . ┌───────────────────┐
// . │ y │
// . └───────────────────┘
// CLEANUP 1/2
// . ┌───────────────────┐
// . │ widest shape here │
// . └───────────────────┘
// . ┌─────────────┐
// . │ x │
// . └─────────────┘
// . long label ├─mr──┤ remove margin we added
// . ┌─────────┐
// . │ y │ outsider
// . └─────────┘
// . ├───mr────┤ remove margin we added
// CLEANUP 2/2
// . ┌───────────────────┐
// . │ widest shape here │
// . └───────────────────┘
// . ┌───────────────────┐
// . │ x │
// . └───────────────────┘
// . long label ├─mr──┤ we removed too much so add back margin we subtracted, then subtract new margin
// . ┌─────────┐
// . │ y │ outsider
// . └─────────┘
// . ├───mr────┤ margin.right is still needed
return func() {
for _, o := range gd.objects {
m, has := margins[o]
if !has {
continue
} }
dy := m.Top + m.Bottom
dx := m.Left + m.Right
o.Height -= dy
o.Width -= dx
// less margin may be needed if layout grew the object
// compute the new margin after removing the old margin we added
margin := o.GetMargin()
marginX := margin.Left + margin.Right
marginY := margin.Top + margin.Bottom
if marginX < dx {
// layout grew width and now we need less of a margin (but we subtracted too much)
// add back dx and subtract the new amount
o.Width += dx - marginX
}
if marginY < dy {
o.Height += dy - marginY
} }
if graph.Root.IsGridDiagram() { if margin.Left > 0 || margin.Top > 0 {
gd, exists := gridDiagrams[graph.Root.AbsID()] o.MoveWithDescendants(margin.Left, margin.Top)
if exists {
gd.cleanup(graph.Root, graph)
} }
} }
for _, obj := range graph.Objects {
restore(obj)
} }
} }

460
d2layouts/d2layouts.go Normal file
View file

@ -0,0 +1,460 @@
package d2layouts
import (
"context"
"fmt"
"math"
"sort"
"strings"
"cdr.dev/slog"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/util-go/go2"
)
type DiagramType string
// a grid diagram at a constant near is
const (
DefaultGraphType DiagramType = ""
ConstantNearGraph DiagramType = "constant-near"
GridDiagram DiagramType = "grid-diagram"
SequenceDiagram DiagramType = "sequence-diagram"
)
type GraphInfo struct {
IsConstantNear bool
DiagramType DiagramType
}
func (gi GraphInfo) isDefault() bool {
return !gi.IsConstantNear && gi.DiagramType == DefaultGraphType
}
func SaveChildrenOrder(container *d2graph.Object) (restoreOrder func()) {
objectOrder := make(map[string]int, len(container.ChildrenArray))
for i, obj := range container.ChildrenArray {
objectOrder[obj.AbsID()] = i
}
return func() {
sort.SliceStable(container.ChildrenArray, func(i, j int) bool {
return objectOrder[container.ChildrenArray[i].AbsID()] < objectOrder[container.ChildrenArray[j].AbsID()]
})
}
}
func SaveOrder(g *d2graph.Graph) (restoreOrder func()) {
objectOrder := make(map[string]int, len(g.Objects))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
}
edgeOrder := make(map[string]int, len(g.Edges))
for i, edge := range g.Edges {
edgeOrder[edge.AbsID()] = i
}
restoreRootOrder := SaveChildrenOrder(g.Root)
return func() {
sort.SliceStable(g.Objects, func(i, j int) bool {
return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
})
sort.SliceStable(g.Edges, func(i, j int) bool {
iIndex, iHas := edgeOrder[g.Edges[i].AbsID()]
jIndex, jHas := edgeOrder[g.Edges[j].AbsID()]
if iHas && jHas {
return iIndex < jIndex
}
return iHas
})
restoreRootOrder()
}
}
func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph) error {
g.Root.Box = &geo.Box{}
// Before we can layout these nodes, we need to handle all nested diagrams first.
extracted := make(map[string]*d2graph.Graph)
var extractedOrder []string
var extractedEdges []*d2graph.Edge
var constantNears []*d2graph.Graph
restoreOrder := SaveOrder(g)
defer restoreOrder()
// Iterate top-down from Root so all nested diagrams can process their own contents
queue := make([]*d2graph.Object, 0, len(g.Root.ChildrenArray))
queue = append(queue, g.Root.ChildrenArray...)
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
isGridCellContainer := graphInfo.DiagramType == GridDiagram &&
curr.IsContainer() && curr.Parent == g.Root
gi := NestedGraphInfo(curr)
if isGridCellContainer && gi.isDefault() {
// if we are in a grid diagram, and our children have descendants
// we need to run layout on them first, even if they are not special diagram types
nestedGraph, externalEdges := ExtractSubgraph(curr, true)
id := curr.AbsID()
err := LayoutNested(ctx, nestedGraph, GraphInfo{}, coreLayout)
if err != nil {
return err
}
InjectNested(g.Root, nestedGraph, false)
g.Edges = append(g.Edges, externalEdges...)
restoreOrder()
// need to update curr *Object incase layout changed it
var obj *d2graph.Object
for _, o := range g.Objects {
if o.AbsID() == id {
obj = o
break
}
}
if obj == nil {
return fmt.Errorf("could not find object %#v after layout", id)
}
curr = obj
// position nested graph (excluding curr) relative to curr
dx := 0 - curr.TopLeft.X
dy := 0 - curr.TopLeft.Y
for _, o := range nestedGraph.Objects {
if o.AbsID() == curr.AbsID() {
continue
}
o.TopLeft.X += dx
o.TopLeft.Y += dy
}
for _, e := range nestedGraph.Edges {
e.Move(dx, dy)
}
// now we keep the descendants out until after grid layout
nestedGraph, externalEdges = ExtractSubgraph(curr, false)
extractedEdges = append(extractedEdges, externalEdges...)
extracted[id] = nestedGraph
extractedOrder = append(extractedOrder, id)
continue
}
if !gi.isDefault() {
// empty grid or sequence can have 0 objects..
if !gi.IsConstantNear && len(curr.Children) == 0 {
continue
}
// There is a nested diagram here, so extract its contents and process in the same way
nestedGraph, externalEdges := ExtractSubgraph(curr, gi.IsConstantNear)
extractedEdges = append(extractedEdges, externalEdges...)
log.Info(ctx, "layout nested", slog.F("level", curr.Level()), slog.F("child", curr.AbsID()), slog.F("gi", gi))
nestedInfo := gi
nearKey := curr.NearKey
if gi.IsConstantNear {
// layout nested as a non-near
nestedInfo = GraphInfo{}
curr.NearKey = nil
}
err := LayoutNested(ctx, nestedGraph, nestedInfo, coreLayout)
if err != nil {
return err
}
// coreLayout can overwrite graph contents with newly created *Object pointers
// so we need to update `curr` with nestedGraph's value
if gi.IsConstantNear {
curr = nestedGraph.Root.ChildrenArray[0]
}
if gi.IsConstantNear {
curr.NearKey = nearKey
} else {
FitToGraph(curr, nestedGraph, geo.Spacing{})
curr.TopLeft = geo.NewPoint(0, 0)
}
if gi.IsConstantNear {
// near layout will inject these nestedGraphs
constantNears = append(constantNears, nestedGraph)
} else {
// We will restore the contents after running layout with child as the placeholder
// We need to reference using ID because there may be a new object to use after coreLayout
id := curr.AbsID()
extracted[id] = nestedGraph
extractedOrder = append(extractedOrder, id)
}
} else if len(curr.ChildrenArray) > 0 {
queue = append(queue, curr.ChildrenArray...)
}
}
// We can now run layout with accurate sizes of nested layout containers
// Layout according to the type of diagram
var err error
if len(g.Objects) > 0 {
switch graphInfo.DiagramType {
case GridDiagram:
log.Debug(ctx, "layout grid", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
if err = d2grid.Layout(ctx, g); err != nil {
return err
}
case SequenceDiagram:
log.Debug(ctx, "layout sequence", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
err = d2sequence.Layout(ctx, g, coreLayout)
if err != nil {
return err
}
default:
log.Debug(ctx, "default layout", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
err := coreLayout(ctx, g)
if err != nil {
return err
}
}
}
if len(constantNears) > 0 {
err = d2near.Layout(ctx, g, constantNears)
if err != nil {
return err
}
}
idToObj := make(map[string]*d2graph.Object)
for _, o := range g.Objects {
idToObj[o.AbsID()] = o
}
// With the layout set, inject all the extracted graphs
for _, id := range extractedOrder {
nestedGraph := extracted[id]
// we have to find the object by ID because coreLayout can replace the Objects in graph
obj, exists := idToObj[id]
if !exists {
return fmt.Errorf("could not find object %#v after layout", id)
}
InjectNested(obj, nestedGraph, true)
PositionNested(obj, nestedGraph)
}
// update map with injected objects
for _, o := range g.Objects {
idToObj[o.AbsID()] = o
}
// Restore cross-graph edges and route them
g.Edges = append(g.Edges, extractedEdges...)
for _, e := range extractedEdges {
// update object references
src, exists := idToObj[e.Src.AbsID()]
if !exists {
return fmt.Errorf("could not find object %#v after layout", e.Src.AbsID())
}
e.Src = src
dst, exists := idToObj[e.Dst.AbsID()]
if !exists {
return fmt.Errorf("could not find object %#v after layout", e.Dst.AbsID())
}
e.Dst = dst
// simple straight line edge routing when going across graphs
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
e.TraceToShape(e.Route, 0, 1)
if e.Label.Value != "" {
e.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
log.Debug(ctx, "done", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
return err
}
func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
if obj.Graph.RootLevel == 0 && obj.IsConstantNear() {
gi.IsConstantNear = true
}
if obj.IsSequenceDiagram() {
gi.DiagramType = SequenceDiagram
} else if obj.IsGridDiagram() {
gi.DiagramType = GridDiagram
}
return gi
}
func ExtractSubgraph(container *d2graph.Object, includeSelf bool) (nestedGraph *d2graph.Graph, externalEdges []*d2graph.Edge) {
// includeSelf: when we have a constant near or a grid cell that is a container,
// we want to include itself in the nested graph, not just its descendants,
nestedGraph = d2graph.NewGraph()
nestedGraph.RootLevel = int(container.Level())
if includeSelf {
nestedGraph.RootLevel--
}
nestedGraph.Root.Attributes = container.Attributes
nestedGraph.Root.Box = &geo.Box{}
isNestedObject := func(obj *d2graph.Object) bool {
if includeSelf {
return obj.IsDescendantOf(container)
}
return obj.Parent.IsDescendantOf(container)
}
// separate out nested edges
g := container.Graph
remainingEdges := make([]*d2graph.Edge, 0, len(g.Edges))
for _, edge := range g.Edges {
srcIsNested := isNestedObject(edge.Src)
if d2sequence.IsLifelineEnd(edge.Dst) {
// special handling for lifelines since their edge.Dst is a special Object
if srcIsNested {
nestedGraph.Edges = append(nestedGraph.Edges, edge)
} else {
remainingEdges = append(remainingEdges, edge)
}
continue
}
dstIsNested := isNestedObject(edge.Dst)
if srcIsNested && dstIsNested {
nestedGraph.Edges = append(nestedGraph.Edges, edge)
} else if srcIsNested || dstIsNested {
externalEdges = append(externalEdges, edge)
} else {
remainingEdges = append(remainingEdges, edge)
}
}
g.Edges = remainingEdges
// separate out nested objects
remainingObjects := make([]*d2graph.Object, 0, len(g.Objects))
for _, obj := range g.Objects {
if isNestedObject(obj) {
nestedGraph.Objects = append(nestedGraph.Objects, obj)
} else {
remainingObjects = append(remainingObjects, obj)
}
}
g.Objects = remainingObjects
// update object and new root references
for _, o := range nestedGraph.Objects {
o.Graph = nestedGraph
}
if includeSelf {
// remove container parent's references
if container.Parent != nil {
container.Parent.RemoveChild(container)
}
// set root references
nestedGraph.Root.ChildrenArray = []*d2graph.Object{container}
container.Parent = nestedGraph.Root
nestedGraph.Root.Children[strings.ToLower(container.ID)] = container
} else {
// set root references
nestedGraph.Root.ChildrenArray = append(nestedGraph.Root.ChildrenArray, container.ChildrenArray...)
for _, child := range container.ChildrenArray {
child.Parent = nestedGraph.Root
nestedGraph.Root.Children[strings.ToLower(child.ID)] = child
}
// remove container's references
for k := range container.Children {
delete(container.Children, k)
}
container.ChildrenArray = nil
}
return nestedGraph, externalEdges
}
func InjectNested(container *d2graph.Object, nestedGraph *d2graph.Graph, isRoot bool) {
g := container.Graph
for _, obj := range nestedGraph.Root.ChildrenArray {
obj.Parent = container
if container.Children == nil {
container.Children = make(map[string]*d2graph.Object)
}
container.Children[strings.ToLower(obj.ID)] = obj
container.ChildrenArray = append(container.ChildrenArray, obj)
}
for _, obj := range nestedGraph.Objects {
obj.Graph = g
}
g.Objects = append(g.Objects, nestedGraph.Objects...)
g.Edges = append(g.Edges, nestedGraph.Edges...)
if isRoot {
if nestedGraph.Root.LabelPosition != nil {
container.LabelPosition = nestedGraph.Root.LabelPosition
}
if nestedGraph.Root.IconPosition != nil {
container.IconPosition = nestedGraph.Root.IconPosition
}
container.Attributes = nestedGraph.Root.Attributes
}
}
func PositionNested(container *d2graph.Object, nestedGraph *d2graph.Graph) {
// tl, _ := boundingBox(nestedGraph)
// Note: assumes nestedGraph's layout has contents positioned relative to 0,0
dx := container.TopLeft.X //- tl.X
dy := container.TopLeft.Y //- tl.Y
if dx == 0 && dy == 0 {
return
}
for _, o := range nestedGraph.Objects {
o.TopLeft.X += dx
o.TopLeft.Y += dy
}
for _, e := range nestedGraph.Edges {
e.Move(dx, dy)
}
}
func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
if len(g.Objects) == 0 {
return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
}
tl = geo.NewPoint(math.Inf(1), math.Inf(1))
br = geo.NewPoint(math.Inf(-1), math.Inf(-1))
for _, obj := range g.Objects {
if obj.TopLeft == nil {
panic(obj.AbsID())
}
tl.X = math.Min(tl.X, obj.TopLeft.X)
tl.Y = math.Min(tl.Y, obj.TopLeft.Y)
br.X = math.Max(br.X, obj.TopLeft.X+obj.Width)
br.Y = math.Max(br.Y, obj.TopLeft.Y+obj.Height)
}
return tl, br
}
func FitToGraph(container *d2graph.Object, nestedGraph *d2graph.Graph, padding geo.Spacing) {
var width, height float64
width = nestedGraph.Root.Width
height = nestedGraph.Root.Height
if width == 0 || height == 0 {
tl, br := boundingBox(nestedGraph)
width = br.X - tl.X
height = br.Y - tl.Y
}
container.Width = padding.Left + width + padding.Right
container.Height = padding.Top + height + padding.Bottom
}

View file

@ -14,6 +14,23 @@ import (
const pad = 20 const pad = 20
type set map[string]struct{}
var HorizontalCenterNears = set{
"center-left": {},
"center-right": {},
}
var VerticalCenterNears = set{
"top-center": {},
"bottom-center": {},
}
var NonCenterNears = set{
"top-left": {},
"top-right": {},
"bottom-left": {},
"bottom-right": {},
}
// Layout finds the shapes which are assigned constant near keywords and places them. // Layout finds the shapes which are assigned constant near keywords and places them.
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error { func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
if len(constantNearGraphs) == 0 { if len(constantNearGraphs) == 0 {
@ -30,10 +47,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph
// Imagine the graph has two long texts, one at top center and one at top left. // Imagine the graph has two long texts, one at top center and one at top left.
// Top left should go left enough to not collide with center. // Top left should go left enough to not collide with center.
// So place the center ones first, then the later ones will consider them for bounding box // So place the center ones first, then the later ones will consider them for bounding box
for _, processCenters := range []bool{true, false} { for _, currentSet := range []set{VerticalCenterNears, HorizontalCenterNears, NonCenterNears} {
for _, tempGraph := range constantNearGraphs { for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0] obj := tempGraph.Root.ChildrenArray[0]
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") { _, in := currentSet[d2graph.Key(obj.NearKey)[0]]
if in {
prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
obj.TopLeft = geo.NewPoint(place(obj)) obj.TopLeft = geo.NewPoint(place(obj))
dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
@ -56,7 +74,8 @@ func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph
} }
for _, tempGraph := range constantNearGraphs { for _, tempGraph := range constantNearGraphs {
obj := tempGraph.Root.ChildrenArray[0] obj := tempGraph.Root.ChildrenArray[0]
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") { _, in := currentSet[d2graph.Key(obj.NearKey)[0]]
if in {
// The z-index for constant nears does not matter, as it will not collide // The z-index for constant nears does not matter, as it will not collide
g.Objects = append(g.Objects, tempGraph.Objects...) g.Objects = append(g.Objects, tempGraph.Objects...)
if obj.Parent.Children == nil { if obj.Parent.Children == nil {
@ -83,28 +102,20 @@ func place(obj *d2graph.Object) (float64, float64) {
switch nearKeyStr { switch nearKeyStr {
case "top-left": case "top-left":
x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
break
case "top-center": case "top-center":
x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
break
case "top-right": case "top-right":
x, y = br.X+pad, tl.Y-obj.Height-pad x, y = br.X+pad, tl.Y-obj.Height-pad
break
case "center-left": case "center-left":
x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2 x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
break
case "center-right": case "center-right":
x, y = br.X+pad, tl.Y+h/2-obj.Height/2 x, y = br.X+pad, tl.Y+h/2-obj.Height/2
break
case "bottom-left": case "bottom-left":
x, y = tl.X-obj.Width-pad, br.Y+pad x, y = tl.X-obj.Width-pad, br.Y+pad
break
case "bottom-center": case "bottom-center":
x, y = br.X-w/2-obj.Width/2, br.Y+pad x, y = br.X-w/2-obj.Width/2, br.Y+pad
break
case "bottom-right": case "bottom-right":
x, y = br.X+pad, br.Y+pad x, y = br.X+pad, br.Y+pad
break
} }
if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") { if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
@ -134,28 +145,6 @@ func place(obj *d2graph.Object) (float64, float64) {
return x, y 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) (constantNearGraphs []*d2graph.Graph) {
for i := 0; i < len(g.Objects); i++ {
obj := g.Objects[i]
if obj.NearKey == nil {
continue
}
_, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
if isKey {
continue
}
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isConst {
tempGraph := g.ExtractAsNestedGraph(obj)
constantNearGraphs = append(constantNearGraphs, tempGraph)
i--
}
}
return constantNearGraphs
}
// boundingBox gets the center of the graph as defined by shapes // boundingBox gets the center of the graph as defined by shapes
// The bounds taking into consideration only shapes gives more of a feeling of true center // The bounds taking into consideration only shapes gives more of a feeling of true center
// It differs from d2target.BoundingBox which needs to include every visible thing // It differs from d2target.BoundingBox which needs to include every visible thing

View file

@ -2,7 +2,6 @@ package d2sequence
import ( import (
"context" "context"
"sort"
"strings" "strings"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
@ -15,149 +14,24 @@ import (
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram // 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` // 1. Run layout on sequence diagrams
// 2. Construct a sequence diagram from all descendant objects and edges // 2. Set the resulting dimensions to the main graph shape
// 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 { func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g) // used in layout code
g.Root.Shape.Value = d2target.ShapeSequenceDiagram
sd, err := layoutSequenceDiagram(g, g.Root)
if err != nil { if err != nil {
return err return err
} }
g.Root.Box = geo.NewBox(nil, sd.getWidth()+GROUP_CONTAINER_PADDING*2, sd.getHeight()+GROUP_CONTAINER_PADDING*2)
if g.Root.IsSequenceDiagram() {
// the sequence diagram is the only layout engine if the whole diagram is // the sequence diagram is the only layout engine if the whole diagram is
// shape: sequence_diagram // shape: sequence_diagram
g.Root.TopLeft = geo.NewPoint(0, 0) g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder) obj := g.Root
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{})
sequenceDiagrams := make(map[string]*sequenceDiagram)
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.Shape.Value != d2target.ShapeSequenceDiagram {
queue = append(queue, obj.ChildrenArray...)
continue
}
sd, err := layoutSequenceDiagram(g, obj)
if err != nil {
return nil, nil, nil, err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
obj.Box = geo.NewBox(nil, sd.getWidth()+GROUP_CONTAINER_PADDING*2, sd.getHeight()+GROUP_CONTAINER_PADDING*2)
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
sequenceDiagrams[obj.AbsID()] = sd
for _, edge := range sd.messages {
edgesToRemove[edge] = struct{}{}
}
for _, obj := range sd.actors {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.notes {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.groups {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.spans {
objectsToRemove[obj] = struct{}{}
}
}
}
layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove)
g.Edges = layoutEdges
layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove)
// TODO this isn't a proper deletion because the objects still appear as children of the object
g.Objects = layoutObjects
return sequenceDiagrams, objectOrder, edgeOrder, 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
for _, edge := range g.Edges {
// both Src and Dst must be inside the sequence diagram
if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) {
edges = append(edges, edge)
}
}
sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
if err != nil {
return nil, err
}
err = sd.layout()
return sd, err
}
func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) {
edgeOrder := make(map[string]int)
layoutEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(toRemove))
for i, edge := range g.Edges {
edgeOrder[edge.AbsID()] = i
if _, exists := toRemove[edge]; !exists {
layoutEdges = append(layoutEdges, edge)
}
}
return layoutEdges, edgeOrder
}
func getLayoutObjects(g *d2graph.Graph, toRemove map[*d2graph.Object]struct{}) ([]*d2graph.Object, map[string]int) {
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)
}
}
return layoutObjects, objectOrder
}
// cleanup restores the graph after the core layout engine finishes
// - translating the sequence diagram to its position placed by the core layout engine
// - restore the children of the sequence diagram graph object
// - adds the sequence diagram edges (messages) back to the graph
// - adds the sequence diagram lifelines to the graph edges
// - adds the sequence diagram descendants back to the graph objects
// - sorts edges and objects to their original graph order
func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, objectsOrder, edgesOrder map[string]int) {
var objects []*d2graph.Object
if g.Root.IsSequenceDiagram() {
objects = []*d2graph.Object{g.Root}
} else {
objects = g.Objects
}
for _, obj := range objects {
sd, exists := sequenceDiagrams[obj.AbsID()]
if !exists {
continue
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
// shift the sequence diagrams as they are always placed at (0, 0) with some padding // shift the sequence diagrams as they are always placed at (0, 0) with some padding
@ -181,29 +55,25 @@ func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, obj
} }
} }
g.Edges = append(g.Edges, sd.messages...)
g.Edges = append(g.Edges, sd.lifelines...) 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 return nil
sort.SliceStable(g.Objects, func(i, j int) bool { }
return objectsOrder[g.Objects[i].AbsID()] < objectsOrder[g.Objects[j].AbsID()]
}) // 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) {
// sequence diagrams add lifelines, and they must be the last ones in this slice var edges []*d2graph.Edge
sort.SliceStable(g.Edges, func(i, j int) bool { for _, edge := range g.Edges {
iOrder, iExists := edgesOrder[g.Edges[i].AbsID()] // both Src and Dst must be inside the sequence diagram
jOrder, jExists := edgesOrder[g.Edges[j].AbsID()] if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) {
if iExists && jExists { edges = append(edges, edge)
return iOrder < jOrder }
} else if iExists && !jExists { }
return true
} sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
// either both don't exist or i doesn't exist and j exists if err != nil {
return false return nil, err
}) }
err = sd.layout()
return sd, err
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math" "math"
"sort" "sort"
"strconv"
"strings" "strings"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
@ -411,6 +412,25 @@ func (sd *sequenceDiagram) addLifelineEdges() {
} }
} }
func IsLifelineEnd(obj *d2graph.Object) bool {
// lifeline ends only have ID and no graph parent or box set
if obj.Graph != nil || obj.Parent != nil || obj.Box != nil {
return false
}
if !strings.Contains(obj.ID, "-lifeline-end-") {
return false
}
parts := strings.Split(obj.ID, "-lifeline-end-")
if len(parts) > 1 {
hash := parts[len(parts)-1]
actorID := strings.Join(parts[:len(parts)-1], "-lifeline-end-")
if strconv.Itoa(go2.StringToIntHash(actorID+"-lifeline-end")) == hash {
return true
}
}
return false
}
func (sd *sequenceDiagram) placeNotes() { func (sd *sequenceDiagram) placeNotes() {
rankToX := make(map[int]float64) rankToX := make(map[int]float64)
for _, actor := range sd.actors { for _, actor := range sd.actors {

View file

@ -10,10 +10,8 @@ import (
"oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter" "oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
@ -84,23 +82,8 @@ func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions,
return nil, err return nil, err
} }
constantNearGraphs := d2near.WithoutConstantNears(ctx, g) graphInfo := d2layouts.NestedGraphInfo(g.Root)
err = d2layouts.LayoutNested(ctx, g, graphInfo, 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, constantNearGraphs)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -65,7 +65,7 @@ func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
g, err = recompile(g.AST) g, err = recompile(g)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@ -112,7 +112,7 @@ func Set(g *d2graph.Graph, boardPath []string, key string, tag, value *string) (
} }
} }
return recompile(g.AST) return recompile(g)
} }
func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey, dstKey *string) (_ *d2graph.Graph, err error) { func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey, dstKey *string) (_ *d2graph.Graph, err error) {
@ -271,7 +271,7 @@ func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey,
} }
} }
return recompile(g.AST) return recompile(g)
} }
func pathFromScopeKey(g *d2graph.Graph, key *d2ast.Key, scopeak []string) ([]*d2ast.StringBox, error) { func pathFromScopeKey(g *d2graph.Graph, key *d2ast.Key, scopeak []string) ([]*d2ast.StringBox, error) {
@ -303,13 +303,15 @@ func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Objec
return pathFromScopeKey(g, key, scopeak) return pathFromScopeKey(g, key, scopeak)
} }
func recompile(ast *d2ast.Map) (*d2graph.Graph, error) { func recompile(g *d2graph.Graph) (*d2graph.Graph, error) {
s := d2format.Format(ast) s := d2format.Format(g.AST)
g, _, err := d2compiler.Compile(ast.Range.Path, strings.NewReader(s), nil) g2, _, err := d2compiler.Compile(g.AST.Range.Path, strings.NewReader(s), &d2compiler.CompileOptions{
FS: g.FS,
})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err) return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err)
} }
return g, nil return g2, nil
} }
// TODO merge flat styles // TODO merge flat styles
@ -451,7 +453,9 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
return nil return nil
} }
} }
ir, err := d2ir.Compile(g.AST, nil) ir, err := d2ir.Compile(g.AST, &d2ir.CompileOptions{
FS: g.FS,
})
if err != nil { if err != nil {
return err return err
} }
@ -489,7 +493,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
noVal2 := &tmp2 noVal2 := &tmp2
noVal1.Value = d2ast.ValueBox{} noVal1.Value = d2ast.ValueBox{}
noVal2.Value = d2ast.ValueBox{} noVal2.Value = d2ast.ValueBox{}
if noVal1.Equals(noVal2) { if noVal1.D2OracleEquals(noVal2) {
ref.MapKey.Value = mk.Value ref.MapKey.Value = mk.Value
return nil return nil
} }
@ -555,6 +559,12 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
if reserved { if reserved {
inlined := func(s *d2graph.Scalar) bool { inlined := func(s *d2graph.Scalar) bool {
if s != nil && s.MapKey != nil {
// The value was set outside of what's writeable
if s.MapKey.Range.Path != baseAST.Range.Path {
return false
}
}
return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey) return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey)
} }
reservedIndex := toSkip - 1 reservedIndex := toSkip - 1
@ -766,7 +776,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
func appendUniqueMapKey(m *d2ast.Map, mk *d2ast.Key) { func appendUniqueMapKey(m *d2ast.Map, mk *d2ast.Key) {
for _, n := range m.Nodes { for _, n := range m.Nodes {
if n.MapKey != nil && n.MapKey.Equals(mk) { if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
return return
} }
} }
@ -797,11 +807,6 @@ func appendMapKey(m *d2ast.Map, mk *d2ast.Key) {
} }
} }
func prependMapKey(m *d2ast.Map, mk *d2ast.Key) {
appendMapKey(m, mk)
m.Nodes = append([]d2ast.MapNodeBox{m.Nodes[len(m.Nodes)-1]}, m.Nodes[:len(m.Nodes)-1]...)
}
func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) { func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) {
defer xdefer.Errorf(&err, "failed to delete %#v", key) defer xdefer.Errorf(&err, "failed to delete %#v", key)
@ -890,19 +895,20 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
} }
} }
} else { } else {
prependMapKey(baseAST, mk) // NOTE: it only needs to be after the last ref, but perhaps simplest and cleanest to append all nulls at the end
appendMapKey(baseAST, mk)
} }
if len(boardPath) > 0 { if len(boardPath) > 0 {
replaced := ReplaceBoardNode(g.AST, baseAST, boardPath) replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
if !replaced { if !replaced {
return nil, fmt.Errorf("board %v AST not found", boardPath) return nil, fmt.Errorf("board %v AST not found", boardPath)
} }
return recompile(g.AST) return recompile(g)
} }
return recompile(boardG.AST) return recompile(boardG)
} }
prevG, _ := recompile(boardG.AST) prevG, _ := recompile(boardG)
boardG, err = renameConflictsToParent(boardG, mk.Key) boardG, err = renameConflictsToParent(boardG, mk.Key)
if err != nil { if err != nil {
@ -931,7 +937,7 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
return nil, err return nil, err
} }
} else { } else {
prependMapKey(baseAST, mk) appendMapKey(baseAST, mk)
} }
if len(boardPath) > 0 { if len(boardPath) > 0 {
@ -939,10 +945,10 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
if !replaced { if !replaced {
return nil, fmt.Errorf("board %v AST not found", boardPath) return nil, fmt.Errorf("board %v AST not found", boardPath)
} }
return recompile(g.AST) return recompile(g)
} }
return recompile(boardG.AST) return recompile(boardG)
} }
func bumpChildrenUnderscores(m *d2ast.Map) { func bumpChildrenUnderscores(m *d2ast.Map) {
@ -1182,7 +1188,7 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
if err := deleteEdgeField(g, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()); err != nil { if err := deleteEdgeField(g, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()); err != nil {
return nil, err return nil, err
} }
return recompile(g.AST) return recompile(g)
} }
isStyleKey := false isStyleKey := false
@ -1221,7 +1227,7 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
} }
} }
return recompile(g.AST) return recompile(g)
} }
func deleteMapField(m *d2ast.Map, field string) { func deleteMapField(m *d2ast.Map, field string) {
@ -1285,7 +1291,7 @@ func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error {
copy(tmpNodes, ref.Scope.Nodes) copy(tmpNodes, ref.Scope.Nodes)
// If I delete this, will the object still exist? // If I delete this, will the object still exist?
deleteFromMap(ref.Scope, ref.MapKey) deleteFromMap(ref.Scope, ref.MapKey)
g2, err := recompile(g.AST) g2, err := recompile(g)
if err != nil { if err != nil {
return err return err
} }
@ -1465,7 +1471,7 @@ func ensureNode(g *d2graph.Graph, excludedEdges []*d2ast.Edge, scopeObj *d2graph
} }
for _, n := range scope.Nodes { for _, n := range scope.Nodes {
if n.MapKey != nil && n.MapKey.Equals(mk) { if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
return return
} }
} }
@ -1605,10 +1611,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
ref.MapKey.Edges[ref.MapKeyEdgeIndex].SrcArrow = mk2.Edges[0].SrcArrow ref.MapKey.Edges[ref.MapKeyEdgeIndex].SrcArrow = mk2.Edges[0].SrcArrow
ref.MapKey.Edges[ref.MapKeyEdgeIndex].DstArrow = mk2.Edges[0].DstArrow ref.MapKey.Edges[ref.MapKeyEdgeIndex].DstArrow = mk2.Edges[0].DstArrow
} }
return recompile(g.AST) return recompile(g)
} }
prevG, _ := recompile(boardG.AST) prevG, _ := recompile(boardG)
ak := d2graph.Key(mk.Key) ak := d2graph.Key(mk.Key)
ak2 := d2graph.Key(mk2.Key) ak2 := d2graph.Key(mk2.Key)
@ -1916,7 +1922,7 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
ref.Key.Path = ref.Key.Path[ref.KeyPathIndex:] ref.Key.Path = ref.Key.Path[ref.KeyPathIndex:]
exists := false exists := false
for _, n := range toScope.Nodes { for _, n := range toScope.Nodes {
if n.MapKey != nil && n.MapKey != ref.MapKey && n.MapKey.Equals(ref.MapKey) { if n.MapKey != nil && n.MapKey != ref.MapKey && n.MapKey.D2OracleEquals(ref.MapKey) {
exists = true exists = true
} }
} }
@ -2026,10 +2032,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
if !replaced { if !replaced {
return nil, fmt.Errorf("board %v AST not found", boardPath) return nil, fmt.Errorf("board %v AST not found", boardPath)
} }
return recompile(g.AST) return recompile(g)
} }
return recompile(boardG.AST) return recompile(boardG)
} }
// filterReserved takes a Value and splits it into 2 // filterReserved takes a Value and splits it into 2
@ -2141,7 +2147,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
if err != nil { if err != nil {
return err return err
} }
tmpG, _ := recompile(prevG.AST) tmpG, _ := recompile(prevG)
appendMapKey(tmpG.AST, valueMK) appendMapKey(tmpG.AST, valueMK)
if to == nil { if to == nil {
deltas, err := DeleteIDDeltas(tmpG, nil, *from) deltas, err := DeleteIDDeltas(tmpG, nil, *from)
@ -2186,7 +2192,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
if err != nil { if err != nil {
return err return err
} }
tmpG, _ := recompile(prevG.AST) tmpG, _ := recompile(prevG)
appendMapKey(tmpG.AST, valueMK) appendMapKey(tmpG.AST, valueMK)
if to == nil { if to == nil {
deltas, err := DeleteIDDeltas(tmpG, nil, *from) deltas, err := DeleteIDDeltas(tmpG, nil, *from)

View file

@ -2,6 +2,9 @@ package d2oracle_test
import ( import (
"fmt" "fmt"
"io"
"io/fs"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -732,6 +735,7 @@ func TestSet(t *testing.T) {
boardPath []string boardPath []string
name string name string
text string text string
fsTexts map[string]string
key string key string
tag *string tag *string
value *string value *string
@ -1984,6 +1988,76 @@ scenarios: {
c -> d c -> d
} }
} }
`,
},
{
name: "import/1",
text: `x: {
...@meow.x
y
}
`,
fsTexts: map[string]string{
"meow": `x: {
style.fill: blue
}
`,
},
key: `x.style.stroke`,
value: go2.Pointer(`red`),
exp: `x: {
...@meow.x
y
style.stroke: red
}
`,
},
{
name: "import/2",
text: `x: {
...@meow.x
y
}
`,
fsTexts: map[string]string{
"meow": `x: {
style.fill: blue
}
`,
},
key: `x.style.fill`,
value: go2.Pointer(`red`),
exp: `x: {
...@meow.x
y
style.fill: red
}
`,
},
{
name: "import/3",
text: `x: {
...@meow.x
y
style.fill: red
}
`,
fsTexts: map[string]string{
"meow": `x: {
style.fill: blue
}
`,
},
key: `x.style.fill`,
value: go2.Pointer(`yellow`),
exp: `x: {
...@meow.x
y
style.fill: yellow
}
`, `,
}, },
} }
@ -1995,6 +2069,7 @@ scenarios: {
et := editTest{ et := editTest{
text: tc.text, text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) { testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
return d2oracle.Set(g, tc.boardPath, tc.key, tc.tag, tc.value) return d2oracle.Set(g, tc.boardPath, tc.key, tc.tag, tc.value)
}, },
@ -2412,6 +2487,7 @@ func TestRename(t *testing.T) {
boardPath []string boardPath []string
text string text string
fsTexts map[string]string
key string key string
newName string newName string
@ -2923,6 +2999,7 @@ scenarios: {
et := editTest{ et := editTest{
text: tc.text, text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) { testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
objectsBefore := len(g.Objects) objectsBefore := len(g.Objects)
var err error var err error
@ -2956,6 +3033,7 @@ func TestMove(t *testing.T) {
boardPath []string boardPath []string
text string text string
fsTexts map[string]string
key string key string
newKey string newKey string
includeDescendants bool includeDescendants bool
@ -5191,6 +5269,7 @@ scenarios: {
et := editTest{ et := editTest{
text: tc.text, text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) { testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
objectsBefore := len(g.Objects) objectsBefore := len(g.Objects)
var err error var err error
@ -5222,6 +5301,7 @@ func TestDelete(t *testing.T) {
boardPath []string boardPath []string
text string text string
fsTexts map[string]string
key string key string
expErr string expErr string
@ -6934,10 +7014,9 @@ scenarios: {
scenarios: { scenarios: {
x: { x: {
a: null
b b
c c
a: null
} }
} }
`, `,
@ -6961,10 +7040,9 @@ scenarios: {
scenarios: { scenarios: {
x: { x: {
(a -> b)[0]: null
b b
c c
(a -> b)[0]: null
} }
} }
`, `,
@ -6978,6 +7056,7 @@ scenarios: {
et := editTest{ et := editTest{
text: tc.text, text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) { testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
return d2oracle.Delete(g, tc.boardPath, tc.key) return d2oracle.Delete(g, tc.boardPath, tc.key)
}, },
@ -6993,6 +7072,7 @@ scenarios: {
type editTest struct { type editTest struct {
text string text string
fsTexts map[string]string
testFunc func(*d2graph.Graph) (*d2graph.Graph, error) testFunc func(*d2graph.Graph) (*d2graph.Graph, error)
exp string exp string
@ -7002,7 +7082,13 @@ type editTest struct {
func (tc editTest) run(t *testing.T) { func (tc editTest) run(t *testing.T) {
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name()) d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil) tfs := testFS(make(map[string]*testF))
for name, text := range tc.fsTexts {
tfs[name] = &testF{content: text}
}
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), &d2compiler.CompileOptions{
FS: tfs,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -8360,3 +8446,38 @@ scenarios: {
}) })
} }
} }
type testF struct {
content string
readIndex int
}
func (tf *testF) Close() error {
return nil
}
func (tf *testF) Read(p []byte) (int, error) {
data := []byte(tf.content)
if tf.readIndex >= len(data) {
tf.readIndex = 0
return 0, io.EOF
}
readBytes := copy(p, data[tf.readIndex:])
tf.readIndex += readBytes
return readBytes, nil
}
func (tf *testF) Stat() (os.FileInfo, error) {
return nil, nil
}
type testFS map[string]*testF
func (tfs testFS) Open(name string) (fs.File, error) {
for k := range tfs {
if strings.HasSuffix(name[:len(name)-3], k) {
return tfs[k], nil
}
}
return nil, fs.ErrNotExist
}

View file

@ -15,17 +15,29 @@ func GetBoardGraph(g *d2graph.Graph, boardPath []string) *d2graph.Graph {
} }
for i, b := range g.Layers { for i, b := range g.Layers {
if b.Name == boardPath[0] { if b.Name == boardPath[0] {
return GetBoardGraph(g.Layers[i], boardPath[1:]) g2 := GetBoardGraph(g.Layers[i], boardPath[1:])
if g2 != nil {
g2.FS = g.FS
}
return g2
} }
} }
for i, b := range g.Scenarios { for i, b := range g.Scenarios {
if b.Name == boardPath[0] { if b.Name == boardPath[0] {
return GetBoardGraph(g.Scenarios[i], boardPath[1:]) g2 := GetBoardGraph(g.Scenarios[i], boardPath[1:])
if g2 != nil {
g2.FS = g.FS
}
return g2
} }
} }
for i, b := range g.Steps { for i, b := range g.Steps {
if b.Name == boardPath[0] { if b.Name == boardPath[0] {
return GetBoardGraph(g.Steps[i], boardPath[1:]) g2 := GetBoardGraph(g.Steps[i], boardPath[1:])
if g2 != nil {
g2.FS = g.FS
}
return g2
} }
} }
return nil return nil

View file

@ -161,6 +161,8 @@ type parser struct {
// TODO: rename to Error and make existing Error a private type errorWithRange // TODO: rename to Error and make existing Error a private type errorWithRange
type ParseError struct { type ParseError struct {
// Errors from globs need to be deduplicated
ErrorsLookup map[d2ast.Error]struct{} `json:"-"`
Errors []d2ast.Error `json:"errs"` Errors []d2ast.Error `json:"errs"`
} }
@ -660,16 +662,27 @@ func (p *parser) parseMapKey() (mk *d2ast.Key) {
} }
}() }()
// Check for ampersand/@. // Check for not ampersand/@.
r, eof := p.peek() r, eof := p.peek()
if eof { if eof {
return mk return mk
} }
if r != '&' { if r == '!' {
p.rewind() r, eof := p.peek()
if eof {
return mk
}
if r == '&' {
p.commit()
mk.NotAmpersand = true
} else { } else {
p.rewind()
}
} else if r == '&' {
p.commit() p.commit()
mk.Ampersand = true mk.Ampersand = true
} else {
p.rewind()
} }
r, eof = p.peek() r, eof = p.peek()
@ -1174,6 +1187,7 @@ func (p *parser) parseUnquotedString(inKey bool) (s *d2ast.UnquotedString) {
rawv := rawb.String() rawv := rawb.String()
s.Value = append(s.Value, d2ast.InterpolationBox{String: &sv, StringRaw: &rawv}) s.Value = append(s.Value, d2ast.InterpolationBox{String: &sv, StringRaw: &rawv})
sb.Reset() sb.Reset()
rawb.Reset()
} }
s.Value = append(s.Value, d2ast.InterpolationBox{Substitution: subst}) s.Value = append(s.Value, d2ast.InterpolationBox{Substitution: subst})
continue continue

View file

@ -380,6 +380,18 @@ b-
c- c-
`, `,
}, },
{
name: "not-amper",
text: `
&k: amper
!&k: not amper
`,
assert: func(t testing.TB, ast *d2ast.Map, err error) {
assert.Success(t, err)
assert.True(t, ast.Nodes[0].MapKey.Ampersand)
assert.True(t, ast.Nodes[1].MapKey.NotAmpersand)
},
},
{ {
name: "whitespace_range", name: "whitespace_range",
text: ` a -> b -> c `, text: ` a -> b -> c `,

View file

@ -152,7 +152,6 @@ func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
// 1. It first searches the bundled plugins in the global plugins slice. // 1. It first searches the bundled plugins in the global plugins slice.
// 2. If not found, it then searches each directory in $PATH for a binary with the name // 2. If not found, it then searches each directory in $PATH for a binary with the name
// d2plugin-<name>. // d2plugin-<name>.
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
// 3. If such a binary is found, it builds an execPlugin in exec.go // 3. If such a binary is found, it builds an execPlugin in exec.go
// to get a plugin implementation around the binary and returns it. // to get a plugin implementation around the binary and returns it.
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) { func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {

View file

@ -13,6 +13,7 @@ import (
"oss.terrastruct.com/d2/lib/font" "oss.terrastruct.com/d2/lib/font"
fontlib "oss.terrastruct.com/d2/lib/font" fontlib "oss.terrastruct.com/d2/lib/font"
"oss.terrastruct.com/d2/lib/syncmap"
) )
type FontFamily string type FontFamily string
@ -44,14 +45,15 @@ func (f Font) GetEncodedSubset(corpus string) string {
FontFamiliesMu.Lock() FontFamiliesMu.Lock()
defer FontFamiliesMu.Unlock() defer FontFamiliesMu.Unlock()
fontBuf := make([]byte, len(FontFaces[f])) face := FontFaces.Get(f)
copy(fontBuf, FontFaces[f]) fontBuf := make([]byte, len(face))
copy(fontBuf, face)
fontBuf = font.UTF8CutFont(fontBuf, uniqueChars) fontBuf = font.UTF8CutFont(fontBuf, uniqueChars)
fontBuf, err := fontlib.Sfnt2Woff(fontBuf) fontBuf, err := fontlib.Sfnt2Woff(fontBuf)
if err != nil { if err != nil {
// If subset fails, return full encoding // If subset fails, return full encoding
return FontEncodings[f] return FontEncodings.Get(f)
} }
return fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(fontBuf)) return fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(fontBuf))
@ -134,165 +136,197 @@ var fuzzyBubblesBoldBase64 string
//go:embed ttf/* //go:embed ttf/*
var fontFacesFS embed.FS var fontFacesFS embed.FS
var FontEncodings map[Font]string var FontEncodings syncmap.SyncMap[Font, string]
var FontFaces map[Font][]byte var FontFaces syncmap.SyncMap[Font, []byte]
func init() { func init() {
FontEncodings = map[Font]string{ FontEncodings = syncmap.New[Font, string]()
{
FontEncodings.Set(
Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
}: sourceSansProRegularBase64, },
{ sourceSansProRegularBase64)
FontEncodings.Set(
Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}: sourceSansProBoldBase64, },
{ sourceSansProBoldBase64)
FontEncodings.Set(
Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
}: sourceSansProSemiboldBase64, },
{ sourceSansProSemiboldBase64)
FontEncodings.Set(
Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
}: sourceSansProItalicBase64, },
{ sourceSansProItalicBase64)
FontEncodings.Set(
Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
}: sourceCodeProRegularBase64, },
{ sourceCodeProRegularBase64)
FontEncodings.Set(
Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}: sourceCodeProBoldBase64, },
{ sourceCodeProBoldBase64)
FontEncodings.Set(
Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
}: sourceCodeProSemiboldBase64, },
{ sourceCodeProSemiboldBase64)
FontEncodings.Set(
Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
}: sourceCodeProItalicBase64, },
{ sourceCodeProItalicBase64)
FontEncodings.Set(
Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
}: fuzzyBubblesRegularBase64, },
{ fuzzyBubblesRegularBase64)
FontEncodings.Set(
Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
// This font has no italic, so just reuse regular // This font has no italic, so just reuse regular
}: fuzzyBubblesRegularBase64, }, fuzzyBubblesRegularBase64)
{ FontEncodings.Set(
Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}: fuzzyBubblesBoldBase64, }, fuzzyBubblesBoldBase64)
{ FontEncodings.Set(
Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
// This font has no semibold, so just reuse bold // This font has no semibold, so just reuse bold
}: fuzzyBubblesBoldBase64, }, fuzzyBubblesBoldBase64)
}
for k, v := range FontEncodings { FontEncodings.Range(func(k Font, v string) bool {
FontEncodings[k] = strings.TrimSuffix(v, "\n") FontEncodings.Set(k, strings.TrimSuffix(v, "\n"))
} return true
})
FontFaces = syncmap.New[Font, []byte]()
FontFaces = map[Font][]byte{}
b, err := fontFacesFS.ReadFile("ttf/SourceSansPro-Regular.ttf") b, err := fontFacesFS.ReadFile("ttf/SourceSansPro-Regular.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Regular.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Regular.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Bold.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Bold.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Semibold.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Semibold.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Bold.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Bold.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Semibold.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Semibold.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf") b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Regular.ttf") b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Regular.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
}] = b }, b)
FontFaces[Font{ FontFaces.Set(Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
}] = b }, b)
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf") b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
FontFaces[Font{ FontFaces.Set(Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }, b)
FontFaces[Font{ FontFaces.Set(Font{
Family: HandDrawn, Family: HandDrawn,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
}] = b }, b)
} }
var D2_FONT_TO_FAMILY = map[string]FontFamily{ var D2_FONT_TO_FAMILY = map[string]FontFamily{
@ -301,14 +335,14 @@ var D2_FONT_TO_FAMILY = map[string]FontFamily{
} }
func AddFontStyle(font Font, style FontStyle, ttf []byte) error { func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
FontFaces[font] = ttf FontFaces.Set(font, ttf)
woff, err := fontlib.Sfnt2Woff(ttf) woff, err := fontlib.Sfnt2Woff(ttf)
if err != nil { if err != nil {
return fmt.Errorf("failed to encode ttf to woff: %v", err) return fmt.Errorf("failed to encode ttf to woff: %v", err)
} }
encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woff)) encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woff))
FontEncodings[font] = encodedWoff FontEncodings.Set(font, encodedWoff)
return nil return nil
} }
@ -332,8 +366,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_REGULAR, Style: FONT_STYLE_REGULAR,
} }
FontFaces[regularFont] = FontFaces[fallbackFont] FontFaces.Set(regularFont, FontFaces.Get(fallbackFont))
FontEncodings[regularFont] = FontEncodings[fallbackFont] FontEncodings.Set(regularFont, FontEncodings.Get(fallbackFont))
} }
italicFont := Font{ italicFont := Font{
@ -350,8 +384,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_ITALIC, Style: FONT_STYLE_ITALIC,
} }
FontFaces[italicFont] = FontFaces[fallbackFont] FontFaces.Set(italicFont, FontFaces.Get(fallbackFont))
FontEncodings[italicFont] = FontEncodings[fallbackFont] FontEncodings.Set(italicFont, FontEncodings.Get(fallbackFont))
} }
boldFont := Font{ boldFont := Font{
@ -368,8 +402,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
} }
FontFaces[boldFont] = FontFaces[fallbackFont] FontFaces.Set(boldFont, FontFaces.Get(fallbackFont))
FontEncodings[boldFont] = FontEncodings[fallbackFont] FontEncodings.Set(boldFont, FontEncodings.Get(fallbackFont))
} }
semiboldFont := Font{ semiboldFont := Font{
@ -386,8 +420,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
Family: SourceSansPro, Family: SourceSansPro,
Style: FONT_STYLE_SEMIBOLD, Style: FONT_STYLE_SEMIBOLD,
} }
FontFaces[semiboldFont] = FontFaces[fallbackFont] FontFaces.Set(semiboldFont, FontFaces.Get(fallbackFont))
FontEncodings[semiboldFont] = FontEncodings[fallbackFont] FontEncodings.Set(semiboldFont, FontEncodings.Get(fallbackFont))
} }
FontFamilies = append(FontFamilies, customFontFamily) FontFamilies = append(FontFamilies, customFontFamily)

View file

@ -14,8 +14,9 @@ func TestCutFont(t *testing.T) {
Family: SourceCodePro, Family: SourceCodePro,
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
} }
fontBuf := make([]byte, len(FontFaces[f])) face := FontFaces.Get(f)
copy(fontBuf, FontFaces[f]) fontBuf := make([]byte, len(face))
copy(fontBuf, face)
fontBuf = font.UTF8CutFont(fontBuf, " 1") fontBuf = font.UTF8CutFont(fontBuf, " 1")
err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf) err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf)
assert.Success(t, err) assert.Success(t, err)

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor(); const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor();
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor) MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
const html = MathJax._.mathjax.mathjax.document('', { const html = MathJax._.mathjax.mathjax.document('', {
InputJax: new MathJax._.input.tex_ts.TeX({ packages: ['base', 'mathtools', 'amscd', 'braket', 'cancel', 'cases', 'color', 'gensymb', 'mhchem', 'physics'] }), InputJax: new MathJax._.input.tex_ts.TeX({ packages: ['base', 'mathtools', 'ams', 'amscd', 'braket', 'cancel', 'cases', 'color', 'gensymb', 'mhchem', 'physics'] }),
OutputJax: new MathJax._.output.svg_ts.SVG(), OutputJax: new MathJax._.output.svg_ts.SVG(),
}); });

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3902229455 .text-bold { .d2-3902229455 .text-bold {
font-family: "d2-3902229455-font-bold"; font-family: "d2-3902229455-font-bold";
} }

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3902229455 .text-bold { .d2-3902229455 .text-bold {
font-family: "d2-3902229455-font-bold"; font-family: "d2-3902229455-font-bold";
} }

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2916329547 .text { .d2-2916329547 .text {
font-family: "d2-2916329547-font-regular"; font-family: "d2-2916329547-font-regular";
} }

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2916329547 .text { .d2-2916329547 .text {
font-family: "d2-2916329547-font-regular"; font-family: "d2-2916329547-font-regular";
} }

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3148583989 .text-bold { .d2-3148583989 .text-bold {
font-family: "d2-3148583989-font-bold"; font-family: "d2-3148583989-font-bold";
} }

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3148583989 .text-bold { .d2-3148583989 .text-bold {
font-family: "d2-3148583989-font-bold"; font-family: "d2-3148583989-font-bold";
} }

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-327680947 .text-bold { .d2-327680947 .text-bold {
font-family: "d2-327680947-font-bold"; font-family: "d2-327680947-font-bold";
} }

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-327680947 .text-bold { .d2-327680947 .text-bold {
font-family: "d2-327680947-font-bold"; font-family: "d2-327680947-font-bold";
} }

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-914436609 .text { .d2-914436609 .text {
font-family: "d2-914436609-font-regular"; font-family: "d2-914436609-font-regular";
} }

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-914436609 .text { .d2-914436609 .text {
font-family: "d2-914436609-font-regular"; font-family: "d2-914436609-font-regular";
} }

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" 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-2730605657 .text-mono { .d2-2730605657 .text-mono {
font-family: "d2-2730605657-font-mono"; font-family: "d2-2730605657-font-mono";
} }

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View file

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

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" 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-2730605657 .text-mono { .d2-2730605657 .text-mono {
font-family: "d2-2730605657-font-mono"; font-family: "d2-2730605657-font-mono";
} }

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2029734873 .text-bold { .d2-2029734873 .text-bold {
font-family: "d2-2029734873-font-bold"; font-family: "d2-2029734873-font-bold";
} }

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2029734873 .text-bold { .d2-2029734873 .text-bold {
font-family: "d2-2029734873-font-bold"; font-family: "d2-2029734873-font-bold";
} }

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-341527886 .text-bold { .d2-341527886 .text-bold {
font-family: "d2-341527886-font-bold"; font-family: "d2-341527886-font-bold";
} }

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-341527886 .text-bold { .d2-341527886 .text-bold {
font-family: "d2-341527886-font-bold"; font-family: "d2-341527886-font-bold";
} }

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 398 285"><svg id="d2-svg" class="d2-1379818723" width="398" height="285" viewBox="-101 -115 398 285"><rect x="-101.000000" y="-115.000000" width="398.000000" height="285.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 398 285"><svg id="d2-svg" class="d2-1379818723" width="398" height="285" viewBox="-101 -115 398 285"><rect x="-101.000000" y="-115.000000" width="398.000000" height="285.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1379818723 .text-bold { .d2-1379818723 .text-bold {
font-family: "d2-1379818723-font-bold"; font-family: "d2-1379818723-font-bold";
} }

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-1207082110" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-1207082110" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1207082110 .text-bold { .d2-1207082110 .text-bold {
font-family: "d2-1207082110-font-bold"; font-family: "d2-1207082110-font-bold";
} }

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1441 721"><svg id="d2-svg" class="d2-1360573900" width="1441" height="721" viewBox="-101 -112 1441 721"><rect x="-101.000000" y="-112.000000" width="1441.000000" height="721.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1441 721"><svg id="d2-svg" class="d2-1360573900" width="1441" height="721" viewBox="-101 -112 1441 721"><rect x="-101.000000" y="-112.000000" width="1441.000000" height="721.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1360573900 .text-bold { .d2-1360573900 .text-bold {
font-family: "d2-1360573900-font-bold"; font-family: "d2-1360573900-font-bold";
} }

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1052 863"><svg id="d2-svg" class="d2-611371411" width="1052" height="863" viewBox="-61 -1 1052 863"><rect x="-61.000000" y="-1.000000" width="1052.000000" height="863.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1052 863"><svg id="d2-svg" class="d2-611371411" width="1052" height="863" viewBox="-61 -1 1052 863"><rect x="-61.000000" y="-1.000000" width="1052.000000" height="863.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-611371411 .text-mono { .d2-611371411 .text-mono {
font-family: "d2-611371411-font-mono"; font-family: "d2-611371411-font-mono";
} }

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 922 408"><svg id="d2-svg" class="d2-2864094478" width="922" height="408" viewBox="-91 -141 922 408"><rect x="-91.000000" y="-141.000000" width="922.000000" height="408.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 922 408"><svg id="d2-svg" class="d2-2864094478" width="922" height="408" viewBox="-91 -141 922 408"><rect x="-91.000000" y="-141.000000" width="922.000000" height="408.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2864094478 .text { .d2-2864094478 .text {
font-family: "d2-2864094478-font-regular"; font-family: "d2-2864094478-font-regular";
} }

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 337 560"><svg id="d2-svg" class="d2-2874225056" width="337" height="560" viewBox="-89 -89 337 560"><rect x="-89.000000" y="-89.000000" width="337.000000" height="560.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 337 560"><svg id="d2-svg" class="d2-2874225056" width="337" height="560" viewBox="-89 -89 337 560"><rect x="-89.000000" y="-89.000000" width="337.000000" height="560.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2874225056 .text-bold { .d2-2874225056 .text-bold {
font-family: "d2-2874225056-font-bold"; font-family: "d2-2874225056-font-bold";
} }

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 676 434"><svg id="d2-svg" class="d2-2558489054" width="676" height="434" viewBox="-101 -101 676 434"><rect x="-101.000000" y="-101.000000" width="676.000000" height="434.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 676 434"><svg id="d2-svg" class="d2-2558489054" width="676" height="434" viewBox="-101 -101 676 434"><rect x="-101.000000" y="-101.000000" width="676.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2558489054 .text-bold { .d2-2558489054 .text-bold {
font-family: "d2-2558489054-font-bold"; font-family: "d2-2558489054-font-bold";
} }

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1098532122 .text { .d2-1098532122 .text {
font-family: "d2-1098532122-font-regular"; font-family: "d2-1098532122-font-regular";
} }

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1098532122 .text { .d2-1098532122 .text {
font-family: "d2-1098532122-font-regular"; font-family: "d2-1098532122-font-regular";
} }

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" 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-4290168978 .text-bold { .d2-4290168978 .text-bold {
font-family: "d2-4290168978-font-bold"; font-family: "d2-4290168978-font-bold";
} }

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 516 636"><svg id="d2-svg" class="d2-3736831304" width="516" height="636" viewBox="-78 -122 516 636"><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" fill="#947A6D" stroke-width="0" /><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" class="paper-overlay" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 516 636"><svg id="d2-svg" class="d2-3736831304" width="516" height="636" viewBox="-78 -122 516 636"><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" fill="#947A6D" stroke-width="0" /><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" class="paper-overlay" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3736831304 .text-mono { .d2-3736831304 .text-mono {
font-family: "d2-3736831304-font-mono"; font-family: "d2-3736831304-font-mono";
} }

Before

Width:  |  Height:  |  Size: 497 KiB

After

Width:  |  Height:  |  Size: 497 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2504113906 .text { .d2-2504113906 .text {
font-family: "d2-2504113906-font-regular"; font-family: "d2-2504113906-font-regular";
} }

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2504113906 .text { .d2-2504113906 .text {
font-family: "d2-2504113906-font-regular"; font-family: "d2-2504113906-font-regular";
} }

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 832 1627"><svg id="d2-svg" class="d2-790545213" width="832" height="1627" viewBox="-92 -101 832 1627"><rect x="-92.000000" y="-101.000000" width="832.000000" height="1627.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 832 1627"><svg id="d2-svg" class="d2-790545213" width="832" height="1627" viewBox="-92 -101 832 1627"><rect x="-92.000000" y="-101.000000" width="832.000000" height="1627.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-790545213 .text-mono { .d2-790545213 .text-mono {
font-family: "d2-790545213-font-mono"; font-family: "d2-790545213-font-mono";
} }

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-103900560 .text { .d2-103900560 .text {
font-family: "d2-103900560-font-regular"; font-family: "d2-103900560-font-regular";
} }

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-103900560 .text { .d2-103900560 .text {
font-family: "d2-103900560-font-regular"; font-family: "d2-103900560-font-regular";
} }

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View file

@ -129,7 +129,7 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
font-family: font-regular; font-family: font-regular;
src: url("%s"); src: url("%s");
} }
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)]) ]]></style>`, d2fonts.FontEncodings.Get(d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)))
} }
if !strings.Contains(svg, `font-family: "font-bold"`) { if !strings.Contains(svg, `font-family: "font-bold"`) {
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[ appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
@ -140,7 +140,7 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
font-family: font-bold; font-family: font-bold;
src: url("%s"); src: url("%s");
} }
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)]) ]]></style>`, d2fonts.FontEncodings.Get(d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)))
} }
closingIndex := strings.LastIndex(svg, "</svg></svg>") closingIndex := strings.LastIndex(svg, "</svg></svg>")

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1431 1588"><svg id="d2-svg" class="d2-4070036272" width="1431" height="1588" viewBox="-192 -70 1431 1588"><rect x="-192.000000" y="-70.000000" width="1431" height="1588" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1431 1588"><svg id="d2-svg" class="d2-4070036272" width="1431" height="1588" viewBox="-192 -70 1431 1588"><rect x="-192.000000" y="-70.000000" width="1431" height="1588" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-4070036272 .text { .d2-4070036272 .text {
font-family: "d2-4070036272-font-regular"; font-family: "d2-4070036272-font-regular";
} }

Before

Width:  |  Height:  |  Size: 677 KiB

After

Width:  |  Height:  |  Size: 677 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-3057089836" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-3057089836" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
} }

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 657 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-3606605273" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-3606605273" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
} }

Before

Width:  |  Height:  |  Size: 662 KiB

After

Width:  |  Height:  |  Size: 662 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-4098508292" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-4098508292" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
} }

Before

Width:  |  Height:  |  Size: 662 KiB

After

Width:  |  Height:  |  Size: 662 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-3026443247" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" fill="PaleVioletRed" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-3026443247" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" fill="PaleVioletRed" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
} }

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-2257413360" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-2257413360" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
} }

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

View file

@ -613,6 +613,15 @@ func renderArrowheadLabel(connection d2target.Connection, text string, isDst boo
textEl.X = baselineCenter.X textEl.X = baselineCenter.X
textEl.Y = baselineCenter.Y textEl.Y = baselineCenter.Y
textEl.Fill = d2target.FG_COLOR textEl.Fill = d2target.FG_COLOR
if isDst {
if connection.DstLabel.Color != "" {
textEl.Fill = connection.DstLabel.Color
}
} else {
if connection.SrcLabel.Color != "" {
textEl.Fill = connection.SrcLabel.Color
}
}
textEl.ClassName = "text-italic" textEl.ClassName = "text-italic"
textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize) textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
textEl.Content = RenderText(text, textEl.X, height) textEl.Content = RenderText(text, textEl.X, height)
@ -1258,7 +1267,13 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
if !isLight { if !isLight {
class = "dark-code" class = "dark-code"
} }
fmt.Fprintf(writer, `<g transform="translate(%f %f)" class="%s">`, box.TopLeft.X, box.TopLeft.Y, class) var fontSize string
if targetShape.FontSize != d2fonts.FONT_SIZE_M {
fontSize = fmt.Sprintf(` style="font-size:%v"`, targetShape.FontSize)
}
fmt.Fprintf(writer, `<g transform="translate(%f %f)" class="%s"%s>`,
box.TopLeft.X, box.TopLeft.Y, class, fontSize,
)
rectEl := d2themes.NewThemableElement("rect") rectEl := d2themes.NewThemableElement("rect")
rectEl.Width = float64(targetShape.Width) rectEl.Width = float64(targetShape.Width)
rectEl.Height = float64(targetShape.Height) rectEl.Height = float64(targetShape.Height)
@ -1312,9 +1327,20 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
mdEl := d2themes.NewThemableElement("div") mdEl := d2themes.NewThemableElement("div")
mdEl.ClassName = "md" mdEl.ClassName = "md"
mdEl.Content = render mdEl.Content = render
// We have to set with styles since within foreignObject, we're in html
// land and not SVG attributes
var styles []string
if targetShape.FontSize != textmeasure.MarkdownFontSize { if targetShape.FontSize != textmeasure.MarkdownFontSize {
mdEl.Style = fmt.Sprintf("font-size:%vpx", targetShape.FontSize) styles = append(styles, fmt.Sprintf("font-size:%vpx", targetShape.FontSize))
} }
if !color.IsThemeColor(targetShape.Color) {
styles = append(styles, fmt.Sprintf(`color:%s`, targetShape.Color))
}
mdEl.Style = strings.Join(styles, ";")
fmt.Fprint(writer, mdEl.Render()) fmt.Fprint(writer, mdEl.Render())
fmt.Fprint(writer, `</foreignObject></g>`) fmt.Fprint(writer, `</foreignObject></g>`)
} else { } else {

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3902229455 .text-bold { .d2-3902229455 .text-bold {
font-family: "d2-3902229455-font-bold"; font-family: "d2-3902229455-font-bold";
} }

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2916329547 .text { .d2-2916329547 .text {
font-family: "d2-2916329547-font-regular"; font-family: "d2-2916329547-font-regular";
} }

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3148583989 .text-bold { .d2-3148583989 .text-bold {
font-family: "d2-3148583989-font-bold"; font-family: "d2-3148583989-font-bold";
} }

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-327680947 .text-bold { .d2-327680947 .text-bold {
font-family: "d2-327680947-font-bold"; font-family: "d2-327680947-font-bold";
} }

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-914436609 .text { .d2-914436609 .text {
font-family: "d2-914436609-font-regular"; font-family: "d2-914436609-font-regular";
} }

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" 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-2730605657 .text-mono { .d2-2730605657 .text-mono {
font-family: "d2-2730605657-font-mono"; font-family: "d2-2730605657-font-mono";
} }

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 985 417"><svg id="d2-svg" class="d2-1533752388" width="985" height="417" viewBox="-101 -101 985 417"><rect x="-101.000000" y="-101.000000" width="985.000000" height="417.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 985 417"><svg id="d2-svg" class="d2-1533752388" width="985" height="417" viewBox="-101 -101 985 417"><rect x="-101.000000" y="-101.000000" width="985.000000" height="417.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1533752388 .text { .d2-1533752388 .text {
font-family: "d2-1533752388-font-regular"; font-family: "d2-1533752388-font-regular";
} }

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2029734873 .text-bold { .d2-2029734873 .text-bold {
font-family: "d2-2029734873-font-bold"; font-family: "d2-2029734873-font-bold";
} }

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1098532122 .text { .d2-1098532122 .text {
font-family: "d2-1098532122-font-regular"; font-family: "d2-1098532122-font-regular";
} }

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" 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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" 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-4290168978 .text-bold { .d2-4290168978 .text-bold {
font-family: "d2-4290168978-font-bold"; font-family: "d2-4290168978-font-bold";
} }

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.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.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2504113906 .text { .d2-2504113906 .text {
font-family: "d2-2504113906-font-regular"; font-family: "d2-2504113906-font-regular";
} }

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

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