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:
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
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)
- **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)
- **MkDocs Plugin**: [https://github.com/landmaj/mkdocs-d2-plugin](https://github.com/landmaj/mkdocs-d2-plugin)
### Misc

View file

@ -4,7 +4,7 @@ cd -- "$(dirname "$0")/../.."
. ./ci/sub/lib.sh
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)"
docker_run \
-e DRY_RUN \

View file

@ -1,9 +1,21 @@
#### 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 🧹
- 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 ⛑️
- 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
}
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
type Key struct {
Range Range `json:"range"`
@ -613,6 +622,9 @@ type Key struct {
// Indicates this MapKey is a filter selector.
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.
// The following are all valid MapKeys:
// Key:
@ -638,8 +650,13 @@ type Key struct {
Value ValueBox `json:"value"`
}
// TODO maybe need to compare Primary
func (mk1 *Key) Equals(mk2 *Key) bool {
func (mk1 *Key) D2OracleEquals(mk2 *Key) bool {
if mk1 == nil && mk2 == nil {
return true
}
if (mk1 == nil) || (mk2 == nil) {
return false
}
if mk1.Ampersand != mk2.Ampersand {
return false
}
@ -712,6 +729,104 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
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) {
if mk.Value.Unbox() != nil && mk.Value.ScalarBox().Unbox() == nil {
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 {
return true
}
@ -733,6 +884,11 @@ func (mk *Key) HasQueryGlob() bool {
return false
}
func (mk *Key) Copy() *Key {
mk2 := *mk
return &mk2
}
type KeyPath struct {
Range Range `json:"range"`
Path []*StringBox `json:"path"`
@ -760,16 +916,16 @@ func (kp *KeyPath) Copy() *KeyPath {
return &kp2
}
func (kp *KeyPath) HasDoubleGlob() bool {
if kp == nil {
return false
func (kp *KeyPath) Last() *StringBox {
return kp.Path[len(kp.Path)-1]
}
for _, el := range kp.Path {
if el.UnquotedString != nil && el.ScalarString() == "**" {
return true
func IsDoubleGlob(pattern []string) bool {
return len(pattern) == 3 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*"
}
}
return false
func IsTripleGlob(pattern []string) bool {
return len(pattern) == 5 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*" && pattern[3] == "" && pattern[4] == "*"
}
func (kp *KeyPath) HasGlob() bool {
@ -784,6 +940,54 @@ func (kp *KeyPath) HasGlob() bool {
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 {
Range Range `json:"range"`
@ -796,6 +1000,22 @@ type Edge struct {
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 {
Range Range `json:"range"`
@ -804,6 +1024,16 @@ type EdgeIndex struct {
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 {
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.
type StringBox struct {
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`

View file

@ -37,6 +37,7 @@ import (
"oss.terrastruct.com/d2/lib/pdf"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/pptx"
"oss.terrastruct.com/d2/lib/simplelog"
"oss.terrastruct.com/d2/lib/textmeasure"
timelib "oss.terrastruct.com/d2/lib/time"
"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)
if err != nil {
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
}
@ -434,7 +435,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, la
if err != nil {
return nil, false, err
}
out, err := xgif.AnimatePNGs(ms, pngs, int(animateInterval))
out, err := AnimatePNGs(ms, pngs, int(animateInterval))
if err != nil {
return nil, false, err
}
@ -748,10 +749,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
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 {
var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
}
if forceAppendix && !toPNG {
@ -764,11 +767,11 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
if !bundle {
var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
}
out, err = png.ConvertSVG(ms, page, svg)
out, err = ConvertSVG(ms, page, svg)
if err != nil {
return svg, err
}
@ -833,15 +836,17 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
return svg, err
}
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
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)
if bundleErr != nil {
return svg, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg)
pngImg, err := ConvertSVG(ms, page, svg)
if err != nil {
return svg, err
}
@ -933,8 +938,10 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
return nil, err
}
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
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)
if bundleErr != nil {
return nil, bundleErr
@ -942,7 +949,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
svg = appendix.Append(diagram, ruler, svg)
pngImg, err := png.ConvertSVG(ms, page, svg)
pngImg, err := ConvertSVG(ms, page, svg)
if err != nil {
return nil, err
}
@ -1178,8 +1185,10 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
return nil, nil, err
}
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
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)
if bundleErr != nil {
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)
pngImg, err := png.ConvertSVG(ms, page, svg)
pngImg, err := ConvertSVG(ms, page, svg)
if err != nil {
return nil, nil, err
}
@ -1218,3 +1227,21 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
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 {
return nil, nil, err
}
g.FS = opts.FS
g.SortObjectsByAST()
g.SortEdgesByAST()
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 {
c.validateKeys(g.Root, ir)
}
c.validateLabels(g)
c.validateNear(g)
c.validateEdges(g)
@ -175,7 +177,14 @@ type compiler struct {
}
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) {
@ -282,6 +291,10 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
return
} else if f.Name == "vars" {
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 {
c.compileReserved(&obj.Attributes, f)
return
@ -321,21 +334,21 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
}
for _, fr := range f.References {
if fr.Primary() {
if fr.Context.Key.Value.Map != nil {
obj.Map = fr.Context.Key.Value.Map
if fr.Context_.Key.Value.Map != nil {
obj.Map = fr.Context_.Key.Value.Map
}
}
r := d2graph.Reference{
Key: fr.KeyPath,
KeyPathIndex: fr.KeyPathIndex(),
MapKey: fr.Context.Key,
MapKeyEdgeIndex: fr.Context.EdgeIndex(),
Scope: fr.Context.Scope,
ScopeAST: fr.Context.ScopeAST,
MapKey: fr.Context_.Key,
MapKeyEdgeIndex: fr.Context_.EdgeIndex(),
Scope: fr.Context_.Scope,
ScopeAST: fr.Context_.ScopeAST,
}
if fr.Context.ScopeMap != nil && !d2ir.IsVar(fr.Context.ScopeMap) {
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context.ScopeMap))
if fr.Context_.ScopeMap != nil && !d2ir.IsVar(fr.Context_.ScopeMap) {
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context_.ScopeMap))
r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA)
}
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 {
for _, constraint := range arr.Values {
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())
}
}
}
}
case "label", "icon":
c.compilePosition(attrs, f)
default:
@ -724,14 +742,14 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
edge.Label.MapKey = e.LastPrimaryKey()
for _, er := range e.References {
r := d2graph.EdgeReference{
Edge: er.Context.Edge,
MapKey: er.Context.Key,
MapKeyEdgeIndex: er.Context.EdgeIndex(),
Scope: er.Context.Scope,
ScopeAST: er.Context.ScopeAST,
Edge: er.Context_.Edge,
MapKey: er.Context_.Key,
MapKeyEdgeIndex: er.Context_.EdgeIndex(),
Scope: er.Context_.Scope,
ScopeAST: er.Context_.ScopeAST,
}
if er.Context.ScopeMap != nil && !d2ir.IsVar(er.Context.ScopeMap) {
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context.ScopeMap))
if er.Context_.ScopeMap != nil && !d2ir.IsVar(er.Context_.ScopeMap) {
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context_.ScopeMap))
r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA)
}
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) {
for _, obj := range g.Objects {
if obj.NearKey != nil {
@ -1050,20 +1084,13 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
for _, edge := range g.Edges {
srcNearContainer := edge.Src.OuterNearContainer()
dstNearContainer := edge.Dst.OuterNearContainer()
var isSrcNearConst, isDstNearConst bool
if srcNearContainer != nil {
_, isSrcNearConst = d2graph.NearConstants[d2graph.Key(srcNearContainer.NearKey)[0]]
if edge.Src.IsConstantNear() && edge.Dst.IsDescendantOf(edge.Src) {
c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Src.AbsID())
continue
}
if dstNearContainer != nil {
_, isDstNearConst = d2graph.NearConstants[d2graph.Key(dstNearContainer.NearKey)[0]]
}
if (isSrcNearConst || isDstNearConst) && srcNearContainer != dstNearContainer {
c.errorf(edge.References[0].Edge, "cannot connect objects from within a container, that has near constant set, to objects outside that container")
if edge.Dst.IsConstantNear() && edge.Src.IsDescendantOf(edge.Dst) {
c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Dst.AbsID())
continue
}
}
@ -1071,12 +1098,32 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges {
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
// edges from a grid to something outside is ok
// 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
}
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
if edge.Dst.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
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
}
}

View file

@ -1616,7 +1616,7 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
}
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",
@ -1627,7 +1627,7 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
}
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",
@ -2040,7 +2040,7 @@ b
}
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",
@ -2199,6 +2199,19 @@ ok: {
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",
text: `Chinchillas: {
@ -2476,16 +2489,75 @@ d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must
name: "grid_edge",
text: `hey: {
grid-rows: 1
a -> b
a -> b: ok
}
c -> hey.b
hey.a -> c
hey -> hey.a
hey -> c: ok
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are not supported yet`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:7:1: edge from grid diagram "hey" cannot enter itself`,
},
{
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",
@ -2625,6 +2697,28 @@ a -> b: { class: [association; one target] }
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",
text: `classes: {
@ -2694,6 +2788,40 @@ object: {
`,
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 {
@ -2745,6 +2873,7 @@ func TestCompile2(t *testing.T) {
t.Run("seqdiagrams", testSeqDiagrams)
t.Run("nulls", testNulls)
t.Run("vars", testVars)
t.Run("globs", testGlobs)
}
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`)
},
},
{
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 {
@ -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) {
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
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,
LabelHeight: edge.SrcArrowhead.LabelDimensions.Height,
}
if edge.SrcArrowhead.Style.FontColor != nil {
connection.SrcLabel.Color = edge.SrcArrowhead.Style.FontColor.Value
}
}
}
if edge.DstArrow {
@ -228,6 +231,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
LabelWidth: edge.DstArrowhead.LabelDimensions.Width,
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 {

View file

@ -17,9 +17,8 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts"
"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/d2target"
"oss.terrastruct.com/d2/lib/geo"
@ -235,7 +234,8 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil)
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 {
t.Fatal(err)
}

View file

@ -810,6 +810,25 @@ steps: {
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"
"errors"
"fmt"
"io/fs"
"math"
"net/url"
"sort"
@ -36,6 +37,7 @@ const DEFAULT_SHAPE_SIZE = 100.
const MIN_SHAPE_SIZE = 5
type Graph struct {
FS fs.FS `json:"-"`
Parent *Graph `json:"-"`
Name string `json:"name"`
// IsFolderOnly indicates a board or scenario itself makes no modifications from its
@ -55,6 +57,9 @@ type Graph struct {
Steps []*Graph `json:"steps,omitempty"`
Theme *d2themes.Theme `json:"theme,omitempty"`
// Object.Level uses the location of a nested graph
RootLevel int `json:"rootLevel,omitempty"`
}
func NewGraph() *Graph {
@ -527,7 +532,7 @@ func (obj *Object) GetStroke(dashGapSize interface{}) string {
func (obj *Object) Level() ContainerLevel {
if obj.Parent == nil {
return 0
return ContainerLevel(obj.Graph.RootLevel)
}
return 1 + obj.Parent.Level()
}
@ -1085,6 +1090,21 @@ func (obj *Object) OuterNearContainer() *Object {
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 {
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 {
srcIDA := e.Src.AbsIDArray()
dstIDA := e.Dst.AbsIDArray()
@ -1198,10 +1225,6 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
src := obj.ensureChildEdge(srcID)
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{
Attributes: Attributes{
Label: Scalar{
@ -1898,7 +1921,7 @@ func (g *Graph) PrintString() string {
buf := &bytes.Buffer{}
fmt.Fprint(buf, "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, "]")
return buf.String()

View file

@ -14,3 +14,33 @@ func (obj *Object) ClosestGridDiagram() *Object {
}
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
import (
"math"
"sort"
"strings"
@ -26,7 +27,7 @@ func (obj *Object) MoveWithDescendantsTo(x, y float64) {
obj.MoveWithDescendants(dx, dy)
}
func (parent *Object) removeChild(child *Object) {
func (parent *Object) RemoveChild(child *Object) {
delete(parent.Children, strings.ToLower(child.ID))
for i := 0; i < len(parent.ChildrenArray); i++ {
if parent.ChildrenArray[i] == child {
@ -41,6 +42,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
descendantObjects, edges := pluckObjAndEdges(g, obj)
tempGraph := NewGraph()
tempGraph.RootLevel = int(obj.Level()) - 1
tempGraph.Root.ChildrenArray = []*Object{obj}
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
@ -50,7 +52,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
tempGraph.Objects = descendantObjects
tempGraph.Edges = edges
obj.Parent.removeChild(obj)
obj.Parent.RemoveChild(obj)
obj.Parent = tempGraph.Root
return tempGraph
@ -59,7 +61,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) {
for i := 0; i < len(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)
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
i--
@ -68,15 +70,10 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge
for i := 0; i < len(g.Objects); i++ {
temp := g.Objects[i]
if temp.AbsID() == obj.AbsID() {
descendantsObjects = append(descendantsObjects, obj)
if temp.IsDescendantOf(obj) {
descendantsObjects = append(descendantsObjects, temp)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
for _, child := range obj.ChildrenArray {
subObjects, subEdges := pluckObjAndEdges(g, child)
descendantsObjects = append(descendantsObjects, subObjects...)
edges = append(edges, subEdges...)
}
break
i--
}
}
@ -85,7 +82,12 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
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
for _, obj := range tempGraph.Objects {
obj.Graph = g
@ -284,6 +286,76 @@ func (obj *Object) GetModifierElementAdjustments() (dx, dy float64) {
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 {
tl := obj.TopLeft
if tl == nil {

View file

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

View file

@ -5,14 +5,25 @@ import (
"strconv"
"strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2themes"
"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 {
err *d2parser.ParseError
@ -23,7 +34,13 @@ type compiler struct {
importCache map[string]*Map
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 {
@ -49,8 +66,8 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, error) {
}
m := &Map{}
m.initRoot()
m.parent.(*Field).References[0].Context.Scope = ast
m.parent.(*Field).References[0].Context.ScopeAST = ast
m.parent.(*Field).References[0].Context_.Scope = ast
m.parent.(*Field).References[0].Context_.ScopeAST = ast
c.pushImportStack(&d2ast.Import{
Path: []*d2ast.StringBox{d2ast.RawStringBox(ast.GetRange().Path, true)},
@ -83,7 +100,7 @@ func (c *compiler) overlayClasses(m *Map) {
for _, lf := range layers.Fields {
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
}
l := lf.Map()
@ -108,6 +125,8 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
if f.Name == "vars" && f.Map() != nil {
varsStack = append([]*Map{f.Map()}, varsStack...)
}
}
for _, f := range m.Fields {
if f.Primary() != nil {
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) {
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
}
base = base.CopyBase(f)
@ -345,7 +364,26 @@ func (c *compiler) overlay(base *Map, f *Field) {
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 {
switch {
case n.MapKey != nil:
@ -355,11 +393,56 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
ScopeMap: dst,
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 {
return
}
}
}
for _, n := range ast.Nodes {
switch {
case n.MapKey != nil:
@ -378,6 +461,13 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
Value: []d2ast.InterpolationBox{{Substitution: n.Substitution}},
},
},
References: []*FieldReference{{
Context_: &RefContext{
Scope: ast,
ScopeMap: dst,
ScopeAST: scopeAST,
},
}},
}
dst.Fields = append(dst.Fields, f)
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")
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())
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) {
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 {
c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx)
} else {
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) {
@ -416,7 +573,7 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
return
}
fa, err := dst.EnsureField(kp, refctx, true)
fa, err := dst.EnsureField(kp, refctx, true, c)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
@ -431,7 +588,7 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
if !refctx.Key.Ampersand {
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")
return false
}
@ -439,14 +596,51 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
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 {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return false
}
if len(fa) == 0 {
if refctx.Key.Key.Last().ScalarString() != "label" {
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 {
ok := c._ampersandFilter(f, refctx)
if !ok {
@ -484,7 +678,22 @@ func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
}
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
// Instead we keep it around, so that resolveSubstitutions can find it
if !IsVar(ParentMap(f)) {
@ -494,11 +703,15 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
}
if refctx.Key.Primary.Unbox() != nil {
if c.ignoreLazyGlob(f) {
return
}
f.Primary_ = &Scalar{
parent: f,
Value: refctx.Key.Primary.Unbox(),
}
}
if refctx.Key.Value.Array != nil {
a := &Array{
parent: f,
@ -506,12 +719,11 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
c.compileArray(a, refctx.Key.Value.Array, refctx.ScopeAST)
f.Composite = a
} else if refctx.Key.Value.Map != nil {
scopeAST := refctx.Key.Value.Map
if f.Map() == nil {
f.Composite = &Map{
parent: f,
}
}
scopeAST := refctx.Key.Value.Map
switch NodeBoardKind(f) {
case BoardScenario:
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
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.globStack = c.globStack[:len(c.globStack)-1]
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
switch NodeBoardKind(f) {
case BoardScenario, BoardStep:
c.overlayClasses(f.Map())
@ -580,16 +795,31 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
}
}
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
// If the link is a board, we need to transform it into an absolute path.
if f.Name == "link" {
c.compileLink(refctx)
if c.ignoreLazyGlob(f) {
return
}
f.Primary_ = &Scalar{
parent: f,
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) {
for _, f := range m.Fields {
@ -613,8 +843,35 @@ func (c *compiler) updateLinks(m *Map) {
aida := IDA(f)
if len(bida) != len(aida) {
prependIDA := aida[:len(aida)-len(bida)]
kp := d2ast.MakeKeyPath(prependIDA)
s := d2format.Format(kp) + strings.TrimPrefix(f.Primary_.Value.ScalarString(), "root")
fullIDA := []string{"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()
}
}
@ -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()
link, err := d2parser.ParseKey(val)
if err != nil {
@ -683,7 +940,7 @@ func (c *compiler) compileLink(refctx *RefContext) {
// Create the absolute path by appending scope path with value specified
scopeIDA = append(scopeIDA, linkIDA...)
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) {
@ -692,7 +949,7 @@ func (c *compiler) compileEdges(refctx *RefContext) {
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 {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
@ -726,21 +983,25 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
var ea []*Edge
if eid.Index != nil || eid.Glob {
ea = refctx.ScopeMap.GetEdges(eid, refctx)
ea = refctx.ScopeMap.GetEdges(eid, refctx, c)
if len(ea) == 0 {
if !eid.Glob {
c.errorf(refctx.Edge, "indexed edge does not exist")
}
continue
}
for _, e := range ea {
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.Dst, refctx)
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx, c)
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx, c)
}
} else {
var err error
ea, err = refctx.ScopeMap.CreateEdge(eid, refctx)
ea, err = refctx.ScopeMap.CreateEdge(eid, refctx, c)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
@ -757,6 +1018,9 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
} else {
if refctx.Key.Primary.Unbox() != nil {
if c.ignoreLazyGlob(e) {
return
}
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Primary.Unbox(),
@ -771,10 +1035,13 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
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.globStack = c.globStack[:len(c.globStack)-1]
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
if c.ignoreLazyGlob(e) {
return
}
e.Primary_ = &Scalar{
parent: e,
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 {
return nil
t.Fatalf("query didn't match anything")
}
return na[0]
@ -416,10 +416,27 @@ scenarios: {
}
}`)
assert.Success(t, err)
assertQuery(t, m, 8, 2, nil, "")
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)
}

View file

@ -13,6 +13,7 @@ import (
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target"
)
// Most errors returned by a node should be created with d2parser.Errorf
@ -29,6 +30,7 @@ type Node interface {
fmt.Stringer
LastRef() Reference
LastPrimaryRef() Reference
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 *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 *Map) 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.
AST() d2ast.Node
Primary() bool
Context() *RefContext
// Result of a glob in Context or from above.
DueToGlob() bool
DueToLazyGlob() bool
}
var _ Reference = &FieldReference{}
@ -126,6 +136,12 @@ var _ Reference = &EdgeReference{}
func (r *FieldReference) 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 {
parent Node
@ -154,13 +170,15 @@ type Map struct {
parent Node
Fields []*Field `json:"fields"`
Edges []*Edge `json:"edges"`
globs []*globContext
}
func (m *Map) initRoot() {
m.parent = &Field{
Name: "root",
References: []*FieldReference{{
Context: &RefContext{
Context_: &RefContext{
ScopeMap: m,
},
}},
@ -245,7 +263,11 @@ func NodeBoardKind(n Node) BoardKind {
}
f = ParentField(n)
case *Map:
f = ParentField(n)
var ok bool
f, ok = n.parent.(*Field)
if !ok {
return ""
}
if f.Root() {
return BoardLayer
}
@ -295,7 +317,7 @@ func (f *Field) Copy(newParent Node) Node {
return f
}
func (f *Field) lastPrimaryRef() *FieldReference {
func (f *Field) LastPrimaryRef() Reference {
for i := len(f.References) - 1; i >= 0; i-- {
if f.References[i].Primary() {
return f.References[i]
@ -305,11 +327,11 @@ func (f *Field) lastPrimaryRef() *FieldReference {
}
func (f *Field) LastPrimaryKey() *d2ast.Key {
fr := f.lastPrimaryRef()
fr := f.LastPrimaryRef()
if fr == nil {
return nil
}
return fr.Context.Key
return fr.(*FieldReference).Context_.Key
}
func (f *Field) LastRef() Reference {
@ -451,10 +473,10 @@ func (e *Edge) Copy(newParent Node) Node {
return e
}
func (e *Edge) lastPrimaryRef() *EdgeReference {
func (e *Edge) LastPrimaryRef() Reference {
for i := len(e.References) - 1; i >= 0; i-- {
fr := e.References[i]
if fr.Context.Key.EdgeKey == nil {
if fr.Context_.Key.EdgeKey == nil && !fr.DueToLazyGlob() {
return fr
}
}
@ -462,11 +484,11 @@ func (e *Edge) lastPrimaryRef() *EdgeReference {
}
func (e *Edge) LastPrimaryKey() *d2ast.Key {
er := e.lastPrimaryRef()
er := e.LastPrimaryRef()
if er == nil {
return nil
}
return er.Context.Key
return er.(*EdgeReference).Context_.Key
}
func (e *Edge) LastRef() Reference {
@ -494,16 +516,18 @@ type FieldReference struct {
String d2ast.String `json:"string"`
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
// represented by String.
func (fr *FieldReference) Primary() bool {
if fr.KeyPath == fr.Context.Key.Key {
return len(fr.Context.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
} else if fr.KeyPath == fr.Context.Key.EdgeKey {
return len(fr.Context.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
if fr.KeyPath == fr.Context_.Key.Key {
return len(fr.Context_.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
} else if fr.KeyPath == fr.Context_.Key.EdgeKey {
return len(fr.Context_.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
}
return false
}
@ -518,33 +542,35 @@ func (fr *FieldReference) KeyPathIndex() int {
}
func (fr *FieldReference) EdgeDest() bool {
return fr.KeyPath == fr.Context.Edge.Dst
return fr.KeyPath == fr.Context_.Edge.Dst
}
func (fr *FieldReference) InEdge() bool {
return fr.Context.Edge != nil
return fr.Context_.Edge != nil
}
func (fr *FieldReference) AST() d2ast.Node {
if fr.String == nil {
// Root map.
return fr.Context.Scope
return fr.Context_.Scope
}
return fr.String
}
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 {
return er.Context.Edge
return er.Context_.Edge
}
// Primary returns true if the Value in Context.Key.Value corresponds to the *Edge
// represented by Context.Edge
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 {
@ -569,6 +595,12 @@ func (rc *RefContext) EdgeIndex() int {
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 {
if m == nil {
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.
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
for kp.Path[i].Unbox().ScalarString() == "_" {
m = ParentMap(m)
@ -680,26 +712,77 @@ func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool) ([
i++
}
var gctx *globContext
if refctx != nil && refctx.Key.HasGlob() && c != nil {
gctx = c.getGlobContext(refctx)
}
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
}
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)
if ok && us.Pattern != nil {
fa2, ok := m.doubleGlob(us.Pattern)
fa2, ok := m.multiGlob(us.Pattern)
if ok {
if i == len(kp.Path)-1 {
*fa = append(*fa, fa2...)
faAppend(fa2...)
} else {
for _, f := range fa2 {
if !filter(f, true) {
continue
}
if f.Map() == nil {
f.Composite = &Map{
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 {
return err
}
@ -710,14 +793,17 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
for _, f := range m.Fields {
if matchPattern(f.Name, us.Pattern) {
if i == len(kp.Path)-1 {
*fa = append(*fa, f)
faAppend(f)
} else {
if !filter(f, true) {
continue
}
if f.Map() == nil {
f.Composite = &Map{
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 {
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{
String: kp.Path[i].Unbox(),
KeyPath: kp,
Context: refctx,
Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
})
}
if i+1 == len(kp.Path) {
*fa = append(*fa, f)
faAppend(f)
return nil
}
if !filter(f, true) {
return nil
}
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,
}
}
return f.Map().ensureField(i+1, kp, refctx, create, fa)
return f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
}
if !create {
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{
parent: m,
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.
if refctx != nil {
f.References = append(f.References, &FieldReference{
String: kp.Path[i].Unbox(),
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)
if i+1 == len(kp.Path) {
*fa = append(*fa, f)
faAppend(f)
return nil
}
if f.Composite == nil {
f.Composite = &Map{
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 {
@ -833,7 +952,7 @@ func (m *Map) DeleteField(ida ...string) *Field {
for _, fr := range f.References {
for _, e := range m.Edges {
for _, er := range e.References {
if er.Context == fr.Context {
if er.Context_ == fr.Context_ {
m.DeleteEdge(e.ID)
break
}
@ -865,10 +984,14 @@ func (m *Map) DeleteField(ida ...string) *Field {
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 {
var gctx *globContext
if refctx.Key.HasGlob() && c != nil {
gctx = c.ensureGlobContext(refctx)
}
var ea []*Edge
m.getEdges(eid, refctx, &ea)
m.getEdges(eid, refctx, gctx, &ea)
return ea
}
@ -882,7 +1005,7 @@ func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
return nil
}
if f.Map() != nil {
return f.Map().GetEdges(eid, nil)
return f.Map().GetEdges(eid, nil, nil)
}
return nil
}
@ -896,7 +1019,7 @@ func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
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)
if err != nil {
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 {
return nil
}
@ -927,7 +1050,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
parent: f,
}
}
err = f.Map().getEdges(eid, refctx, ea)
err = f.Map().getEdges(eid, refctx, gctx, ea)
if err != nil {
return err
}
@ -935,11 +1058,11 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
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 {
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 {
return err
}
@ -950,19 +1073,46 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
eid2.SrcPath = RelIDA(m, src)
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...)
}
}
}
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
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 {
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 {
return err
}
@ -996,7 +1146,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
parent: f,
}
}
err = f.Map().createEdge(eid, refctx, ea)
err = f.Map().createEdge(eid, refctx, gctx, c, ea)
if err != nil {
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")
}
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx, true)
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx, true, c)
if err != nil {
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 {
return err
}
@ -1038,21 +1188,21 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
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.Map().IsContainer() {
continue
}
if ParentBoard(src) != ParentBoard(dst) {
if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) {
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.Map().IsContainer() {
continue
}
if ParentBoard(src) != ParentBoard(dst) {
if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) {
continue
}
}
@ -1060,17 +1210,20 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
eid2 := eid.Copy()
eid2.SrcPath = RelIDA(m, src)
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 {
return err
}
if e != nil {
*ea = append(*ea, e)
}
}
}
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) != "" {
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.Glob = true
ea := m.GetEdges(eid, nil)
ea := m.GetEdges(eid, nil, nil)
index := len(ea)
eid.Index = &index
eid.Glob = false
@ -1091,9 +1244,29 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Ed
parent: m,
ID: eid,
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)
return e, nil
@ -1148,6 +1321,18 @@ func (e *Edge) AST() d2ast.Node {
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 {
if a == nil {
return nil
@ -1178,7 +1363,7 @@ func (m *Map) AST() d2ast.Node {
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]
f := m.GetField(sb.Unbox().ScalarString())
if f == nil {
@ -1188,13 +1373,15 @@ func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext
f.References = append(f.References, &FieldReference{
String: sb.Unbox(),
KeyPath: kp,
Context: refctx,
Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
})
if i+1 == len(kp.Path) {
return
}
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 {
for i, el := range p {
if el != "_" {
@ -1310,6 +1515,18 @@ func parentRef(n Node) Reference {
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 {
f := ParentField(n)
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.
func BoardIDA(n Node) (ida []string) {
for {
f, ok := n.(*Field)
if ok {
if f.Root() || NodeBoardKind(f) != "" {
switch n := n.(type) {
case *Field:
if n.Root() || NodeBoardKind(n) != "" {
reverseIDA(ida)
return ida
}
ida = append(ida, f.Name)
ida = append(ida, n.Name)
case *Edge:
ida = append(ida, n.IDString())
}
f = ParentField(n)
if f == nil {
n = n.Parent()
if n == nil {
reverseIDA(ida)
return ida
}
n = f
}
}
// IDA returns the absolute path to n.
func IDA(n Node) (ida []string) {
for {
f, ok := n.(*Field)
if ok {
ida = append(ida, f.Name)
if f.Root() {
switch n := n.(type) {
case *Field:
ida = append(ida, n.Name)
if n.Root() {
reverseIDA(ida)
return ida
}
case *Edge:
ida = append(ida, n.IDString())
}
f = ParentField(n)
if f == nil {
n = n.Parent()
if n == nil {
reverseIDA(ida)
return ida
}
n = f
}
}
// RelIDA returns the path to n relative to p.
func RelIDA(p, n Node) (ida []string) {
for {
f, ok := n.(*Field)
if ok {
ida = append(ida, f.Name)
if f.Root() {
switch n := n.(type) {
case *Field:
ida = append(ida, n.Name)
if n.Root() {
reverseIDA(ida)
return ida
}
case *Edge:
ida = append(ida, n.String())
}
f = ParentField(n)
if f == nil || f.Root() || f == p || f.Composite == p {
n = n.Parent()
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)
return ida
}
n = f
}
}
@ -1476,7 +1698,7 @@ func (m *Map) InClass(key *d2ast.Key) bool {
}
for _, ref := range classF.References {
if ref.Context.Key == key {
if ref.Context_.Key == key {
return true
}
}

View file

@ -96,6 +96,120 @@ x -> y
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)

View file

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

View file

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

View file

@ -3,32 +3,68 @@ package d2ir
import (
"strings"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2graph"
)
func (m *Map) doubleGlob(pattern []string) ([]*Field, bool) {
if !(len(pattern) == 3 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*") {
return nil, false
}
func (m *Map) multiGlob(pattern []string) ([]*Field, bool) {
var fa []*Field
if d2ast.IsDoubleGlob(pattern) {
m._doubleGlob(&fa)
return fa, true
}
if d2ast.IsTripleGlob(pattern) {
m._tripleGlob(&fa)
return fa, true
}
return nil, false
}
func (m *Map) _doubleGlob(fa *[]*Field) {
for _, f := range m.Fields {
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
if f.Name == "layers" {
continue
}
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()._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 {
if len(pattern) == 0 {
return true

View file

@ -128,7 +128,7 @@ an* -> an*`)
assert.Success(t, err)
assertQuery(t, m, 2, 2, nil, "")
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, 2, 2, nil, "shared")
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, "(* -> *)[*]")
},
},
{
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",
run: func(t testing.TB) {
@ -314,21 +339,438 @@ task.** -> fast
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) {
_, 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
}
runa(t, tca)
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)
}

View file

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

View file

@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"math"
"regexp"
"sort"
"strings"
@ -140,17 +139,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
return err
}
loadScript := ""
idToObj := make(map[string]*d2graph.Object)
mapper := NewObjectMapper()
for _, obj := range g.Objects {
id := obj.AbsID()
idToObj[id] = obj
width, height := obj.Width, obj.Height
loadScript += generateAddNodeLine(id, int(width), int(height))
mapper.Register(obj)
}
loadScript := ""
for _, obj := range g.Objects {
loadScript += mapper.generateAddNodeLine(obj, int(obj.Width), int(obj.Height))
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 {
@ -209,7 +206,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
log.Debug(ctx, "graph", slog.F("json", dn))
}
obj := idToObj[dn.ID]
obj := mapper.ToObj(dn.ID)
// dagre gives center of node
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
// 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 {

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"`
Height float64 `json:"height"`
Children []*ELKNode `json:"children,omitempty"`
Ports []*ELKPort `json:"ports,omitempty"`
Labels []*ELKLabel `json:"labels,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 {
Text string `json:"text"`
X float64 `json:"x"`
@ -105,7 +133,7 @@ type elkOpts struct {
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
Thoroughness int `json:"elk.layered.thoroughness,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"`
InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
@ -118,6 +146,9 @@ type elkOpts struct {
ContentAlignment string `json:"elk.contentAlignment,omitempty"`
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
PortSide PortSide `json:"elk.port.side,omitempty"`
PortConstraints string `json:"elk.portConstraints,omitempty"`
ConfigurableOpts
}
@ -171,15 +202,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
switch g.Root.Direction.Value {
case "down":
elkGraph.LayoutOptions.Direction = "DOWN"
elkGraph.LayoutOptions.Direction = Down
case "up":
elkGraph.LayoutOptions.Direction = "UP"
elkGraph.LayoutOptions.Direction = Up
case "right":
elkGraph.LayoutOptions.Direction = "RIGHT"
elkGraph.LayoutOptions.Direction = Right
case "left":
elkGraph.LayoutOptions.Direction = "LEFT"
elkGraph.LayoutOptions.Direction = Left
default:
elkGraph.LayoutOptions.Direction = "DOWN"
elkGraph.LayoutOptions.Direction = Down
}
// 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 {
switch g.Root.Direction.Value {
case "right", "left":
if obj.Attributes.HeightAttr == nil {
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
}
default:
if obj.Attributes.WidthAttr == nil {
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
}
}
}
width, height := adjustDimensions(obj)
@ -253,9 +288,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
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)))
case "RIGHT", "LEFT":
case Right, Left:
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
}
} else {
@ -283,6 +318,33 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
} else {
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
})
@ -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{
ID: edge.AbsID(),
Sources: []string{edge.Src.AbsID()},
Targets: []string{edge.Dst.AbsID()},
Sources: []string{src},
Targets: []string{dst},
}
if edge.Label.Value != "" {
e.Labels = append(e.Labels, &ELKLabel{
@ -341,6 +456,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
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)
if err != nil {
return err
@ -503,6 +626,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
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
// see https://github.com/terrastruct/d2/issues/1030
func deleteBends(g *d2graph.Graph) {
@ -521,30 +652,42 @@ func deleteBends(g *d2graph.Graph) {
var corner *geo.Point
var end *geo.Point
var columnIndex *int
if isSource {
start = e.Route[0]
corner = e.Route[1]
end = e.Route[2]
endpoint = e.Src
columnIndex = e.SrcTableColumnIndex
} else {
start = e.Route[len(e.Route)-1]
corner = e.Route[len(e.Route)-2]
end = e.Route[len(e.Route)-3]
endpoint = e.Dst
columnIndex = e.DstTableColumnIndex
}
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
dx, dy := endpoint.GetModifierElementAdjustments()
// 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 {
continue
}
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
continue
}
} else {
default:
if end.X <= endpoint.TopLeft.X+10 {
continue
}
@ -606,12 +749,21 @@ func deleteBends(g *d2graph.Graph) {
}
}
}
// Get rid of ladders
// ELK likes to do these for some reason
// . ┌─
// . ┌─┘
// . │
// 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 {
if len(e.Route) < 6 {
continue
@ -627,6 +779,11 @@ func deleteBends(g *d2graph.Graph) {
end := e.Route[i+2]
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
// These concern two segments

View file

@ -2,7 +2,6 @@ package d2grid
import (
"strconv"
"strings"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
@ -11,6 +10,7 @@ import (
type gridDiagram struct {
root *d2graph.Object
objects []*d2graph.Object
edges []*d2graph.Edge
rows int
columns int
@ -107,19 +107,7 @@ func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.objects {
obj.MoveWithDescendants(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)
for _, e := range gd.edges {
e.Move(dx, dy)
}
}

View file

@ -5,7 +5,6 @@ import (
"context"
"fmt"
"math"
"sort"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
@ -21,77 +20,15 @@ const (
// Layout runs the grid layout on containers with rows/columns
// Note: children are not allowed edges or descendants
//
// 1. Traverse graph from root, skip objects with no rows/columns
// 2. Construct a grid with the container children
// 3. Remove the children from the main graph
// 4. Run grid layout
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (without grid children)
// 7. Put grid children back in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
return func(ctx context.Context, g *d2graph.Graph) error {
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g, 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{}{}
}
}
}
// 1. Run grid layout on the graph root
// 2. Set the resulting dimensions to the graph root
func Layout(ctx context.Context, g *d2graph.Graph) error {
obj := g.Root
gd, err := layoutGrid(g, obj)
if err != nil {
return err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
if obj.Box != nil {
// 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 the label doesn't fit within the padding, we need to add more
dy = occupiedHeight - float64(verticalPadding)
@ -195,70 +95,92 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
if obj.Icon != nil {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
}
gridDiagrams[obj.AbsID()] = gd
for _, o := range gd.objects {
toRemove[o] = struct{}{}
// simple straight line edge routing between grid objects
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
}
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) {
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 {
gd.layoutEvenly(g, obj)
} else {
gd.layoutDynamic(g, obj)
}
// position labels and icons
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))
}
}
}
revertAdjustments()
return gd, nil
}
@ -486,7 +408,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
cursor.X = 0
cursor.Y += rowHeight + verticalGap
}
maxY = cursor.Y - horizontalGap
maxY = cursor.Y - verticalGap
} else {
// measure column heights
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 rowSize > okThreshold*targetSize {
skipCount++
if skipCount >= SKIP_LIMIT {
// there may even be too many to skip
return true
}
return false
return skipCount >= SKIP_LIMIT
}
}
// row is too small to be good overall
if rowSize < targetSize/okThreshold {
skipCount++
if skipCount >= SKIP_LIMIT {
return true
}
return false
return skipCount >= SKIP_LIMIT
}
return true
}
@ -936,53 +852,110 @@ func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalG
return totalDelta
}
// cleanup restores the graph after the core layout engine finishes
// - translating the grid to its position placed by the core layout engine
// - restore the children of the grid
// - sorts objects to their original graph order
func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objectsOrder map[string]int) {
defer func() {
sort.SliceStable(graph.Objects, func(i, j int) bool {
return objectsOrder[graph.Objects[i].AbsID()] < objectsOrder[graph.Objects[j].AbsID()]
})
}()
func (gd *gridDiagram) sizeForOutsideLabels() (revert func()) {
margins := make(map[*d2graph.Object]geo.Spacing)
var restore func(obj *d2graph.Object)
restore = func(obj *d2graph.Object) {
gd, exists := gridDiagrams[obj.AbsID()]
if !exists {
return
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
for _, o := range gd.objects {
margin := o.GetMargin()
margins[o] = margin
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
o.Height += margin.Top + margin.Bottom
o.Width += margin.Left + margin.Right
}
// shift the grid from (0, 0)
gd.shift(
obj.TopLeft.X+float64(horizontalPadding),
obj.TopLeft.Y+float64(verticalPadding),
)
gd.cleanup(obj, graph)
// Example: a single column with 3 shapes and
// `x.label: long label {near: outside-bottom-left}`
// `y.label: outsider {near: outside-right-center}`
// . ┌───────────────────┐
// . │ widest shape here │
// . └───────────────────┘
// . ┌───┐
// . │ 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 {
restore(child)
// BEFORE LAYOUT
// . ┌───────────────────┐
// . │ 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() {
gd, exists := gridDiagrams[graph.Root.AbsID()]
if exists {
gd.cleanup(graph.Root, graph)
if margin.Left > 0 || margin.Top > 0 {
o.MoveWithDescendants(margin.Left, margin.Top)
}
}
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
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.
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
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.
// Top left should go left enough to not collide with center.
// So place the center ones first, then the later ones will consider them for bounding box
for _, processCenters := range []bool{true, false} {
for _, currentSet := range []set{VerticalCenterNears, HorizontalCenterNears, NonCenterNears} {
for _, tempGraph := range constantNearGraphs {
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
obj.TopLeft = geo.NewPoint(place(obj))
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 {
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
g.Objects = append(g.Objects, tempGraph.Objects...)
if obj.Parent.Children == nil {
@ -83,28 +102,20 @@ func place(obj *d2graph.Object) (float64, float64) {
switch nearKeyStr {
case "top-left":
x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
break
case "top-center":
x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
break
case "top-right":
x, y = br.X+pad, tl.Y-obj.Height-pad
break
case "center-left":
x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
break
case "center-right":
x, y = br.X+pad, tl.Y+h/2-obj.Height/2
break
case "bottom-left":
x, y = tl.X-obj.Width-pad, br.Y+pad
break
case "bottom-center":
x, y = br.X-w/2-obj.Width/2, br.Y+pad
break
case "bottom-right":
x, y = br.X+pad, br.Y+pad
break
}
if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
@ -134,28 +145,6 @@ func place(obj *d2graph.Object) (float64, float64) {
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
// 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

View file

@ -2,7 +2,6 @@ package d2sequence
import (
"context"
"sort"
"strings"
"oss.terrastruct.com/util-go/go2"
@ -15,149 +14,24 @@ import (
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
//
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
// 2. Construct a sequence diagram from all descendant objects and edges
// 3. Remove those objects and edges from the main graph
// 4. Run layout on sequence diagrams
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (still without sequence diagram innards)
// 7. Put back sequence diagram innards in correct location
// 1. Run layout on sequence diagrams
// 2. Set the resulting dimensions to the main graph shape
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 {
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
// shape: sequence_diagram
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
return nil
}
obj := g.Root
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))
// 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.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...)
return nil
}
// no new objects, so just keep the same position
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) {
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)
}
}
// sequence diagrams add lifelines, and they must be the last ones in this slice
sort.SliceStable(g.Edges, func(i, j int) bool {
iOrder, iExists := edgesOrder[g.Edges[i].AbsID()]
jOrder, jExists := edgesOrder[g.Edges[j].AbsID()]
if iExists && jExists {
return iOrder < jOrder
} else if iExists && !jExists {
return true
sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
if err != nil {
return nil, err
}
// either both don't exist or i doesn't exist and j exists
return false
})
err = sd.layout()
return sd, err
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"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() {
rankToX := make(map[int]float64)
for _, actor := range sd.actors {

View file

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

View file

@ -65,7 +65,7 @@ func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
if err != nil {
return nil, "", err
}
g, err = recompile(g.AST)
g, err = recompile(g)
if err != nil {
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) {
@ -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) {
@ -303,13 +303,15 @@ func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Objec
return pathFromScopeKey(g, key, scopeak)
}
func recompile(ast *d2ast.Map) (*d2graph.Graph, error) {
s := d2format.Format(ast)
g, _, err := d2compiler.Compile(ast.Range.Path, strings.NewReader(s), nil)
func recompile(g *d2graph.Graph) (*d2graph.Graph, error) {
s := d2format.Format(g.AST)
g2, _, err := d2compiler.Compile(g.AST.Range.Path, strings.NewReader(s), &d2compiler.CompileOptions{
FS: g.FS,
})
if err != nil {
return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err)
}
return g, nil
return g2, nil
}
// TODO merge flat styles
@ -451,7 +453,9 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
return nil
}
}
ir, err := d2ir.Compile(g.AST, nil)
ir, err := d2ir.Compile(g.AST, &d2ir.CompileOptions{
FS: g.FS,
})
if err != nil {
return err
}
@ -489,7 +493,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
noVal2 := &tmp2
noVal1.Value = d2ast.ValueBox{}
noVal2.Value = d2ast.ValueBox{}
if noVal1.Equals(noVal2) {
if noVal1.D2OracleEquals(noVal2) {
ref.MapKey.Value = mk.Value
return nil
}
@ -555,6 +559,12 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
if reserved {
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)
}
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) {
for _, n := range m.Nodes {
if n.MapKey != nil && n.MapKey.Equals(mk) {
if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
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) {
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 {
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 {
replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
if !replaced {
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)
if err != nil {
@ -931,7 +937,7 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
return nil, err
}
} else {
prependMapKey(baseAST, mk)
appendMapKey(baseAST, mk)
}
if len(boardPath) > 0 {
@ -939,10 +945,10 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
if !replaced {
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) {
@ -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 {
return nil, err
}
return recompile(g.AST)
return recompile(g)
}
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) {
@ -1285,7 +1291,7 @@ func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error {
copy(tmpNodes, ref.Scope.Nodes)
// If I delete this, will the object still exist?
deleteFromMap(ref.Scope, ref.MapKey)
g2, err := recompile(g.AST)
g2, err := recompile(g)
if err != nil {
return err
}
@ -1465,7 +1471,7 @@ func ensureNode(g *d2graph.Graph, excludedEdges []*d2ast.Edge, scopeObj *d2graph
}
for _, n := range scope.Nodes {
if n.MapKey != nil && n.MapKey.Equals(mk) {
if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
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].DstArrow = mk2.Edges[0].DstArrow
}
return recompile(g.AST)
return recompile(g)
}
prevG, _ := recompile(boardG.AST)
prevG, _ := recompile(boardG)
ak := d2graph.Key(mk.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:]
exists := false
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
}
}
@ -2026,10 +2032,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
if !replaced {
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
@ -2141,7 +2147,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
if err != nil {
return err
}
tmpG, _ := recompile(prevG.AST)
tmpG, _ := recompile(prevG)
appendMapKey(tmpG.AST, valueMK)
if to == nil {
deltas, err := DeleteIDDeltas(tmpG, nil, *from)
@ -2186,7 +2192,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
if err != nil {
return err
}
tmpG, _ := recompile(prevG.AST)
tmpG, _ := recompile(prevG)
appendMapKey(tmpG.AST, valueMK)
if to == nil {
deltas, err := DeleteIDDeltas(tmpG, nil, *from)

View file

@ -2,6 +2,9 @@ package d2oracle_test
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
@ -732,6 +735,7 @@ func TestSet(t *testing.T) {
boardPath []string
name string
text string
fsTexts map[string]string
key string
tag *string
value *string
@ -1984,6 +1988,76 @@ scenarios: {
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{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
return d2oracle.Set(g, tc.boardPath, tc.key, tc.tag, tc.value)
},
@ -2412,6 +2487,7 @@ func TestRename(t *testing.T) {
boardPath []string
text string
fsTexts map[string]string
key string
newName string
@ -2923,6 +2999,7 @@ scenarios: {
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
objectsBefore := len(g.Objects)
var err error
@ -2956,6 +3033,7 @@ func TestMove(t *testing.T) {
boardPath []string
text string
fsTexts map[string]string
key string
newKey string
includeDescendants bool
@ -5191,6 +5269,7 @@ scenarios: {
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
objectsBefore := len(g.Objects)
var err error
@ -5222,6 +5301,7 @@ func TestDelete(t *testing.T) {
boardPath []string
text string
fsTexts map[string]string
key string
expErr string
@ -6934,10 +7014,9 @@ scenarios: {
scenarios: {
x: {
a: null
b
c
a: null
}
}
`,
@ -6961,10 +7040,9 @@ scenarios: {
scenarios: {
x: {
(a -> b)[0]: null
b
c
(a -> b)[0]: null
}
}
`,
@ -6978,6 +7056,7 @@ scenarios: {
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
return d2oracle.Delete(g, tc.boardPath, tc.key)
},
@ -6993,6 +7072,7 @@ scenarios: {
type editTest struct {
text string
fsTexts map[string]string
testFunc func(*d2graph.Graph) (*d2graph.Graph, error)
exp string
@ -7002,7 +7082,13 @@ type editTest struct {
func (tc editTest) run(t *testing.T) {
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 {
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 {
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 {
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 {
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

View file

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

View file

@ -380,6 +380,18 @@ b-
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",
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.
// 2. If not found, it then searches each directory in $PATH for a binary with the 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
// to get a plugin implementation around the binary and returns it.
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {

View file

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

View file

@ -14,8 +14,9 @@ func TestCutFont(t *testing.T) {
Family: SourceCodePro,
Style: FONT_STYLE_BOLD,
}
fontBuf := make([]byte, len(FontFaces[f]))
copy(fontBuf, FontFaces[f])
face := FontFaces.Get(f)
fontBuf := make([]byte, len(face))
copy(fontBuf, face)
fontBuf = font.UTF8CutFont(fontBuf, " 1")
err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf)
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();
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
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(),
});

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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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;
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"`) {
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;
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>")

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 {
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 {
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 {
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 {
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 {
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 {
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.Y = baselineCenter.Y
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.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
textEl.Content = RenderText(text, textEl.X, height)
@ -1258,7 +1267,13 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
if !isLight {
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.Width = float64(targetShape.Width)
rectEl.Height = float64(targetShape.Height)
@ -1312,9 +1327,20 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
mdEl := d2themes.NewThemableElement("div")
mdEl.ClassName = "md"
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 {
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, `</foreignObject></g>`)
} 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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