Merge branch 'terrastruct:master' into master
9
.github/workflows/ci.yml
vendored
|
|
@ -31,3 +31,12 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
signed:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: git submodule update --init
|
||||||
|
- run: COLOR=1 ./ci/sub/bin/ensure_signed.sh
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ let us know and we'll be happy to include it here!
|
||||||
- **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter)
|
- **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter)
|
||||||
- **Logseq-D2**: [https://github.com/b-yp/logseq-d2](https://github.com/b-yp/logseq-d2)
|
- **Logseq-D2**: [https://github.com/b-yp/logseq-d2](https://github.com/b-yp/logseq-d2)
|
||||||
- **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2)
|
- **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2)
|
||||||
|
- **MkDocs Plugin**: [https://github.com/landmaj/mkdocs-d2-plugin](https://github.com/landmaj/mkdocs-d2-plugin)
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ cd -- "$(dirname "$0")/../.."
|
||||||
. ./ci/sub/lib.sh
|
. ./ci/sub/lib.sh
|
||||||
|
|
||||||
tag="$(sh_c docker build \
|
tag="$(sh_c docker build \
|
||||||
--build-arg GOVERSION="1.19.3.linux-$ARCH" \
|
--build-arg GOVERSION="1.20.8.linux-$ARCH" \
|
||||||
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
|
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
|
||||||
docker_run \
|
docker_run \
|
||||||
-e DRY_RUN \
|
-e DRY_RUN \
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
#### Features 🚀
|
#### Features 🚀
|
||||||
|
|
||||||
- UTF-16 files are automatically detected and supported [#1525](https://github.com/terrastruct/d2/pull/1525)
|
- ELK now routes `sql_table` edges to the exact columns (ty @landmaj) [#1681](https://github.com/terrastruct/d2/pull/1681)
|
||||||
|
|
||||||
#### Improvements 🧹
|
#### Improvements 🧹
|
||||||
|
|
||||||
|
- Grid cells can now contain nested edges [#1629](https://github.com/terrastruct/d2/pull/1629)
|
||||||
|
- Edges can now go across constant nears, sequence diagrams, and grids including nested ones. [#1631](https://github.com/terrastruct/d2/pull/1631)
|
||||||
|
- All vars defined in a scope are accessible everywhere in that scope, i.e., an object can use a var defined after itself. [#1695](https://github.com/terrastruct/d2/pull/1695)
|
||||||
|
|
||||||
#### Bugfixes ⛑️
|
#### Bugfixes ⛑️
|
||||||
|
|
||||||
- Fixes `d2 fmt` to format all files passed as arguments rather than first non-formatted only [#1523](https://github.com/terrastruct/d2/issues/1523)
|
- Fixes a bug calculating grid height with only grid-rows and different horizontal-gap and vertical-gap values. [#1646](https://github.com/terrastruct/d2/pull/1646)
|
||||||
|
- Grid layout now accounts for each cell's outside labels and icons [#1624](https://github.com/terrastruct/d2/pull/1624)
|
||||||
|
- Grid layout now accounts for labels wider or taller than the shape and fixes default label positions for image grid cells. [#1670](https://github.com/terrastruct/d2/pull/1670)
|
||||||
|
- Fixes a panic with a spread substitution in a glob map [#1643](https://github.com/terrastruct/d2/pull/1643)
|
||||||
|
- Fixes use of `null` in `sql_table` constraints (ty @landmaj) [#1660](https://github.com/terrastruct/d2/pull/1660)
|
||||||
|
- Fixes elk growing shapes with width/height set [#1679](https://github.com/terrastruct/d2/pull/1679)
|
||||||
|
- Adds a compiler error when accidentally using an arrowhead on a shape [#1686](https://github.com/terrastruct/d2/pull/1686)
|
||||||
|
- Correctly reports errors from invalid values set by globs. [#1691](https://github.com/terrastruct/d2/pull/1691)
|
||||||
|
- Fixes panic when spread substitution referenced a nonexistant var. [#1695](https://github.com/terrastruct/d2/pull/1695)
|
||||||
|
|
|
||||||
52
ci/release/changelogs/v0.6.1.md
Normal 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).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
#### 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
|
||||||
260
d2ast/d2ast.go
|
|
@ -606,6 +606,15 @@ func (m *Map) IsFileMap() bool {
|
||||||
return m.Range.Start.Line == 0 && m.Range.Start.Column == 0
|
return m.Range.Start.Line == 0 && m.Range.Start.Column == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Map) HasFilter() bool {
|
||||||
|
for _, n := range m.Nodes {
|
||||||
|
if n.MapKey != nil && (n.MapKey.Ampersand || n.MapKey.NotAmpersand) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: require @ on import values for readability
|
// TODO: require @ on import values for readability
|
||||||
type Key struct {
|
type Key struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
|
|
@ -613,6 +622,9 @@ type Key struct {
|
||||||
// Indicates this MapKey is a filter selector.
|
// Indicates this MapKey is a filter selector.
|
||||||
Ampersand bool `json:"ampersand,omitempty"`
|
Ampersand bool `json:"ampersand,omitempty"`
|
||||||
|
|
||||||
|
// Indicates this MapKey is a not filter selector.
|
||||||
|
NotAmpersand bool `json:"not_ampersand,omitempty"`
|
||||||
|
|
||||||
// At least one of Key and Edges will be set but all four can also be set.
|
// At least one of Key and Edges will be set but all four can also be set.
|
||||||
// The following are all valid MapKeys:
|
// The following are all valid MapKeys:
|
||||||
// Key:
|
// Key:
|
||||||
|
|
@ -638,8 +650,13 @@ type Key struct {
|
||||||
Value ValueBox `json:"value"`
|
Value ValueBox `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO maybe need to compare Primary
|
func (mk1 *Key) D2OracleEquals(mk2 *Key) bool {
|
||||||
func (mk1 *Key) Equals(mk2 *Key) bool {
|
if mk1 == nil && mk2 == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mk1 == nil) || (mk2 == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if mk1.Ampersand != mk2.Ampersand {
|
if mk1.Ampersand != mk2.Ampersand {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -712,6 +729,104 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mk1 *Key) Equals(mk2 *Key) bool {
|
||||||
|
if mk1 == nil && mk2 == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mk1 == nil) || (mk2 == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mk1.Ampersand != mk2.Ampersand {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (mk1.Key == nil) != (mk2.Key == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (mk1.EdgeIndex == nil) != (mk2.EdgeIndex == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mk1.EdgeIndex != nil {
|
||||||
|
if !mk1.EdgeIndex.Equals(mk2.EdgeIndex) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mk1.EdgeKey == nil) != (mk2.EdgeKey == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(mk1.Edges) != len(mk2.Edges) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range mk1.Edges {
|
||||||
|
if !mk1.Edges[i].Equals(mk2.Edges[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mk1.Value.Map == nil) != (mk2.Value.Map == nil) {
|
||||||
|
if mk1.Value.Map != nil && len(mk1.Value.Map.Nodes) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mk2.Value.Map != nil && len(mk2.Value.Map.Nodes) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (mk1.Value.Unbox() == nil) != (mk2.Value.Unbox() == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mk1.Key != nil {
|
||||||
|
if len(mk1.Key.Path) != len(mk2.Key.Path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, id := range mk1.Key.Path {
|
||||||
|
if id.Unbox().ScalarString() != mk2.Key.Path[i].Unbox().ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mk1.EdgeKey != nil {
|
||||||
|
if len(mk1.EdgeKey.Path) != len(mk2.EdgeKey.Path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, id := range mk1.EdgeKey.Path {
|
||||||
|
if id.Unbox().ScalarString() != mk2.EdgeKey.Path[i].Unbox().ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mk1.Value.Map != nil && len(mk1.Value.Map.Nodes) > 0 {
|
||||||
|
if len(mk1.Value.Map.Nodes) != len(mk2.Value.Map.Nodes) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range mk1.Value.Map.Nodes {
|
||||||
|
if !mk1.Value.Map.Nodes[i].MapKey.Equals(mk2.Value.Map.Nodes[i].MapKey) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mk1.Value.Unbox() != nil {
|
||||||
|
if (mk1.Value.ScalarBox().Unbox() == nil) != (mk2.Value.ScalarBox().Unbox() == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mk1.Value.ScalarBox().Unbox() != nil {
|
||||||
|
if mk1.Value.ScalarBox().Unbox().ScalarString() != mk2.Value.ScalarBox().Unbox().ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mk1.Primary.Unbox() != nil {
|
||||||
|
if (mk1.Primary.Unbox() == nil) != (mk2.Primary.Unbox() == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mk1.Primary.ScalarString() != mk2.Primary.ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (mk *Key) SetScalar(scalar ScalarBox) {
|
func (mk *Key) SetScalar(scalar ScalarBox) {
|
||||||
if mk.Value.Unbox() != nil && mk.Value.ScalarBox().Unbox() == nil {
|
if mk.Value.Unbox() != nil && mk.Value.ScalarBox().Unbox() == nil {
|
||||||
mk.Primary = scalar
|
mk.Primary = scalar
|
||||||
|
|
@ -720,7 +835,43 @@ func (mk *Key) SetScalar(scalar ScalarBox) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mk *Key) HasQueryGlob() bool {
|
func (mk *Key) HasGlob() bool {
|
||||||
|
if mk.Key.HasGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, e := range mk.Edges {
|
||||||
|
if e.Src.HasGlob() || e.Dst.HasGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mk.EdgeIndex != nil && mk.EdgeIndex.Glob {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if mk.EdgeKey.HasGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mk *Key) HasTripleGlob() bool {
|
||||||
|
if mk.Key.HasTripleGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, e := range mk.Edges {
|
||||||
|
if e.Src.HasTripleGlob() || e.Dst.HasTripleGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mk.EdgeIndex != nil && mk.EdgeIndex.Glob {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if mk.EdgeKey.HasTripleGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mk *Key) SupportsGlobFilters() bool {
|
||||||
if mk.Key.HasGlob() && len(mk.Edges) == 0 {
|
if mk.Key.HasGlob() && len(mk.Edges) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -733,6 +884,11 @@ func (mk *Key) HasQueryGlob() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mk *Key) Copy() *Key {
|
||||||
|
mk2 := *mk
|
||||||
|
return &mk2
|
||||||
|
}
|
||||||
|
|
||||||
type KeyPath struct {
|
type KeyPath struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
Path []*StringBox `json:"path"`
|
Path []*StringBox `json:"path"`
|
||||||
|
|
@ -760,16 +916,16 @@ func (kp *KeyPath) Copy() *KeyPath {
|
||||||
return &kp2
|
return &kp2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kp *KeyPath) HasDoubleGlob() bool {
|
func (kp *KeyPath) Last() *StringBox {
|
||||||
if kp == nil {
|
return kp.Path[len(kp.Path)-1]
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
for _, el := range kp.Path {
|
func IsDoubleGlob(pattern []string) bool {
|
||||||
if el.UnquotedString != nil && el.ScalarString() == "**" {
|
return len(pattern) == 3 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*"
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
}
|
func IsTripleGlob(pattern []string) bool {
|
||||||
return false
|
return len(pattern) == 5 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*" && pattern[3] == "" && pattern[4] == "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kp *KeyPath) HasGlob() bool {
|
func (kp *KeyPath) HasGlob() bool {
|
||||||
|
|
@ -784,6 +940,54 @@ func (kp *KeyPath) HasGlob() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (kp *KeyPath) FirstGlob() int {
|
||||||
|
if kp == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for i, el := range kp.Path {
|
||||||
|
if el.UnquotedString != nil && len(el.UnquotedString.Pattern) > 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kp *KeyPath) HasTripleGlob() bool {
|
||||||
|
if kp == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, el := range kp.Path {
|
||||||
|
if el.UnquotedString != nil && IsTripleGlob(el.UnquotedString.Pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kp *KeyPath) HasMultiGlob() bool {
|
||||||
|
if kp == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, el := range kp.Path {
|
||||||
|
if el.UnquotedString != nil && (IsDoubleGlob(el.UnquotedString.Pattern) || IsTripleGlob(el.UnquotedString.Pattern)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kp1 *KeyPath) Equals(kp2 *KeyPath) bool {
|
||||||
|
if len(kp1.Path) != len(kp2.Path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, id := range kp1.Path {
|
||||||
|
if id.Unbox().ScalarString() != kp2.Path[i].Unbox().ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type Edge struct {
|
type Edge struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
|
|
||||||
|
|
@ -796,6 +1000,22 @@ type Edge struct {
|
||||||
DstArrow string `json:"dst_arrow"`
|
DstArrow string `json:"dst_arrow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e1 *Edge) Equals(e2 *Edge) bool {
|
||||||
|
if !e1.Src.Equals(e2.Src) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e1.SrcArrow != e2.SrcArrow {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !e1.Dst.Equals(e2.Dst) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e1.DstArrow != e2.DstArrow {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type EdgeIndex struct {
|
type EdgeIndex struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
|
|
||||||
|
|
@ -804,6 +1024,16 @@ type EdgeIndex struct {
|
||||||
Glob bool `json:"glob"`
|
Glob bool `json:"glob"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ei1 *EdgeIndex) Equals(ei2 *EdgeIndex) bool {
|
||||||
|
if ei1.Int != ei2.Int {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ei1.Glob != ei2.Glob {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type Substitution struct {
|
type Substitution struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
|
|
||||||
|
|
@ -1079,6 +1309,10 @@ func (sb ScalarBox) Unbox() Scalar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sb ScalarBox) ScalarString() string {
|
||||||
|
return sb.Unbox().ScalarString()
|
||||||
|
}
|
||||||
|
|
||||||
// StringBox is used to box String for JSON persistence.
|
// StringBox is used to box String for JSON persistence.
|
||||||
type StringBox struct {
|
type StringBox struct {
|
||||||
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/lib/pdf"
|
"oss.terrastruct.com/d2/lib/pdf"
|
||||||
"oss.terrastruct.com/d2/lib/png"
|
"oss.terrastruct.com/d2/lib/png"
|
||||||
"oss.terrastruct.com/d2/lib/pptx"
|
"oss.terrastruct.com/d2/lib/pptx"
|
||||||
|
"oss.terrastruct.com/d2/lib/simplelog"
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||||
timelib "oss.terrastruct.com/d2/lib/time"
|
timelib "oss.terrastruct.com/d2/lib/time"
|
||||||
"oss.terrastruct.com/d2/lib/version"
|
"oss.terrastruct.com/d2/lib/version"
|
||||||
|
|
@ -334,9 +335,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
_, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, "", *bundleFlag, *forceAppendixFlag, pw.Page)
|
_, written, err := compile(ctx, ms, plugins, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, "", *bundleFlag, *forceAppendixFlag, pw.Page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if written {
|
if written {
|
||||||
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
|
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to compile: %w", err)
|
return fmt.Errorf("failed to compile %s: %w", ms.HumanPath(inputPath), err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +435,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, la
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
out, err := xgif.AnimatePNGs(ms, pngs, int(animateInterval))
|
out, err := AnimatePNGs(ms, pngs, int(animateInterval))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
@ -748,10 +749,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||||
|
l := simplelog.FromCmdLog(ms.Log)
|
||||||
|
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||||
if bundle {
|
if bundle {
|
||||||
var bundleErr2 error
|
var bundleErr2 error
|
||||||
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
|
svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||||
}
|
}
|
||||||
if forceAppendix && !toPNG {
|
if forceAppendix && !toPNG {
|
||||||
|
|
@ -764,11 +767,11 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
||||||
|
|
||||||
if !bundle {
|
if !bundle {
|
||||||
var bundleErr2 error
|
var bundleErr2 error
|
||||||
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
|
svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err = png.ConvertSVG(ms, page, svg)
|
out, err = ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
|
|
@ -833,15 +836,17 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
|
l := simplelog.FromCmdLog(ms.Log)
|
||||||
|
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||||
|
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||||
if bundleErr != nil {
|
if bundleErr != nil {
|
||||||
return svg, bundleErr
|
return svg, bundleErr
|
||||||
}
|
}
|
||||||
svg = appendix.Append(diagram, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := png.ConvertSVG(ms, page, svg)
|
pngImg, err := ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
|
|
@ -933,8 +938,10 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
|
l := simplelog.FromCmdLog(ms.Log)
|
||||||
|
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||||
|
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||||
if bundleErr != nil {
|
if bundleErr != nil {
|
||||||
return nil, bundleErr
|
return nil, bundleErr
|
||||||
|
|
@ -942,7 +949,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
||||||
|
|
||||||
svg = appendix.Append(diagram, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := png.ConvertSVG(ms, page, svg)
|
pngImg, err := ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1178,8 +1185,10 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
|
l := simplelog.FromCmdLog(ms.Log)
|
||||||
|
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||||
|
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||||
if bundleErr != nil {
|
if bundleErr != nil {
|
||||||
return nil, nil, bundleErr
|
return nil, nil, bundleErr
|
||||||
|
|
@ -1187,7 +1196,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
||||||
|
|
||||||
svg = appendix.Append(diagram, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := png.ConvertSVG(ms, page, svg)
|
pngImg, err := ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1218,3 +1227,21 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
||||||
|
|
||||||
return svg, pngs, nil
|
return svg, pngs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
|
||||||
|
cancel := background.Repeat(func() {
|
||||||
|
ms.Log.Info.Printf("converting to PNG...")
|
||||||
|
}, time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return png.ConvertSVG(page, svg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnimatePNGs(ms *xmain.State, pngs [][]byte, animIntervalMs int) ([]byte, error) {
|
||||||
|
cancel := background.Repeat(func() {
|
||||||
|
ms.Log.Info.Printf("generating GIF...")
|
||||||
|
}, time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return xgif.AnimatePNGs(pngs, animIntervalMs)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ func Compile(p string, r io.Reader, opts *CompileOptions) (*d2graph.Graph, *d2ta
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
g.FS = opts.FS
|
||||||
g.SortObjectsByAST()
|
g.SortObjectsByAST()
|
||||||
g.SortEdgesByAST()
|
g.SortEdgesByAST()
|
||||||
return g, compileConfig(ir), nil
|
return g, compileConfig(ir), nil
|
||||||
|
|
@ -81,6 +82,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
||||||
if len(c.err.Errors) == 0 {
|
if len(c.err.Errors) == 0 {
|
||||||
c.validateKeys(g.Root, ir)
|
c.validateKeys(g.Root, ir)
|
||||||
}
|
}
|
||||||
|
c.validateLabels(g)
|
||||||
c.validateNear(g)
|
c.validateNear(g)
|
||||||
c.validateEdges(g)
|
c.validateEdges(g)
|
||||||
|
|
||||||
|
|
@ -175,7 +177,14 @@ type compiler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
|
func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
|
||||||
c.err.Errors = append(c.err.Errors, d2parser.Errorf(n, f, v...).(d2ast.Error))
|
err := d2parser.Errorf(n, f, v...).(d2ast.Error)
|
||||||
|
if c.err.ErrorsLookup == nil {
|
||||||
|
c.err.ErrorsLookup = make(map[d2ast.Error]struct{})
|
||||||
|
}
|
||||||
|
if _, ok := c.err.ErrorsLookup[err]; !ok {
|
||||||
|
c.err.Errors = append(c.err.Errors, err)
|
||||||
|
c.err.ErrorsLookup[err] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
||||||
|
|
@ -282,6 +291,10 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
||||||
return
|
return
|
||||||
} else if f.Name == "vars" {
|
} else if f.Name == "vars" {
|
||||||
return
|
return
|
||||||
|
} else if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
|
||||||
|
c.errorf(f.LastRef().AST(), `%#v can only be used on connections`, f.Name)
|
||||||
|
return
|
||||||
|
|
||||||
} else if isReserved {
|
} else if isReserved {
|
||||||
c.compileReserved(&obj.Attributes, f)
|
c.compileReserved(&obj.Attributes, f)
|
||||||
return
|
return
|
||||||
|
|
@ -321,21 +334,21 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
||||||
}
|
}
|
||||||
for _, fr := range f.References {
|
for _, fr := range f.References {
|
||||||
if fr.Primary() {
|
if fr.Primary() {
|
||||||
if fr.Context.Key.Value.Map != nil {
|
if fr.Context_.Key.Value.Map != nil {
|
||||||
obj.Map = fr.Context.Key.Value.Map
|
obj.Map = fr.Context_.Key.Value.Map
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r := d2graph.Reference{
|
r := d2graph.Reference{
|
||||||
Key: fr.KeyPath,
|
Key: fr.KeyPath,
|
||||||
KeyPathIndex: fr.KeyPathIndex(),
|
KeyPathIndex: fr.KeyPathIndex(),
|
||||||
|
|
||||||
MapKey: fr.Context.Key,
|
MapKey: fr.Context_.Key,
|
||||||
MapKeyEdgeIndex: fr.Context.EdgeIndex(),
|
MapKeyEdgeIndex: fr.Context_.EdgeIndex(),
|
||||||
Scope: fr.Context.Scope,
|
Scope: fr.Context_.Scope,
|
||||||
ScopeAST: fr.Context.ScopeAST,
|
ScopeAST: fr.Context_.ScopeAST,
|
||||||
}
|
}
|
||||||
if fr.Context.ScopeMap != nil && !d2ir.IsVar(fr.Context.ScopeMap) {
|
if fr.Context_.ScopeMap != nil && !d2ir.IsVar(fr.Context_.ScopeMap) {
|
||||||
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context.ScopeMap))
|
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context_.ScopeMap))
|
||||||
r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA)
|
r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA)
|
||||||
}
|
}
|
||||||
obj.References = append(obj.References, r)
|
obj.References = append(obj.References, r)
|
||||||
|
|
@ -441,10 +454,15 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
if arr, ok := f.Composite.(*d2ir.Array); ok {
|
if arr, ok := f.Composite.(*d2ir.Array); ok {
|
||||||
for _, constraint := range arr.Values {
|
for _, constraint := range arr.Values {
|
||||||
if scalar, ok := constraint.(*d2ir.Scalar); ok {
|
if scalar, ok := constraint.(*d2ir.Scalar); ok {
|
||||||
|
switch scalar.Value.(type) {
|
||||||
|
case *d2ast.Null:
|
||||||
|
attrs.Constraint = append(attrs.Constraint, "null")
|
||||||
|
default:
|
||||||
attrs.Constraint = append(attrs.Constraint, scalar.Value.ScalarString())
|
attrs.Constraint = append(attrs.Constraint, scalar.Value.ScalarString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case "label", "icon":
|
case "label", "icon":
|
||||||
c.compilePosition(attrs, f)
|
c.compilePosition(attrs, f)
|
||||||
default:
|
default:
|
||||||
|
|
@ -724,14 +742,14 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
|
||||||
edge.Label.MapKey = e.LastPrimaryKey()
|
edge.Label.MapKey = e.LastPrimaryKey()
|
||||||
for _, er := range e.References {
|
for _, er := range e.References {
|
||||||
r := d2graph.EdgeReference{
|
r := d2graph.EdgeReference{
|
||||||
Edge: er.Context.Edge,
|
Edge: er.Context_.Edge,
|
||||||
MapKey: er.Context.Key,
|
MapKey: er.Context_.Key,
|
||||||
MapKeyEdgeIndex: er.Context.EdgeIndex(),
|
MapKeyEdgeIndex: er.Context_.EdgeIndex(),
|
||||||
Scope: er.Context.Scope,
|
Scope: er.Context_.Scope,
|
||||||
ScopeAST: er.Context.ScopeAST,
|
ScopeAST: er.Context_.ScopeAST,
|
||||||
}
|
}
|
||||||
if er.Context.ScopeMap != nil && !d2ir.IsVar(er.Context.ScopeMap) {
|
if er.Context_.ScopeMap != nil && !d2ir.IsVar(er.Context_.ScopeMap) {
|
||||||
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context.ScopeMap))
|
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context_.ScopeMap))
|
||||||
r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA)
|
r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA)
|
||||||
}
|
}
|
||||||
edge.References = append(edge.References, r)
|
edge.References = append(edge.References, r)
|
||||||
|
|
@ -997,6 +1015,22 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *compiler) validateLabels(g *d2graph.Graph) {
|
||||||
|
for _, obj := range g.Objects {
|
||||||
|
if obj.Shape.Value != d2target.ShapeText {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj.Attributes.Language != "" {
|
||||||
|
// blockstrings have already been validated
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(obj.Label.Value) == "" {
|
||||||
|
c.errorf(obj.Label.MapKey, "shape text must have a non-empty label")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *compiler) validateNear(g *d2graph.Graph) {
|
func (c *compiler) validateNear(g *d2graph.Graph) {
|
||||||
for _, obj := range g.Objects {
|
for _, obj := range g.Objects {
|
||||||
if obj.NearKey != nil {
|
if obj.NearKey != nil {
|
||||||
|
|
@ -1050,20 +1084,13 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, edge := range g.Edges {
|
for _, edge := range g.Edges {
|
||||||
srcNearContainer := edge.Src.OuterNearContainer()
|
if edge.Src.IsConstantNear() && edge.Dst.IsDescendantOf(edge.Src) {
|
||||||
dstNearContainer := edge.Dst.OuterNearContainer()
|
c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Src.AbsID())
|
||||||
|
continue
|
||||||
var isSrcNearConst, isDstNearConst bool
|
|
||||||
|
|
||||||
if srcNearContainer != nil {
|
|
||||||
_, isSrcNearConst = d2graph.NearConstants[d2graph.Key(srcNearContainer.NearKey)[0]]
|
|
||||||
}
|
}
|
||||||
if dstNearContainer != nil {
|
if edge.Dst.IsConstantNear() && edge.Src.IsDescendantOf(edge.Dst) {
|
||||||
_, isDstNearConst = d2graph.NearConstants[d2graph.Key(dstNearContainer.NearKey)[0]]
|
c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Dst.AbsID())
|
||||||
}
|
continue
|
||||||
|
|
||||||
if (isSrcNearConst || isDstNearConst) && srcNearContainer != dstNearContainer {
|
|
||||||
c.errorf(edge.References[0].Edge, "cannot connect objects from within a container, that has near constant set, to objects outside that container")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1071,12 +1098,32 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
|
||||||
|
|
||||||
func (c *compiler) validateEdges(g *d2graph.Graph) {
|
func (c *compiler) validateEdges(g *d2graph.Graph) {
|
||||||
for _, edge := range g.Edges {
|
for _, edge := range g.Edges {
|
||||||
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil {
|
// edges from a grid to something outside is ok
|
||||||
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
|
// grid -> outside : ok
|
||||||
|
// grid -> grid.cell : not ok
|
||||||
|
// grid -> grid.cell.inner : not ok
|
||||||
|
if edge.Src.IsGridDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
|
||||||
|
c.errorf(edge.GetAstEdge(), "edge from grid diagram %#v cannot enter itself", edge.Src.AbsID())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil {
|
if edge.Dst.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
|
||||||
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
|
c.errorf(edge.GetAstEdge(), "edge from grid diagram %#v cannot enter itself", edge.Dst.AbsID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if edge.Src.Parent.IsGridDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
|
||||||
|
c.errorf(edge.GetAstEdge(), "edge from grid cell %#v cannot enter itself", edge.Src.AbsID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if edge.Dst.Parent.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
|
||||||
|
c.errorf(edge.GetAstEdge(), "edge from grid cell %#v cannot enter itself", edge.Dst.AbsID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if edge.Src.IsSequenceDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
|
||||||
|
c.errorf(edge.GetAstEdge(), "edge from sequence diagram %#v cannot enter itself", edge.Src.AbsID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if edge.Dst.IsSequenceDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
|
||||||
|
c.errorf(edge.GetAstEdge(), "edge from sequence diagram %#v cannot enter itself", edge.Dst.AbsID())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1616,7 +1616,7 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
|
||||||
}
|
}
|
||||||
x -> y
|
x -> y
|
||||||
`,
|
`,
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:5:5: cannot connect objects from within a container, that has near constant set, to objects outside that container`,
|
expErr: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "near_descendant_connect_to_outside",
|
name: "near_descendant_connect_to_outside",
|
||||||
|
|
@ -1627,7 +1627,7 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
|
||||||
}
|
}
|
||||||
x.y -> z
|
x.y -> z
|
||||||
`,
|
`,
|
||||||
expErr: "d2/testdata/d2compiler/TestCompile/near_descendant_connect_to_outside.d2:6:5: cannot connect objects from within a container, that has near constant set, to objects outside that container",
|
expErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested_near_constant",
|
name: "nested_near_constant",
|
||||||
|
|
@ -2040,7 +2040,7 @@ b
|
||||||
}
|
}
|
||||||
b -> x.a
|
b -> x.a
|
||||||
`,
|
`,
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/leaky_sequence.d2:5:1: connections within sequence diagrams can connect only to other objects within the same sequence diagram`,
|
expErr: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sequence_scoping",
|
name: "sequence_scoping",
|
||||||
|
|
@ -2199,6 +2199,19 @@ ok: {
|
||||||
tassert.Equal(t, []string{"primary_key", "foreign_key"}, table.Columns[1].Constraint)
|
tassert.Equal(t, []string{"primary_key", "foreign_key"}, table.Columns[1].Constraint)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sql-null-constraint",
|
||||||
|
text: `x: {
|
||||||
|
shape: sql_table
|
||||||
|
a: int {constraint: null}
|
||||||
|
b: int {constraint: [null]}
|
||||||
|
}`,
|
||||||
|
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||||
|
table := g.Objects[0].SQLTable
|
||||||
|
tassert.Nil(t, table.Columns[0].Constraint)
|
||||||
|
tassert.Equal(t, []string{"null"}, table.Columns[1].Constraint)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "wrong_column_index",
|
name: "wrong_column_index",
|
||||||
text: `Chinchillas: {
|
text: `Chinchillas: {
|
||||||
|
|
@ -2476,16 +2489,75 @@ d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must
|
||||||
name: "grid_edge",
|
name: "grid_edge",
|
||||||
text: `hey: {
|
text: `hey: {
|
||||||
grid-rows: 1
|
grid-rows: 1
|
||||||
a -> b
|
a -> b: ok
|
||||||
}
|
}
|
||||||
c -> hey.b
|
c -> hey.b
|
||||||
hey.a -> c
|
hey.a -> c
|
||||||
|
hey -> hey.a
|
||||||
|
|
||||||
hey -> c: ok
|
hey -> c: ok
|
||||||
`,
|
`,
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet
|
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:7:1: edge from grid diagram "hey" cannot enter itself`,
|
||||||
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges in grid diagrams are not supported yet
|
},
|
||||||
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are not supported yet`,
|
{
|
||||||
|
name: "grid_deeper_edge",
|
||||||
|
text: `hey: {
|
||||||
|
grid-rows: 1
|
||||||
|
a -> b: ok
|
||||||
|
b: {
|
||||||
|
c -> d: ok now
|
||||||
|
c.e -> c.f.g: ok
|
||||||
|
c.e -> d.h: ok
|
||||||
|
c -> d.h: ok
|
||||||
|
}
|
||||||
|
a: {
|
||||||
|
grid-columns: 1
|
||||||
|
e -> f: also ok now
|
||||||
|
e: {
|
||||||
|
g -> h: ok
|
||||||
|
g -> h.h: ok
|
||||||
|
}
|
||||||
|
e -> f.i: ok now
|
||||||
|
e.g -> f.i: ok now
|
||||||
|
}
|
||||||
|
a -> b.c: ok now
|
||||||
|
a.e -> b.c: ok now
|
||||||
|
a -> a.e: not ok
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expErr: `d2/testdata/d2compiler/TestCompile/grid_deeper_edge.d2:22:2: edge from grid diagram "hey.a" cannot enter itself`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parent_graph_edge_to_descendant",
|
||||||
|
text: `tl: {
|
||||||
|
near: top-left
|
||||||
|
a.b
|
||||||
|
}
|
||||||
|
grid: {
|
||||||
|
grid-rows: 1
|
||||||
|
cell.c.d
|
||||||
|
}
|
||||||
|
seq: {
|
||||||
|
shape: sequence_diagram
|
||||||
|
e.f
|
||||||
|
}
|
||||||
|
tl -> tl.a: no
|
||||||
|
tl -> tl.a.b: no
|
||||||
|
grid-> grid.cell: no
|
||||||
|
grid-> grid.cell.c: no
|
||||||
|
grid.cell -> grid.cell.c: no
|
||||||
|
grid.cell -> grid.cell.c.d: no
|
||||||
|
seq -> seq.e: no
|
||||||
|
seq -> seq.e.f: no
|
||||||
|
`,
|
||||||
|
expErr: `d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:13:1: edge from constant near "tl" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:14:1: edge from constant near "tl" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:17:1: edge from grid cell "grid.cell" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:18:1: edge from grid cell "grid.cell" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:15:1: edge from grid diagram "grid" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:16:1: edge from grid diagram "grid" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:19:1: edge from sequence diagram "seq" cannot enter itself
|
||||||
|
d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:20:1: edge from sequence diagram "seq" cannot enter itself`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "grid_nested",
|
name: "grid_nested",
|
||||||
|
|
@ -2625,6 +2697,28 @@ a -> b: { class: [association; one target] }
|
||||||
tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value)
|
tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "var_in_glob",
|
||||||
|
text: `vars: {
|
||||||
|
v: {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x1 -> x2
|
||||||
|
|
||||||
|
x*: {
|
||||||
|
...${v}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||||
|
tassert.Equal(t, 4, len(g.Objects))
|
||||||
|
tassert.Equal(t, "x1.ok", g.Objects[0].AbsID())
|
||||||
|
tassert.Equal(t, "x2.ok", g.Objects[1].AbsID())
|
||||||
|
tassert.Equal(t, "x1", g.Objects[2].AbsID())
|
||||||
|
tassert.Equal(t, "x2", g.Objects[3].AbsID())
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "class-shape-class",
|
name: "class-shape-class",
|
||||||
text: `classes: {
|
text: `classes: {
|
||||||
|
|
@ -2694,6 +2788,40 @@ object: {
|
||||||
`,
|
`,
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`,
|
expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "text_no_label",
|
||||||
|
text: `a: "ok" {
|
||||||
|
shape: text
|
||||||
|
}
|
||||||
|
b: " \n " {
|
||||||
|
shape: text
|
||||||
|
}
|
||||||
|
c: "" {
|
||||||
|
shape: text
|
||||||
|
}
|
||||||
|
d: "" {
|
||||||
|
shape: circle
|
||||||
|
}
|
||||||
|
e: " \n "
|
||||||
|
f: |md |
|
||||||
|
g: |md
|
||||||
|
|
||||||
|
|
|
||||||
|
`,
|
||||||
|
expErr: `d2/testdata/d2compiler/TestCompile/text_no_label.d2:14:1: block string cannot be empty
|
||||||
|
d2/testdata/d2compiler/TestCompile/text_no_label.d2:15:1: block string cannot be empty
|
||||||
|
d2/testdata/d2compiler/TestCompile/text_no_label.d2:4:1: shape text must have a non-empty label
|
||||||
|
d2/testdata/d2compiler/TestCompile/text_no_label.d2:7:1: shape text must have a non-empty label`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no_arrowheads_in_shape",
|
||||||
|
|
||||||
|
text: `x.target-arrowhead.shape: cf-one
|
||||||
|
y.source-arrowhead.shape: cf-one
|
||||||
|
`,
|
||||||
|
expErr: `d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:1:3: "target-arrowhead" can only be used on connections
|
||||||
|
d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:2:3: "source-arrowhead" can only be used on connections`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|
@ -2745,6 +2873,7 @@ func TestCompile2(t *testing.T) {
|
||||||
t.Run("seqdiagrams", testSeqDiagrams)
|
t.Run("seqdiagrams", testSeqDiagrams)
|
||||||
t.Run("nulls", testNulls)
|
t.Run("nulls", testNulls)
|
||||||
t.Run("vars", testVars)
|
t.Run("vars", testVars)
|
||||||
|
t.Run("globs", testGlobs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testBoards(t *testing.T) {
|
func testBoards(t *testing.T) {
|
||||||
|
|
@ -4017,6 +4146,44 @@ z: {
|
||||||
`, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-solo.d2:8:2: cannot substitute composite variable "x" as part of a string`)
|
`, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-solo.d2:8:2: cannot substitute composite variable "x" as part of a string`)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "spread-mid-string",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
vars: {
|
||||||
|
test: hello
|
||||||
|
}
|
||||||
|
|
||||||
|
mybox: {
|
||||||
|
label: prefix${test}suffix
|
||||||
|
}
|
||||||
|
`, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "undeclared-var-usage",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
x: { ...${v} }
|
||||||
|
`, `d2/testdata/d2compiler/TestCompile2/vars/errors/undeclared-var-usage.d2:2:4: could not resolve variable "v"`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "split-var-usage",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
x1
|
||||||
|
|
||||||
|
vars: {
|
||||||
|
v: {
|
||||||
|
style.fill: green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x1: { ...${v} }
|
||||||
|
`, ``)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tca {
|
for _, tc := range tca {
|
||||||
|
|
@ -4032,6 +4199,118 @@ z: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testGlobs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tca := []struct {
|
||||||
|
name string
|
||||||
|
skip bool
|
||||||
|
run func(t *testing.T)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "alixander-lazy-globs-review/1",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
***.style.fill: yellow
|
||||||
|
**.shape: circle
|
||||||
|
*.style.multiple: true
|
||||||
|
|
||||||
|
x: {
|
||||||
|
y
|
||||||
|
}
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
next: {
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-lazy-globs-review/2",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
**.style.fill: yellow
|
||||||
|
|
||||||
|
scenarios: {
|
||||||
|
b: {
|
||||||
|
a -> b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-lazy-globs-review/3",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
***: {
|
||||||
|
c: d
|
||||||
|
}
|
||||||
|
|
||||||
|
***: {
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
|
||||||
|
table: {
|
||||||
|
shape: sql_table
|
||||||
|
a: b
|
||||||
|
}
|
||||||
|
|
||||||
|
class: {
|
||||||
|
shape: class
|
||||||
|
a: b
|
||||||
|
}
|
||||||
|
`, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double-glob-err-val",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
**: {
|
||||||
|
label: hi
|
||||||
|
label.near: center
|
||||||
|
}
|
||||||
|
|
||||||
|
x: {
|
||||||
|
a -> b
|
||||||
|
}
|
||||||
|
`, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-err-val.d2:4:3: invalid "near" field`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double-glob-override-err-val",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
assertCompile(t, `
|
||||||
|
(** -> **)[*]: {
|
||||||
|
label.near: top-center
|
||||||
|
}
|
||||||
|
(** -> **)[*]: {
|
||||||
|
label.near: invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
x: {
|
||||||
|
a -> b
|
||||||
|
}
|
||||||
|
`, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-override-err-val.d2:6:2: invalid "near" field`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tca {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if tc.skip {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
tc.run(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) {
|
func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) {
|
||||||
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
|
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
|
||||||
g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil)
|
g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil)
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
||||||
LabelWidth: edge.SrcArrowhead.LabelDimensions.Width,
|
LabelWidth: edge.SrcArrowhead.LabelDimensions.Width,
|
||||||
LabelHeight: edge.SrcArrowhead.LabelDimensions.Height,
|
LabelHeight: edge.SrcArrowhead.LabelDimensions.Height,
|
||||||
}
|
}
|
||||||
|
if edge.SrcArrowhead.Style.FontColor != nil {
|
||||||
|
connection.SrcLabel.Color = edge.SrcArrowhead.Style.FontColor.Value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if edge.DstArrow {
|
if edge.DstArrow {
|
||||||
|
|
@ -228,6 +231,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
||||||
LabelWidth: edge.DstArrowhead.LabelDimensions.Width,
|
LabelWidth: edge.DstArrowhead.LabelDimensions.Width,
|
||||||
LabelHeight: edge.DstArrowhead.LabelDimensions.Height,
|
LabelHeight: edge.DstArrowhead.LabelDimensions.Height,
|
||||||
}
|
}
|
||||||
|
if edge.DstArrowhead.Style.FontColor != nil {
|
||||||
|
connection.DstLabel.Color = edge.DstArrowhead.Style.FontColor.Value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if theme != nil && theme.SpecialRules.NoCornerRadius {
|
if theme != nil && theme.SpecialRules.NoCornerRadius {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2compiler"
|
"oss.terrastruct.com/d2/d2compiler"
|
||||||
"oss.terrastruct.com/d2/d2exporter"
|
"oss.terrastruct.com/d2/d2exporter"
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
|
"oss.terrastruct.com/d2/d2layouts"
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2grid"
|
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
|
||||||
"oss.terrastruct.com/d2/d2lib"
|
"oss.terrastruct.com/d2/d2lib"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
|
@ -235,7 +234,8 @@ func run(t *testing.T, tc testCase) {
|
||||||
err = g.SetDimensions(nil, ruler, nil)
|
err = g.SetDimensions(nil, ruler, nil)
|
||||||
assert.JSON(t, nil, err)
|
assert.JSON(t, nil, err)
|
||||||
|
|
||||||
err = d2sequence.Layout(ctx, g, d2grid.Layout(ctx, g, d2dagrelayout.DefaultLayout))
|
graphInfo := d2layouts.NestedGraphInfo(g.Root)
|
||||||
|
err = d2layouts.LayoutNested(ctx, g, graphInfo, d2dagrelayout.DefaultLayout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -810,6 +810,25 @@ steps: {
|
||||||
step-1-content
|
step-1-content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "substitution_mid_string",
|
||||||
|
in: `vars: {
|
||||||
|
test: hello
|
||||||
|
}
|
||||||
|
|
||||||
|
mybox: {
|
||||||
|
label: prefix${test}suffix
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
exp: `vars: {
|
||||||
|
test: hello
|
||||||
|
}
|
||||||
|
|
||||||
|
mybox: {
|
||||||
|
label: prefix${test}suffix
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
@ -36,6 +37,7 @@ const DEFAULT_SHAPE_SIZE = 100.
|
||||||
const MIN_SHAPE_SIZE = 5
|
const MIN_SHAPE_SIZE = 5
|
||||||
|
|
||||||
type Graph struct {
|
type Graph struct {
|
||||||
|
FS fs.FS `json:"-"`
|
||||||
Parent *Graph `json:"-"`
|
Parent *Graph `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// IsFolderOnly indicates a board or scenario itself makes no modifications from its
|
// IsFolderOnly indicates a board or scenario itself makes no modifications from its
|
||||||
|
|
@ -55,6 +57,9 @@ type Graph struct {
|
||||||
Steps []*Graph `json:"steps,omitempty"`
|
Steps []*Graph `json:"steps,omitempty"`
|
||||||
|
|
||||||
Theme *d2themes.Theme `json:"theme,omitempty"`
|
Theme *d2themes.Theme `json:"theme,omitempty"`
|
||||||
|
|
||||||
|
// Object.Level uses the location of a nested graph
|
||||||
|
RootLevel int `json:"rootLevel,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGraph() *Graph {
|
func NewGraph() *Graph {
|
||||||
|
|
@ -527,7 +532,7 @@ func (obj *Object) GetStroke(dashGapSize interface{}) string {
|
||||||
|
|
||||||
func (obj *Object) Level() ContainerLevel {
|
func (obj *Object) Level() ContainerLevel {
|
||||||
if obj.Parent == nil {
|
if obj.Parent == nil {
|
||||||
return 0
|
return ContainerLevel(obj.Graph.RootLevel)
|
||||||
}
|
}
|
||||||
return 1 + obj.Parent.Level()
|
return 1 + obj.Parent.Level()
|
||||||
}
|
}
|
||||||
|
|
@ -1085,6 +1090,21 @@ func (obj *Object) OuterNearContainer() *Object {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (obj *Object) IsConstantNear() bool {
|
||||||
|
if obj.NearKey == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
keyPath := Key(obj.NearKey)
|
||||||
|
|
||||||
|
// interesting if there is a shape with id=top-left, then top-left isn't treated a constant near
|
||||||
|
_, isKey := obj.Graph.Root.HasChild(keyPath)
|
||||||
|
if isKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, isConst := NearConstants[keyPath[0]]
|
||||||
|
return isConst
|
||||||
|
}
|
||||||
|
|
||||||
type Edge struct {
|
type Edge struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
|
|
||||||
|
|
@ -1164,6 +1184,13 @@ func (e *Edge) Text() *d2target.MText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Edge) Move(dx, dy float64) {
|
||||||
|
for _, p := range e.Route {
|
||||||
|
p.X += dx
|
||||||
|
p.Y += dy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Edge) AbsID() string {
|
func (e *Edge) AbsID() string {
|
||||||
srcIDA := e.Src.AbsIDArray()
|
srcIDA := e.Src.AbsIDArray()
|
||||||
dstIDA := e.Dst.AbsIDArray()
|
dstIDA := e.Dst.AbsIDArray()
|
||||||
|
|
@ -1198,10 +1225,6 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
|
||||||
src := obj.ensureChildEdge(srcID)
|
src := obj.ensureChildEdge(srcID)
|
||||||
dst := obj.ensureChildEdge(dstID)
|
dst := obj.ensureChildEdge(dstID)
|
||||||
|
|
||||||
if src.OuterSequenceDiagram() != dst.OuterSequenceDiagram() {
|
|
||||||
return nil, errors.New("connections within sequence diagrams can connect only to other objects within the same sequence diagram")
|
|
||||||
}
|
|
||||||
|
|
||||||
e := &Edge{
|
e := &Edge{
|
||||||
Attributes: Attributes{
|
Attributes: Attributes{
|
||||||
Label: Scalar{
|
Label: Scalar{
|
||||||
|
|
@ -1898,7 +1921,7 @@ func (g *Graph) PrintString() string {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
fmt.Fprint(buf, "Objects: [")
|
fmt.Fprint(buf, "Objects: [")
|
||||||
for _, obj := range g.Objects {
|
for _, obj := range g.Objects {
|
||||||
fmt.Fprintf(buf, "%#v @(%v)", obj.AbsID(), obj.TopLeft.ToString())
|
fmt.Fprintf(buf, "%v, ", obj.AbsID())
|
||||||
}
|
}
|
||||||
fmt.Fprint(buf, "]")
|
fmt.Fprint(buf, "]")
|
||||||
return buf.String()
|
return buf.String()
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,33 @@ func (obj *Object) ClosestGridDiagram() *Object {
|
||||||
}
|
}
|
||||||
return obj.Parent.ClosestGridDiagram()
|
return obj.Parent.ClosestGridDiagram()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (obj *Object) ClosestGridCell() *Object {
|
||||||
|
if obj == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// grid cells can be a nested grid diagram
|
||||||
|
if obj.Parent.IsGridDiagram() {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return obj.Parent.ClosestGridCell()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopGridDiagram returns the least nested (outermost) grid diagram
|
||||||
|
func (obj *Object) TopGridDiagram() *Object {
|
||||||
|
if obj == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var gd *Object
|
||||||
|
if obj.IsGridDiagram() {
|
||||||
|
gd = obj
|
||||||
|
}
|
||||||
|
curr := obj.Parent
|
||||||
|
for curr != nil {
|
||||||
|
if curr.IsGridDiagram() {
|
||||||
|
gd = curr
|
||||||
|
}
|
||||||
|
curr = curr.Parent
|
||||||
|
}
|
||||||
|
return gd
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package d2graph
|
package d2graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ func (obj *Object) MoveWithDescendantsTo(x, y float64) {
|
||||||
obj.MoveWithDescendants(dx, dy)
|
obj.MoveWithDescendants(dx, dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (parent *Object) removeChild(child *Object) {
|
func (parent *Object) RemoveChild(child *Object) {
|
||||||
delete(parent.Children, strings.ToLower(child.ID))
|
delete(parent.Children, strings.ToLower(child.ID))
|
||||||
for i := 0; i < len(parent.ChildrenArray); i++ {
|
for i := 0; i < len(parent.ChildrenArray); i++ {
|
||||||
if parent.ChildrenArray[i] == child {
|
if parent.ChildrenArray[i] == child {
|
||||||
|
|
@ -41,6 +42,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
|
||||||
descendantObjects, edges := pluckObjAndEdges(g, obj)
|
descendantObjects, edges := pluckObjAndEdges(g, obj)
|
||||||
|
|
||||||
tempGraph := NewGraph()
|
tempGraph := NewGraph()
|
||||||
|
tempGraph.RootLevel = int(obj.Level()) - 1
|
||||||
tempGraph.Root.ChildrenArray = []*Object{obj}
|
tempGraph.Root.ChildrenArray = []*Object{obj}
|
||||||
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
|
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
|
||||||
|
|
||||||
|
|
@ -50,7 +52,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
|
||||||
tempGraph.Objects = descendantObjects
|
tempGraph.Objects = descendantObjects
|
||||||
tempGraph.Edges = edges
|
tempGraph.Edges = edges
|
||||||
|
|
||||||
obj.Parent.removeChild(obj)
|
obj.Parent.RemoveChild(obj)
|
||||||
obj.Parent = tempGraph.Root
|
obj.Parent = tempGraph.Root
|
||||||
|
|
||||||
return tempGraph
|
return tempGraph
|
||||||
|
|
@ -59,7 +61,7 @@ func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
|
||||||
func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) {
|
func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) {
|
||||||
for i := 0; i < len(g.Edges); i++ {
|
for i := 0; i < len(g.Edges); i++ {
|
||||||
edge := g.Edges[i]
|
edge := g.Edges[i]
|
||||||
if edge.Src == obj || edge.Dst == obj {
|
if edge.Src.IsDescendantOf(obj) && edge.Dst.IsDescendantOf(obj) {
|
||||||
edges = append(edges, edge)
|
edges = append(edges, edge)
|
||||||
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
|
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
|
||||||
i--
|
i--
|
||||||
|
|
@ -68,15 +70,10 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge
|
||||||
|
|
||||||
for i := 0; i < len(g.Objects); i++ {
|
for i := 0; i < len(g.Objects); i++ {
|
||||||
temp := g.Objects[i]
|
temp := g.Objects[i]
|
||||||
if temp.AbsID() == obj.AbsID() {
|
if temp.IsDescendantOf(obj) {
|
||||||
descendantsObjects = append(descendantsObjects, obj)
|
descendantsObjects = append(descendantsObjects, temp)
|
||||||
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
|
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
|
||||||
for _, child := range obj.ChildrenArray {
|
i--
|
||||||
subObjects, subEdges := pluckObjAndEdges(g, child)
|
|
||||||
descendantsObjects = append(descendantsObjects, subObjects...)
|
|
||||||
edges = append(edges, subEdges...)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +82,12 @@ func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edge
|
||||||
|
|
||||||
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
|
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
|
||||||
obj := tempGraph.Root.ChildrenArray[0]
|
obj := tempGraph.Root.ChildrenArray[0]
|
||||||
obj.MoveWithDescendantsTo(0, 0)
|
dx := 0 - obj.TopLeft.X
|
||||||
|
dy := 0 - obj.TopLeft.Y
|
||||||
|
obj.MoveWithDescendants(dx, dy)
|
||||||
|
for _, e := range tempGraph.Edges {
|
||||||
|
e.Move(dx, dy)
|
||||||
|
}
|
||||||
obj.Parent = parent
|
obj.Parent = parent
|
||||||
for _, obj := range tempGraph.Objects {
|
for _, obj := range tempGraph.Objects {
|
||||||
obj.Graph = g
|
obj.Graph = g
|
||||||
|
|
@ -284,6 +286,76 @@ func (obj *Object) GetModifierElementAdjustments() (dx, dy float64) {
|
||||||
return dx, dy
|
return dx, dy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (obj *Object) GetMargin() geo.Spacing {
|
||||||
|
margin := geo.Spacing{}
|
||||||
|
|
||||||
|
if obj.HasLabel() && obj.LabelPosition != nil {
|
||||||
|
position := label.Position(*obj.LabelPosition)
|
||||||
|
|
||||||
|
labelWidth := float64(obj.LabelDimensions.Width + label.PADDING)
|
||||||
|
labelHeight := float64(obj.LabelDimensions.Height + label.PADDING)
|
||||||
|
|
||||||
|
switch position {
|
||||||
|
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
|
||||||
|
margin.Top = labelHeight
|
||||||
|
case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
||||||
|
margin.Bottom = labelHeight
|
||||||
|
case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom:
|
||||||
|
margin.Left = labelWidth
|
||||||
|
case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
|
||||||
|
margin.Right = labelWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// if an outside label is larger than the object add margin accordingly
|
||||||
|
if labelWidth > obj.Width {
|
||||||
|
dx := labelWidth - obj.Width
|
||||||
|
switch position {
|
||||||
|
case label.OutsideTopLeft, label.OutsideBottomLeft:
|
||||||
|
// label fixed at left will overflow on right
|
||||||
|
margin.Right = dx
|
||||||
|
case label.OutsideTopCenter, label.OutsideBottomCenter:
|
||||||
|
margin.Left = math.Ceil(dx / 2)
|
||||||
|
margin.Right = math.Ceil(dx / 2)
|
||||||
|
case label.OutsideTopRight, label.OutsideBottomRight:
|
||||||
|
margin.Left = dx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if labelHeight > obj.Height {
|
||||||
|
dy := labelHeight - obj.Height
|
||||||
|
switch position {
|
||||||
|
case label.OutsideLeftTop, label.OutsideRightTop:
|
||||||
|
margin.Bottom = dy
|
||||||
|
case label.OutsideLeftMiddle, label.OutsideRightMiddle:
|
||||||
|
margin.Top = math.Ceil(dy / 2)
|
||||||
|
margin.Bottom = math.Ceil(dy / 2)
|
||||||
|
case label.OutsideLeftBottom, label.OutsideRightBottom:
|
||||||
|
margin.Top = dy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Icon != nil && obj.IconPosition != nil && obj.Shape.Value != d2target.ShapeImage {
|
||||||
|
position := label.Position(*obj.IconPosition)
|
||||||
|
|
||||||
|
iconSize := float64(d2target.MAX_ICON_SIZE + label.PADDING)
|
||||||
|
switch position {
|
||||||
|
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
|
||||||
|
margin.Top = math.Max(margin.Top, iconSize)
|
||||||
|
case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
|
||||||
|
margin.Bottom = math.Max(margin.Bottom, iconSize)
|
||||||
|
case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom:
|
||||||
|
margin.Left = math.Max(margin.Left, iconSize)
|
||||||
|
case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
|
||||||
|
margin.Right = math.Max(margin.Right, iconSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dx, dy := obj.GetModifierElementAdjustments()
|
||||||
|
margin.Right += dx
|
||||||
|
margin.Top += dy
|
||||||
|
return margin
|
||||||
|
}
|
||||||
|
|
||||||
func (obj *Object) ToShape() shape.Shape {
|
func (obj *Object) ToShape() shape.Shape {
|
||||||
tl := obj.TopLeft
|
tl := obj.TopLeft
|
||||||
if tl == nil {
|
if tl == nil {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type SerializedGraph struct {
|
||||||
Root SerializedObject `json:"root"`
|
Root SerializedObject `json:"root"`
|
||||||
Edges []SerializedEdge `json:"edges"`
|
Edges []SerializedEdge `json:"edges"`
|
||||||
Objects []SerializedObject `json:"objects"`
|
Objects []SerializedObject `json:"objects"`
|
||||||
|
RootLevel int `json:"rootLevel"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SerializedObject map[string]interface{}
|
type SerializedObject map[string]interface{}
|
||||||
|
|
@ -30,6 +31,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
|
||||||
convert(sg.Root, &root)
|
convert(sg.Root, &root)
|
||||||
g.Root = &root
|
g.Root = &root
|
||||||
root.Graph = g
|
root.Graph = g
|
||||||
|
g.RootLevel = sg.RootLevel
|
||||||
|
|
||||||
idToObj := make(map[string]*Object)
|
idToObj := make(map[string]*Object)
|
||||||
idToObj[""] = g.Root
|
idToObj[""] = g.Root
|
||||||
|
|
@ -39,6 +41,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
|
||||||
if err := convert(so, &o); err != nil {
|
if err := convert(so, &o); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
o.Graph = g
|
||||||
objects = append(objects, &o)
|
objects = append(objects, &o)
|
||||||
idToObj[so["AbsID"].(string)] = &o
|
idToObj[so["AbsID"].(string)] = &o
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +94,7 @@ func SerializeGraph(g *Graph) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sg.Root = root
|
sg.Root = root
|
||||||
|
sg.RootLevel = g.RootLevel
|
||||||
|
|
||||||
var sobjects []SerializedObject
|
var sobjects []SerializedObject
|
||||||
for _, o := range g.Objects {
|
for _, o := range g.Objects {
|
||||||
|
|
|
||||||
331
d2ir/compile.go
|
|
@ -5,14 +5,25 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2ast"
|
"oss.terrastruct.com/d2/d2ast"
|
||||||
"oss.terrastruct.com/d2/d2format"
|
"oss.terrastruct.com/d2/d2format"
|
||||||
"oss.terrastruct.com/d2/d2parser"
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
"oss.terrastruct.com/d2/d2themes"
|
"oss.terrastruct.com/d2/d2themes"
|
||||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||||
"oss.terrastruct.com/util-go/go2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type globContext struct {
|
||||||
|
root *globContext
|
||||||
|
refctx *RefContext
|
||||||
|
|
||||||
|
// Set of BoardIDA that this glob has already applied to.
|
||||||
|
appliedFields map[string]struct{}
|
||||||
|
// Set of Edge IDs that this glob has already applied to.
|
||||||
|
appliedEdges map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
type compiler struct {
|
type compiler struct {
|
||||||
err *d2parser.ParseError
|
err *d2parser.ParseError
|
||||||
|
|
||||||
|
|
@ -23,7 +34,13 @@ type compiler struct {
|
||||||
importCache map[string]*Map
|
importCache map[string]*Map
|
||||||
utf16Pos bool
|
utf16Pos bool
|
||||||
|
|
||||||
globStack []bool
|
// Stack of globs that must be recomputed at each new object in and below the current scope.
|
||||||
|
globContextStack [][]*globContext
|
||||||
|
// Used to prevent field globs causing infinite loops.
|
||||||
|
globRefContextStack []*RefContext
|
||||||
|
// Used to check whether ampersands are allowed in the current map.
|
||||||
|
mapRefContextStack []*RefContext
|
||||||
|
lazyGlobBeingApplied bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompileOptions struct {
|
type CompileOptions struct {
|
||||||
|
|
@ -49,8 +66,8 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, error) {
|
||||||
}
|
}
|
||||||
m := &Map{}
|
m := &Map{}
|
||||||
m.initRoot()
|
m.initRoot()
|
||||||
m.parent.(*Field).References[0].Context.Scope = ast
|
m.parent.(*Field).References[0].Context_.Scope = ast
|
||||||
m.parent.(*Field).References[0].Context.ScopeAST = ast
|
m.parent.(*Field).References[0].Context_.ScopeAST = ast
|
||||||
|
|
||||||
c.pushImportStack(&d2ast.Import{
|
c.pushImportStack(&d2ast.Import{
|
||||||
Path: []*d2ast.StringBox{d2ast.RawStringBox(ast.GetRange().Path, true)},
|
Path: []*d2ast.StringBox{d2ast.RawStringBox(ast.GetRange().Path, true)},
|
||||||
|
|
@ -83,7 +100,7 @@ func (c *compiler) overlayClasses(m *Map) {
|
||||||
|
|
||||||
for _, lf := range layers.Fields {
|
for _, lf := range layers.Fields {
|
||||||
if lf.Map() == nil || lf.Primary() != nil {
|
if lf.Map() == nil || lf.Primary() != nil {
|
||||||
c.errorf(lf.References[0].Context.Key, "invalid layer")
|
c.errorf(lf.References[0].Context_.Key, "invalid layer")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l := lf.Map()
|
l := lf.Map()
|
||||||
|
|
@ -108,6 +125,8 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
|
||||||
if f.Name == "vars" && f.Map() != nil {
|
if f.Name == "vars" && f.Map() != nil {
|
||||||
varsStack = append([]*Map{f.Map()}, varsStack...)
|
varsStack = append([]*Map{f.Map()}, varsStack...)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
for _, f := range m.Fields {
|
||||||
if f.Primary() != nil {
|
if f.Primary() != nil {
|
||||||
c.resolveSubstitutions(varsStack, f)
|
c.resolveSubstitutions(varsStack, f)
|
||||||
}
|
}
|
||||||
|
|
@ -337,7 +356,7 @@ func (c *compiler) resolveSubstitution(vars *Map, substitution *d2ast.Substituti
|
||||||
|
|
||||||
func (c *compiler) overlay(base *Map, f *Field) {
|
func (c *compiler) overlay(base *Map, f *Field) {
|
||||||
if f.Map() == nil || f.Primary() != nil {
|
if f.Map() == nil || f.Primary() != nil {
|
||||||
c.errorf(f.References[0].Context.Key, "invalid %s", NodeBoardKind(f))
|
c.errorf(f.References[0].Context_.Key, "invalid %s", NodeBoardKind(f))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
base = base.CopyBase(f)
|
base = base.CopyBase(f)
|
||||||
|
|
@ -345,7 +364,26 @@ func (c *compiler) overlay(base *Map, f *Field) {
|
||||||
f.Composite = base
|
f.Composite = base
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
func (g *globContext) copy() *globContext {
|
||||||
|
g2 := *g
|
||||||
|
g2.refctx = g.root.refctx.Copy()
|
||||||
|
return &g2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *globContext) prefixed(dst *Map) *globContext {
|
||||||
|
g2 := g.copy()
|
||||||
|
prefix := d2ast.MakeKeyPath(RelIDA(g2.refctx.ScopeMap, dst))
|
||||||
|
g2.refctx.Key = g2.refctx.Key.Copy()
|
||||||
|
if g2.refctx.Key.Key != nil {
|
||||||
|
prefix.Path = append(prefix.Path, g2.refctx.Key.Key.Path...)
|
||||||
|
}
|
||||||
|
if len(prefix.Path) > 0 {
|
||||||
|
g2.refctx.Key.Key = prefix
|
||||||
|
}
|
||||||
|
return g2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
|
||||||
for _, n := range ast.Nodes {
|
for _, n := range ast.Nodes {
|
||||||
switch {
|
switch {
|
||||||
case n.MapKey != nil:
|
case n.MapKey != nil:
|
||||||
|
|
@ -355,11 +393,56 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
ScopeMap: dst,
|
ScopeMap: dst,
|
||||||
ScopeAST: scopeAST,
|
ScopeAST: scopeAST,
|
||||||
})
|
})
|
||||||
|
if !ok {
|
||||||
|
// Unapply glob if appropriate.
|
||||||
|
gctx := c.getGlobContext(c.mapRefContextStack[len(c.mapRefContextStack)-1])
|
||||||
|
if gctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var ks string
|
||||||
|
if gctx.refctx.Key.HasTripleGlob() {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(IDA(dst)))
|
||||||
|
} else {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst)))
|
||||||
|
}
|
||||||
|
delete(gctx.appliedFields, ks)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
|
var globs []*globContext
|
||||||
|
if len(c.globContextStack) > 0 {
|
||||||
|
previousGlobs := c.globContextStack[len(c.globContextStack)-1]
|
||||||
|
if NodeBoardKind(dst) == BoardLayer {
|
||||||
|
for _, g := range previousGlobs {
|
||||||
|
if g.refctx.Key.HasTripleGlob() {
|
||||||
|
globs = append(globs, g.prefixed(dst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if NodeBoardKind(dst) != "" {
|
||||||
|
// Make all globs relative to the scenario or step.
|
||||||
|
for _, g := range previousGlobs {
|
||||||
|
globs = append(globs, g.prefixed(dst))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
globs = append(globs, previousGlobs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.globContextStack = append(c.globContextStack, globs)
|
||||||
|
defer func() {
|
||||||
|
dst.globs = c.globContexts()
|
||||||
|
c.globContextStack = c.globContextStack[:len(c.globContextStack)-1]
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok := c.ampersandFilterMap(dst, ast, scopeAST)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, n := range ast.Nodes {
|
for _, n := range ast.Nodes {
|
||||||
switch {
|
switch {
|
||||||
case n.MapKey != nil:
|
case n.MapKey != nil:
|
||||||
|
|
@ -378,6 +461,13 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
Value: []d2ast.InterpolationBox{{Substitution: n.Substitution}},
|
Value: []d2ast.InterpolationBox{{Substitution: n.Substitution}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
References: []*FieldReference{{
|
||||||
|
Context_: &RefContext{
|
||||||
|
Scope: ast,
|
||||||
|
ScopeMap: dst,
|
||||||
|
ScopeAST: scopeAST,
|
||||||
|
},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
dst.Fields = append(dst.Fields, f)
|
dst.Fields = append(dst.Fields, f)
|
||||||
case n.Import != nil:
|
case n.Import != nil:
|
||||||
|
|
@ -389,6 +479,17 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
c.errorf(n.Import, "cannot spread import non map into map")
|
c.errorf(n.Import, "cannot spread import non map into map")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, gctx := range impn.Map().globs {
|
||||||
|
if !gctx.refctx.Key.HasTripleGlob() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gctx2 := gctx.copy()
|
||||||
|
gctx2.refctx.ScopeMap = dst
|
||||||
|
c.compileKey(gctx2.refctx)
|
||||||
|
c.ensureGlobContext(gctx2.refctx)
|
||||||
|
}
|
||||||
|
|
||||||
OverlayMap(dst, impn.Map())
|
OverlayMap(dst, impn.Map())
|
||||||
|
|
||||||
if impnf, ok := impn.(*Field); ok {
|
if impnf, ok := impn.(*Field); ok {
|
||||||
|
|
@ -403,12 +504,68 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *compiler) globContexts() []*globContext {
|
||||||
|
return c.globContextStack[len(c.globContextStack)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compiler) getGlobContext(refctx *RefContext) *globContext {
|
||||||
|
for _, gctx := range c.globContexts() {
|
||||||
|
if gctx.refctx.Equal(refctx) {
|
||||||
|
return gctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compiler) ensureGlobContext(refctx *RefContext) *globContext {
|
||||||
|
gctx := c.getGlobContext(refctx)
|
||||||
|
if gctx != nil {
|
||||||
|
return gctx
|
||||||
|
}
|
||||||
|
gctx = &globContext{
|
||||||
|
refctx: refctx,
|
||||||
|
appliedFields: make(map[string]struct{}),
|
||||||
|
appliedEdges: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
gctx.root = gctx
|
||||||
|
c.globContextStack[len(c.globContextStack)-1] = append(c.globContexts(), gctx)
|
||||||
|
return gctx
|
||||||
|
}
|
||||||
|
|
||||||
func (c *compiler) compileKey(refctx *RefContext) {
|
func (c *compiler) compileKey(refctx *RefContext) {
|
||||||
|
if refctx.Key.HasGlob() {
|
||||||
|
// These printlns are for debugging infinite loops.
|
||||||
|
// println("og", refctx.Edge, refctx.Key, refctx.Scope, refctx.ScopeMap, refctx.ScopeAST)
|
||||||
|
for _, refctx2 := range c.globRefContextStack {
|
||||||
|
// println("st", refctx2.Edge, refctx2.Key, refctx2.Scope, refctx2.ScopeMap, refctx2.ScopeAST)
|
||||||
|
if refctx.Equal(refctx2) {
|
||||||
|
// Break the infinite loop.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// println("keys", d2format.Format(refctx2.Key), d2format.Format(refctx.Key))
|
||||||
|
}
|
||||||
|
c.globRefContextStack = append(c.globRefContextStack, refctx)
|
||||||
|
defer func() {
|
||||||
|
c.globRefContextStack = c.globRefContextStack[:len(c.globRefContextStack)-1]
|
||||||
|
}()
|
||||||
|
c.ensureGlobContext(refctx)
|
||||||
|
}
|
||||||
|
oldFields := refctx.ScopeMap.FieldCountRecursive()
|
||||||
|
oldEdges := refctx.ScopeMap.EdgeCountRecursive()
|
||||||
if len(refctx.Key.Edges) == 0 {
|
if len(refctx.Key.Edges) == 0 {
|
||||||
c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx)
|
c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx)
|
||||||
} else {
|
} else {
|
||||||
c.compileEdges(refctx)
|
c.compileEdges(refctx)
|
||||||
}
|
}
|
||||||
|
if oldFields != refctx.ScopeMap.FieldCountRecursive() || oldEdges != refctx.ScopeMap.EdgeCountRecursive() {
|
||||||
|
for _, gctx2 := range c.globContexts() {
|
||||||
|
// println(d2format.Format(gctx2.refctx.Key), d2format.Format(refctx.Key))
|
||||||
|
old := c.lazyGlobBeingApplied
|
||||||
|
c.lazyGlobBeingApplied = true
|
||||||
|
c.compileKey(gctx2.refctx)
|
||||||
|
c.lazyGlobBeingApplied = old
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) {
|
func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) {
|
||||||
|
|
@ -416,7 +573,7 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fa, err := dst.EnsureField(kp, refctx, true)
|
fa, err := dst.EnsureField(kp, refctx, true, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||||
return
|
return
|
||||||
|
|
@ -431,7 +588,7 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||||
if !refctx.Key.Ampersand {
|
if !refctx.Key.Ampersand {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if len(c.globStack) == 0 || !c.globStack[len(c.globStack)-1] {
|
if len(c.mapRefContextStack) == 0 || !c.mapRefContextStack[len(c.mapRefContextStack)-1].Key.SupportsGlobFilters() {
|
||||||
c.errorf(refctx.Key, "glob filters cannot be used outside globs")
|
c.errorf(refctx.Key, "glob filters cannot be used outside globs")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -439,14 +596,51 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, false)
|
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, false, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(fa) == 0 {
|
if len(fa) == 0 {
|
||||||
|
if refctx.Key.Key.Last().ScalarString() != "label" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
kp := refctx.Key.Key.Copy()
|
||||||
|
kp.Path = kp.Path[:len(kp.Path)-1]
|
||||||
|
if len(kp.Path) == 0 {
|
||||||
|
n := refctx.ScopeMap.Parent()
|
||||||
|
switch n := n.(type) {
|
||||||
|
case *Field:
|
||||||
|
fa = append(fa, n)
|
||||||
|
case *Edge:
|
||||||
|
if n.Primary_ == nil {
|
||||||
|
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n.Primary_.Value.ScalarString() != refctx.Key.Value.ScalarBox().Unbox().ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fa, err = refctx.ScopeMap.EnsureField(kp, refctx, false, c)
|
||||||
|
if err != nil {
|
||||||
|
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range fa {
|
||||||
|
label := f.Name
|
||||||
|
if f.Primary_ != nil {
|
||||||
|
label = f.Primary_.Value.ScalarString()
|
||||||
|
}
|
||||||
|
if label != refctx.Key.Value.ScalarBox().Unbox().ScalarString() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
for _, f := range fa {
|
for _, f := range fa {
|
||||||
ok := c._ampersandFilter(f, refctx)
|
ok := c._ampersandFilter(f, refctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -484,7 +678,22 @@ func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
if len(refctx.Key.Edges) == 0 && refctx.Key.Value.Null != nil {
|
// In case of filters, we need to pass filters before continuing
|
||||||
|
if refctx.Key.Value.Map != nil && refctx.Key.Value.Map.HasFilter() {
|
||||||
|
if f.Map() == nil {
|
||||||
|
f.Composite = &Map{
|
||||||
|
parent: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||||
|
ok := c.ampersandFilterMap(f.Map(), refctx.Key.Value.Map, refctx.ScopeAST)
|
||||||
|
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(refctx.Key.Edges) == 0 && (refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil) {
|
||||||
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name
|
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name
|
||||||
// Instead we keep it around, so that resolveSubstitutions can find it
|
// Instead we keep it around, so that resolveSubstitutions can find it
|
||||||
if !IsVar(ParentMap(f)) {
|
if !IsVar(ParentMap(f)) {
|
||||||
|
|
@ -494,11 +703,15 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if refctx.Key.Primary.Unbox() != nil {
|
if refctx.Key.Primary.Unbox() != nil {
|
||||||
|
if c.ignoreLazyGlob(f) {
|
||||||
|
return
|
||||||
|
}
|
||||||
f.Primary_ = &Scalar{
|
f.Primary_ = &Scalar{
|
||||||
parent: f,
|
parent: f,
|
||||||
Value: refctx.Key.Primary.Unbox(),
|
Value: refctx.Key.Primary.Unbox(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if refctx.Key.Value.Array != nil {
|
if refctx.Key.Value.Array != nil {
|
||||||
a := &Array{
|
a := &Array{
|
||||||
parent: f,
|
parent: f,
|
||||||
|
|
@ -506,12 +719,11 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
c.compileArray(a, refctx.Key.Value.Array, refctx.ScopeAST)
|
c.compileArray(a, refctx.Key.Value.Array, refctx.ScopeAST)
|
||||||
f.Composite = a
|
f.Composite = a
|
||||||
} else if refctx.Key.Value.Map != nil {
|
} else if refctx.Key.Value.Map != nil {
|
||||||
|
scopeAST := refctx.Key.Value.Map
|
||||||
if f.Map() == nil {
|
if f.Map() == nil {
|
||||||
f.Composite = &Map{
|
f.Composite = &Map{
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
scopeAST := refctx.Key.Value.Map
|
|
||||||
switch NodeBoardKind(f) {
|
switch NodeBoardKind(f) {
|
||||||
case BoardScenario:
|
case BoardScenario:
|
||||||
c.overlay(ParentBoard(f).Map(), f)
|
c.overlay(ParentBoard(f).Map(), f)
|
||||||
|
|
@ -532,9 +744,12 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
// If new board type, use that as the new scope AST, otherwise, carry on
|
// If new board type, use that as the new scope AST, otherwise, carry on
|
||||||
scopeAST = refctx.ScopeAST
|
scopeAST = refctx.ScopeAST
|
||||||
}
|
}
|
||||||
c.globStack = append(c.globStack, refctx.Key.HasQueryGlob())
|
} else {
|
||||||
|
scopeAST = refctx.ScopeAST
|
||||||
|
}
|
||||||
|
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||||
c.compileMap(f.Map(), refctx.Key.Value.Map, scopeAST)
|
c.compileMap(f.Map(), refctx.Key.Value.Map, scopeAST)
|
||||||
c.globStack = c.globStack[:len(c.globStack)-1]
|
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||||
switch NodeBoardKind(f) {
|
switch NodeBoardKind(f) {
|
||||||
case BoardScenario, BoardStep:
|
case BoardScenario, BoardStep:
|
||||||
c.overlayClasses(f.Map())
|
c.overlayClasses(f.Map())
|
||||||
|
|
@ -580,15 +795,30 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
||||||
// If the link is a board, we need to transform it into an absolute path.
|
if c.ignoreLazyGlob(f) {
|
||||||
if f.Name == "link" {
|
return
|
||||||
c.compileLink(refctx)
|
|
||||||
}
|
}
|
||||||
f.Primary_ = &Scalar{
|
f.Primary_ = &Scalar{
|
||||||
parent: f,
|
parent: f,
|
||||||
Value: refctx.Key.Value.ScalarBox().Unbox(),
|
Value: refctx.Key.Value.ScalarBox().Unbox(),
|
||||||
}
|
}
|
||||||
|
// If the link is a board, we need to transform it into an absolute path.
|
||||||
|
if f.Name == "link" {
|
||||||
|
c.compileLink(f, refctx)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether the current lazy glob being applied should not override the field
|
||||||
|
// if already set by a non glob key.
|
||||||
|
func (c *compiler) ignoreLazyGlob(n Node) bool {
|
||||||
|
if c.lazyGlobBeingApplied && n.Primary() != nil {
|
||||||
|
lastPrimaryRef := n.LastPrimaryRef()
|
||||||
|
if lastPrimaryRef != nil && !lastPrimaryRef.DueToLazyGlob() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) updateLinks(m *Map) {
|
func (c *compiler) updateLinks(m *Map) {
|
||||||
|
|
@ -613,8 +843,35 @@ func (c *compiler) updateLinks(m *Map) {
|
||||||
aida := IDA(f)
|
aida := IDA(f)
|
||||||
if len(bida) != len(aida) {
|
if len(bida) != len(aida) {
|
||||||
prependIDA := aida[:len(aida)-len(bida)]
|
prependIDA := aida[:len(aida)-len(bida)]
|
||||||
kp := d2ast.MakeKeyPath(prependIDA)
|
fullIDA := []string{"root"}
|
||||||
s := d2format.Format(kp) + strings.TrimPrefix(f.Primary_.Value.ScalarString(), "root")
|
// With nested imports, a value may already have been updated with part of the absolute path
|
||||||
|
// E.g.,
|
||||||
|
// The import prepends path a b c
|
||||||
|
// The existing path is b c d
|
||||||
|
// So the new path is
|
||||||
|
// a b c
|
||||||
|
// b c d
|
||||||
|
// -------
|
||||||
|
// a b c d
|
||||||
|
OUTER:
|
||||||
|
for i := 1; i < len(prependIDA); i += 2 {
|
||||||
|
for j := 0; i+j < len(prependIDA); j++ {
|
||||||
|
if prependIDA[i+j] != linkIDA[1+j] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Reached the end and all common
|
||||||
|
if i+j == len(prependIDA)-1 {
|
||||||
|
break OUTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fullIDA = append(fullIDA, prependIDA[i])
|
||||||
|
fullIDA = append(fullIDA, prependIDA[i+1])
|
||||||
|
}
|
||||||
|
// Chop off "root"
|
||||||
|
fullIDA = append(fullIDA, linkIDA[1:]...)
|
||||||
|
|
||||||
|
kp := d2ast.MakeKeyPath(fullIDA)
|
||||||
|
s := d2format.Format(kp)
|
||||||
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox()
|
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -624,7 +881,7 @@ func (c *compiler) updateLinks(m *Map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileLink(refctx *RefContext) {
|
func (c *compiler) compileLink(f *Field, refctx *RefContext) {
|
||||||
val := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
val := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||||
link, err := d2parser.ParseKey(val)
|
link, err := d2parser.ParseKey(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -683,7 +940,7 @@ func (c *compiler) compileLink(refctx *RefContext) {
|
||||||
// Create the absolute path by appending scope path with value specified
|
// Create the absolute path by appending scope path with value specified
|
||||||
scopeIDA = append(scopeIDA, linkIDA...)
|
scopeIDA = append(scopeIDA, linkIDA...)
|
||||||
kp := d2ast.MakeKeyPath(scopeIDA)
|
kp := d2ast.MakeKeyPath(scopeIDA)
|
||||||
refctx.Key.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(d2format.Format(kp)))
|
f.Primary_.Value = d2ast.FlatUnquotedString(d2format.Format(kp))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileEdges(refctx *RefContext) {
|
func (c *compiler) compileEdges(refctx *RefContext) {
|
||||||
|
|
@ -692,7 +949,7 @@ func (c *compiler) compileEdges(refctx *RefContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, true)
|
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, true, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||||
return
|
return
|
||||||
|
|
@ -726,21 +983,25 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||||
|
|
||||||
var ea []*Edge
|
var ea []*Edge
|
||||||
if eid.Index != nil || eid.Glob {
|
if eid.Index != nil || eid.Glob {
|
||||||
ea = refctx.ScopeMap.GetEdges(eid, refctx)
|
ea = refctx.ScopeMap.GetEdges(eid, refctx, c)
|
||||||
if len(ea) == 0 {
|
if len(ea) == 0 {
|
||||||
|
if !eid.Glob {
|
||||||
c.errorf(refctx.Edge, "indexed edge does not exist")
|
c.errorf(refctx.Edge, "indexed edge does not exist")
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, e := range ea {
|
for _, e := range ea {
|
||||||
e.References = append(e.References, &EdgeReference{
|
e.References = append(e.References, &EdgeReference{
|
||||||
Context: refctx,
|
Context_: refctx,
|
||||||
|
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||||
|
DueToLazyGlob_: c.lazyGlobBeingApplied,
|
||||||
})
|
})
|
||||||
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx)
|
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx, c)
|
||||||
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx)
|
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx, c)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
ea, err = refctx.ScopeMap.CreateEdge(eid, refctx)
|
ea, err = refctx.ScopeMap.CreateEdge(eid, refctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||||
continue
|
continue
|
||||||
|
|
@ -757,6 +1018,9 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||||
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
|
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
|
||||||
} else {
|
} else {
|
||||||
if refctx.Key.Primary.Unbox() != nil {
|
if refctx.Key.Primary.Unbox() != nil {
|
||||||
|
if c.ignoreLazyGlob(e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
e.Primary_ = &Scalar{
|
e.Primary_ = &Scalar{
|
||||||
parent: e,
|
parent: e,
|
||||||
Value: refctx.Key.Primary.Unbox(),
|
Value: refctx.Key.Primary.Unbox(),
|
||||||
|
|
@ -771,10 +1035,13 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||||
parent: e,
|
parent: e,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.globStack = append(c.globStack, refctx.Key.HasQueryGlob())
|
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||||
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
||||||
c.globStack = c.globStack[:len(c.globStack)-1]
|
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||||
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
||||||
|
if c.ignoreLazyGlob(e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
e.Primary_ = &Scalar{
|
e.Primary_ = &Scalar{
|
||||||
parent: e,
|
parent: e,
|
||||||
Value: refctx.Key.Value.ScalarBox().Unbox(),
|
Value: refctx.Key.Value.ScalarBox().Unbox(),
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ func assertQuery(t testing.TB, n d2ir.Node, nfields, nedges int, primary interfa
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(na) == 0 {
|
if len(na) == 0 {
|
||||||
return nil
|
t.Fatalf("query didn't match anything")
|
||||||
}
|
}
|
||||||
|
|
||||||
return na[0]
|
return na[0]
|
||||||
|
|
@ -416,10 +416,27 @@ scenarios: {
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
assert.Success(t, err)
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 8, 2, nil, "")
|
||||||
assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
|
assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "multiple-scenario-map",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `a -> b: { style.opacity: 0.3 }
|
||||||
|
scenarios: {
|
||||||
|
1: {
|
||||||
|
(a -> b)[0].style.opacity: 0.1
|
||||||
|
}
|
||||||
|
1: {
|
||||||
|
z
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 11, 2, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "scenarios.1.(a -> b)[0].style.opacity")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
runa(t, tca)
|
runa(t, tca)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
390
d2ir/d2ir.go
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2format"
|
"oss.terrastruct.com/d2/d2format"
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
"oss.terrastruct.com/d2/d2parser"
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
|
"oss.terrastruct.com/d2/d2target"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Most errors returned by a node should be created with d2parser.Errorf
|
// Most errors returned by a node should be created with d2parser.Errorf
|
||||||
|
|
@ -29,6 +30,7 @@ type Node interface {
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
|
|
||||||
LastRef() Reference
|
LastRef() Reference
|
||||||
|
LastPrimaryRef() Reference
|
||||||
LastPrimaryKey() *d2ast.Key
|
LastPrimaryKey() *d2ast.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,6 +112,10 @@ func (n *Scalar) LastRef() Reference { return parentRef(n) }
|
||||||
func (n *Map) LastRef() Reference { return parentRef(n) }
|
func (n *Map) LastRef() Reference { return parentRef(n) }
|
||||||
func (n *Array) LastRef() Reference { return parentRef(n) }
|
func (n *Array) LastRef() Reference { return parentRef(n) }
|
||||||
|
|
||||||
|
func (n *Scalar) LastPrimaryRef() Reference { return parentPrimaryRef(n) }
|
||||||
|
func (n *Map) LastPrimaryRef() Reference { return parentPrimaryRef(n) }
|
||||||
|
func (n *Array) LastPrimaryRef() Reference { return parentPrimaryRef(n) }
|
||||||
|
|
||||||
func (n *Scalar) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
|
func (n *Scalar) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
|
||||||
func (n *Map) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
|
func (n *Map) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
|
||||||
func (n *Array) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
|
func (n *Array) LastPrimaryKey() *d2ast.Key { return parentPrimaryKey(n) }
|
||||||
|
|
@ -119,6 +125,10 @@ type Reference interface {
|
||||||
// Most specific AST node for the reference.
|
// Most specific AST node for the reference.
|
||||||
AST() d2ast.Node
|
AST() d2ast.Node
|
||||||
Primary() bool
|
Primary() bool
|
||||||
|
Context() *RefContext
|
||||||
|
// Result of a glob in Context or from above.
|
||||||
|
DueToGlob() bool
|
||||||
|
DueToLazyGlob() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Reference = &FieldReference{}
|
var _ Reference = &FieldReference{}
|
||||||
|
|
@ -126,6 +136,12 @@ var _ Reference = &EdgeReference{}
|
||||||
|
|
||||||
func (r *FieldReference) reference() {}
|
func (r *FieldReference) reference() {}
|
||||||
func (r *EdgeReference) reference() {}
|
func (r *EdgeReference) reference() {}
|
||||||
|
func (r *FieldReference) Context() *RefContext { return r.Context_ }
|
||||||
|
func (r *EdgeReference) Context() *RefContext { return r.Context_ }
|
||||||
|
func (r *FieldReference) DueToGlob() bool { return r.DueToGlob_ }
|
||||||
|
func (r *EdgeReference) DueToGlob() bool { return r.DueToGlob_ }
|
||||||
|
func (r *FieldReference) DueToLazyGlob() bool { return r.DueToLazyGlob_ }
|
||||||
|
func (r *EdgeReference) DueToLazyGlob() bool { return r.DueToLazyGlob_ }
|
||||||
|
|
||||||
type Scalar struct {
|
type Scalar struct {
|
||||||
parent Node
|
parent Node
|
||||||
|
|
@ -154,13 +170,15 @@ type Map struct {
|
||||||
parent Node
|
parent Node
|
||||||
Fields []*Field `json:"fields"`
|
Fields []*Field `json:"fields"`
|
||||||
Edges []*Edge `json:"edges"`
|
Edges []*Edge `json:"edges"`
|
||||||
|
|
||||||
|
globs []*globContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) initRoot() {
|
func (m *Map) initRoot() {
|
||||||
m.parent = &Field{
|
m.parent = &Field{
|
||||||
Name: "root",
|
Name: "root",
|
||||||
References: []*FieldReference{{
|
References: []*FieldReference{{
|
||||||
Context: &RefContext{
|
Context_: &RefContext{
|
||||||
ScopeMap: m,
|
ScopeMap: m,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
|
@ -245,7 +263,11 @@ func NodeBoardKind(n Node) BoardKind {
|
||||||
}
|
}
|
||||||
f = ParentField(n)
|
f = ParentField(n)
|
||||||
case *Map:
|
case *Map:
|
||||||
f = ParentField(n)
|
var ok bool
|
||||||
|
f, ok = n.parent.(*Field)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
if f.Root() {
|
if f.Root() {
|
||||||
return BoardLayer
|
return BoardLayer
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +317,7 @@ func (f *Field) Copy(newParent Node) Node {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) lastPrimaryRef() *FieldReference {
|
func (f *Field) LastPrimaryRef() Reference {
|
||||||
for i := len(f.References) - 1; i >= 0; i-- {
|
for i := len(f.References) - 1; i >= 0; i-- {
|
||||||
if f.References[i].Primary() {
|
if f.References[i].Primary() {
|
||||||
return f.References[i]
|
return f.References[i]
|
||||||
|
|
@ -305,11 +327,11 @@ func (f *Field) lastPrimaryRef() *FieldReference {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) LastPrimaryKey() *d2ast.Key {
|
func (f *Field) LastPrimaryKey() *d2ast.Key {
|
||||||
fr := f.lastPrimaryRef()
|
fr := f.LastPrimaryRef()
|
||||||
if fr == nil {
|
if fr == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fr.Context.Key
|
return fr.(*FieldReference).Context_.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) LastRef() Reference {
|
func (f *Field) LastRef() Reference {
|
||||||
|
|
@ -451,10 +473,10 @@ func (e *Edge) Copy(newParent Node) Node {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Edge) lastPrimaryRef() *EdgeReference {
|
func (e *Edge) LastPrimaryRef() Reference {
|
||||||
for i := len(e.References) - 1; i >= 0; i-- {
|
for i := len(e.References) - 1; i >= 0; i-- {
|
||||||
fr := e.References[i]
|
fr := e.References[i]
|
||||||
if fr.Context.Key.EdgeKey == nil {
|
if fr.Context_.Key.EdgeKey == nil && !fr.DueToLazyGlob() {
|
||||||
return fr
|
return fr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -462,11 +484,11 @@ func (e *Edge) lastPrimaryRef() *EdgeReference {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Edge) LastPrimaryKey() *d2ast.Key {
|
func (e *Edge) LastPrimaryKey() *d2ast.Key {
|
||||||
er := e.lastPrimaryRef()
|
er := e.LastPrimaryRef()
|
||||||
if er == nil {
|
if er == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return er.Context.Key
|
return er.(*EdgeReference).Context_.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Edge) LastRef() Reference {
|
func (e *Edge) LastRef() Reference {
|
||||||
|
|
@ -494,16 +516,18 @@ type FieldReference struct {
|
||||||
String d2ast.String `json:"string"`
|
String d2ast.String `json:"string"`
|
||||||
KeyPath *d2ast.KeyPath `json:"key_path"`
|
KeyPath *d2ast.KeyPath `json:"key_path"`
|
||||||
|
|
||||||
Context *RefContext `json:"context"`
|
Context_ *RefContext `json:"context"`
|
||||||
|
DueToGlob_ bool `json:"due_to_glob"`
|
||||||
|
DueToLazyGlob_ bool `json:"due_to_lazy_glob"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary returns true if the Value in Context.Key.Value corresponds to the Field
|
// Primary returns true if the Value in Context.Key.Value corresponds to the Field
|
||||||
// represented by String.
|
// represented by String.
|
||||||
func (fr *FieldReference) Primary() bool {
|
func (fr *FieldReference) Primary() bool {
|
||||||
if fr.KeyPath == fr.Context.Key.Key {
|
if fr.KeyPath == fr.Context_.Key.Key {
|
||||||
return len(fr.Context.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
|
return len(fr.Context_.Key.Edges) == 0 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
|
||||||
} else if fr.KeyPath == fr.Context.Key.EdgeKey {
|
} else if fr.KeyPath == fr.Context_.Key.EdgeKey {
|
||||||
return len(fr.Context.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
|
return len(fr.Context_.Key.Edges) == 1 && fr.KeyPathIndex() == len(fr.KeyPath.Path)-1
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -518,33 +542,35 @@ func (fr *FieldReference) KeyPathIndex() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fr *FieldReference) EdgeDest() bool {
|
func (fr *FieldReference) EdgeDest() bool {
|
||||||
return fr.KeyPath == fr.Context.Edge.Dst
|
return fr.KeyPath == fr.Context_.Edge.Dst
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fr *FieldReference) InEdge() bool {
|
func (fr *FieldReference) InEdge() bool {
|
||||||
return fr.Context.Edge != nil
|
return fr.Context_.Edge != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fr *FieldReference) AST() d2ast.Node {
|
func (fr *FieldReference) AST() d2ast.Node {
|
||||||
if fr.String == nil {
|
if fr.String == nil {
|
||||||
// Root map.
|
// Root map.
|
||||||
return fr.Context.Scope
|
return fr.Context_.Scope
|
||||||
}
|
}
|
||||||
return fr.String
|
return fr.String
|
||||||
}
|
}
|
||||||
|
|
||||||
type EdgeReference struct {
|
type EdgeReference struct {
|
||||||
Context *RefContext `json:"context"`
|
Context_ *RefContext `json:"context"`
|
||||||
|
DueToGlob_ bool `json:"due_to_glob"`
|
||||||
|
DueToLazyGlob_ bool `json:"due_to_lazy_glob"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (er *EdgeReference) AST() d2ast.Node {
|
func (er *EdgeReference) AST() d2ast.Node {
|
||||||
return er.Context.Edge
|
return er.Context_.Edge
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary returns true if the Value in Context.Key.Value corresponds to the *Edge
|
// Primary returns true if the Value in Context.Key.Value corresponds to the *Edge
|
||||||
// represented by Context.Edge
|
// represented by Context.Edge
|
||||||
func (er *EdgeReference) Primary() bool {
|
func (er *EdgeReference) Primary() bool {
|
||||||
return len(er.Context.Key.Edges) == 1 && er.Context.Key.EdgeKey == nil
|
return len(er.Context_.Key.Edges) == 1 && er.Context_.Key.EdgeKey == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefContext struct {
|
type RefContext struct {
|
||||||
|
|
@ -569,6 +595,12 @@ func (rc *RefContext) EdgeIndex() int {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rc *RefContext) Equal(rc2 *RefContext) bool {
|
||||||
|
// We intentionally ignore edges here because the same glob can produce multiple RefContexts that should be treated the same with only the edge as the difference.
|
||||||
|
// Same with ScopeMap.
|
||||||
|
return rc.Key.Equals(rc2.Key) && rc.Scope == rc2.Scope && rc.ScopeAST == rc2.ScopeAST
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Map) FieldCountRecursive() int {
|
func (m *Map) FieldCountRecursive() int {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -667,7 +699,7 @@ func (m *Map) getField(ida []string) *Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureField is a bit of a misnomer. It's more of a Query/Ensure combination function at this point.
|
// EnsureField is a bit of a misnomer. It's more of a Query/Ensure combination function at this point.
|
||||||
func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool) ([]*Field, error) {
|
func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool, c *compiler) ([]*Field, error) {
|
||||||
i := 0
|
i := 0
|
||||||
for kp.Path[i].Unbox().ScalarString() == "_" {
|
for kp.Path[i].Unbox().ScalarString() == "_" {
|
||||||
m = ParentMap(m)
|
m = ParentMap(m)
|
||||||
|
|
@ -680,26 +712,77 @@ func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool) ([
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gctx *globContext
|
||||||
|
if refctx != nil && refctx.Key.HasGlob() && c != nil {
|
||||||
|
gctx = c.getGlobContext(refctx)
|
||||||
|
}
|
||||||
|
|
||||||
var fa []*Field
|
var fa []*Field
|
||||||
err := m.ensureField(i, kp, refctx, create, &fa)
|
err := m.ensureField(i, kp, refctx, create, gctx, c, &fa)
|
||||||
|
if len(fa) > 0 && c != nil && len(c.globRefContextStack) == 0 {
|
||||||
|
for _, gctx2 := range c.globContexts() {
|
||||||
|
old := c.lazyGlobBeingApplied
|
||||||
|
c.lazyGlobBeingApplied = true
|
||||||
|
c.compileKey(gctx2.refctx)
|
||||||
|
c.lazyGlobBeingApplied = old
|
||||||
|
}
|
||||||
|
}
|
||||||
return fa, err
|
return fa, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create bool, fa *[]*Field) error {
|
func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create bool, gctx *globContext, c *compiler, fa *[]*Field) error {
|
||||||
|
filter := func(f *Field, passthrough bool) bool {
|
||||||
|
if gctx != nil {
|
||||||
|
var ks string
|
||||||
|
if refctx.Key.HasTripleGlob() {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(IDA(f)))
|
||||||
|
} else {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f)))
|
||||||
|
}
|
||||||
|
if !kp.HasGlob() {
|
||||||
|
if !passthrough {
|
||||||
|
gctx.appliedFields[ks] = struct{}{}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// For globs with edges, we only ignore duplicate fields if the glob is not at the terminal of the keypath, the glob is on the common key or the glob is on the edge key. And only for globs with edge indexes.
|
||||||
|
lastEl := kp.Path[len(kp.Path)-1]
|
||||||
|
if len(refctx.Key.Edges) == 0 || lastEl.UnquotedString == nil || len(lastEl.UnquotedString.Pattern) == 0 || kp == refctx.Key.Key || kp == refctx.Key.EdgeKey {
|
||||||
|
if _, ok := gctx.appliedFields[ks]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !passthrough {
|
||||||
|
gctx.appliedFields[ks] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
faAppend := func(fa2 ...*Field) {
|
||||||
|
for _, f := range fa2 {
|
||||||
|
if filter(f, false) {
|
||||||
|
*fa = append(*fa, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
us, ok := kp.Path[i].Unbox().(*d2ast.UnquotedString)
|
us, ok := kp.Path[i].Unbox().(*d2ast.UnquotedString)
|
||||||
if ok && us.Pattern != nil {
|
if ok && us.Pattern != nil {
|
||||||
fa2, ok := m.doubleGlob(us.Pattern)
|
fa2, ok := m.multiGlob(us.Pattern)
|
||||||
if ok {
|
if ok {
|
||||||
if i == len(kp.Path)-1 {
|
if i == len(kp.Path)-1 {
|
||||||
*fa = append(*fa, fa2...)
|
faAppend(fa2...)
|
||||||
} else {
|
} else {
|
||||||
for _, f := range fa2 {
|
for _, f := range fa2 {
|
||||||
|
if !filter(f, true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if f.Map() == nil {
|
if f.Map() == nil {
|
||||||
f.Composite = &Map{
|
f.Composite = &Map{
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := f.Map().ensureField(i+1, kp, refctx, create, fa)
|
err := f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -710,14 +793,17 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
||||||
for _, f := range m.Fields {
|
for _, f := range m.Fields {
|
||||||
if matchPattern(f.Name, us.Pattern) {
|
if matchPattern(f.Name, us.Pattern) {
|
||||||
if i == len(kp.Path)-1 {
|
if i == len(kp.Path)-1 {
|
||||||
*fa = append(*fa, f)
|
faAppend(f)
|
||||||
} else {
|
} else {
|
||||||
|
if !filter(f, true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if f.Map() == nil {
|
if f.Map() == nil {
|
||||||
f.Composite = &Map{
|
f.Composite = &Map{
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := f.Map().ensureField(i+1, kp, refctx, create, fa)
|
err := f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -758,12 +844,17 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
||||||
f.References = append(f.References, &FieldReference{
|
f.References = append(f.References, &FieldReference{
|
||||||
String: kp.Path[i].Unbox(),
|
String: kp.Path[i].Unbox(),
|
||||||
KeyPath: kp,
|
KeyPath: kp,
|
||||||
Context: refctx,
|
Context_: refctx,
|
||||||
|
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||||
|
DueToLazyGlob_: c.lazyGlobBeingApplied,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if i+1 == len(kp.Path) {
|
if i+1 == len(kp.Path) {
|
||||||
*fa = append(*fa, f)
|
faAppend(f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !filter(f, true) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, ok := f.Composite.(*Array); ok {
|
if _, ok := f.Composite.(*Array); ok {
|
||||||
|
|
@ -774,33 +865,61 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f.Map().ensureField(i+1, kp, refctx, create, fa)
|
return f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !create {
|
if !create {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
shape := ParentShape(m)
|
||||||
|
if _, ok := d2graph.ReservedKeywords[strings.ToLower(head)]; !ok && len(c.globRefContextStack) > 0 {
|
||||||
|
if shape == d2target.ShapeClass || shape == d2target.ShapeSQLTable {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
f := &Field{
|
f := &Field{
|
||||||
parent: m,
|
parent: m,
|
||||||
Name: head,
|
Name: head,
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if i < kp.FirstGlob() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, grefctx := range c.globRefContextStack {
|
||||||
|
var ks string
|
||||||
|
if grefctx.Key.HasTripleGlob() {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(IDA(f)))
|
||||||
|
} else {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f)))
|
||||||
|
}
|
||||||
|
gctx2 := c.getGlobContext(grefctx)
|
||||||
|
gctx2.appliedFields[ks] = struct{}{}
|
||||||
|
}
|
||||||
|
}()
|
||||||
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
|
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
|
||||||
if refctx != nil {
|
if refctx != nil {
|
||||||
f.References = append(f.References, &FieldReference{
|
f.References = append(f.References, &FieldReference{
|
||||||
String: kp.Path[i].Unbox(),
|
String: kp.Path[i].Unbox(),
|
||||||
KeyPath: kp,
|
KeyPath: kp,
|
||||||
Context: refctx,
|
Context_: refctx,
|
||||||
|
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||||
|
DueToLazyGlob_: c.lazyGlobBeingApplied,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if !filter(f, true) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
m.Fields = append(m.Fields, f)
|
m.Fields = append(m.Fields, f)
|
||||||
if i+1 == len(kp.Path) {
|
if i+1 == len(kp.Path) {
|
||||||
*fa = append(*fa, f)
|
faAppend(f)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if f.Composite == nil {
|
||||||
f.Composite = &Map{
|
f.Composite = &Map{
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
return f.Map().ensureField(i+1, kp, refctx, create, fa)
|
}
|
||||||
|
return f.Map().ensureField(i+1, kp, refctx, create, gctx, c, fa)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) DeleteEdge(eid *EdgeID) *Edge {
|
func (m *Map) DeleteEdge(eid *EdgeID) *Edge {
|
||||||
|
|
@ -833,7 +952,7 @@ func (m *Map) DeleteField(ida ...string) *Field {
|
||||||
for _, fr := range f.References {
|
for _, fr := range f.References {
|
||||||
for _, e := range m.Edges {
|
for _, e := range m.Edges {
|
||||||
for _, er := range e.References {
|
for _, er := range e.References {
|
||||||
if er.Context == fr.Context {
|
if er.Context_ == fr.Context_ {
|
||||||
m.DeleteEdge(e.ID)
|
m.DeleteEdge(e.ID)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -865,10 +984,14 @@ func (m *Map) DeleteField(ida ...string) *Field {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
|
func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext, c *compiler) []*Edge {
|
||||||
if refctx != nil {
|
if refctx != nil {
|
||||||
|
var gctx *globContext
|
||||||
|
if refctx.Key.HasGlob() && c != nil {
|
||||||
|
gctx = c.ensureGlobContext(refctx)
|
||||||
|
}
|
||||||
var ea []*Edge
|
var ea []*Edge
|
||||||
m.getEdges(eid, refctx, &ea)
|
m.getEdges(eid, refctx, gctx, &ea)
|
||||||
return ea
|
return ea
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -882,7 +1005,7 @@ func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if f.Map() != nil {
|
if f.Map() != nil {
|
||||||
return f.Map().GetEdges(eid, nil)
|
return f.Map().GetEdges(eid, nil, nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -896,7 +1019,7 @@ func (m *Map) GetEdges(eid *EdgeID, refctx *RefContext) []*Edge {
|
||||||
return ea
|
return ea
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[]*Edge) error {
|
||||||
eid, m, common, err := eid.resolve(m)
|
eid, m, common, err := eid.resolve(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -914,7 +1037,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fa, err := m.EnsureField(commonKP, nil, false)
|
fa, err := m.EnsureField(commonKP, nil, false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -927,7 +1050,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = f.Map().getEdges(eid, refctx, ea)
|
err = f.Map().getEdges(eid, refctx, gctx, ea)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -935,11 +1058,11 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, nil, false)
|
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, nil, false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, nil, false)
|
dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, nil, false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -950,19 +1073,46 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
eid2.SrcPath = RelIDA(m, src)
|
eid2.SrcPath = RelIDA(m, src)
|
||||||
eid2.DstPath = RelIDA(m, dst)
|
eid2.DstPath = RelIDA(m, dst)
|
||||||
|
|
||||||
ea2 := m.GetEdges(eid2, nil)
|
ea2 := m.GetEdges(eid2, nil, nil)
|
||||||
|
for _, e := range ea2 {
|
||||||
|
if gctx != nil {
|
||||||
|
var ks string
|
||||||
|
if refctx.Key.HasTripleGlob() {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(IDA(e)))
|
||||||
|
} else {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e)))
|
||||||
|
}
|
||||||
|
if _, ok := gctx.appliedEdges[ks]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gctx.appliedEdges[ks] = struct{}{}
|
||||||
|
}
|
||||||
*ea = append(*ea, ea2...)
|
*ea = append(*ea, ea2...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext) ([]*Edge, error) {
|
func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext, c *compiler) ([]*Edge, error) {
|
||||||
var ea []*Edge
|
var ea []*Edge
|
||||||
return ea, m.createEdge(eid, refctx, &ea)
|
var gctx *globContext
|
||||||
|
if refctx != nil && refctx.Key.HasGlob() && c != nil {
|
||||||
|
gctx = c.ensureGlobContext(refctx)
|
||||||
|
}
|
||||||
|
err := m.createEdge(eid, refctx, gctx, c, &ea)
|
||||||
|
if len(ea) > 0 && c != nil && len(c.globRefContextStack) == 0 {
|
||||||
|
for _, gctx2 := range c.globContexts() {
|
||||||
|
old := c.lazyGlobBeingApplied
|
||||||
|
c.lazyGlobBeingApplied = true
|
||||||
|
c.compileKey(gctx2.refctx)
|
||||||
|
c.lazyGlobBeingApplied = old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ea, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *compiler, ea *[]*Edge) error {
|
||||||
if ParentEdge(m) != nil {
|
if ParentEdge(m) != nil {
|
||||||
return d2parser.Errorf(refctx.Edge, "cannot create edge inside edge")
|
return d2parser.Errorf(refctx.Edge, "cannot create edge inside edge")
|
||||||
}
|
}
|
||||||
|
|
@ -983,7 +1133,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fa, err := m.EnsureField(commonKP, nil, true)
|
fa, err := m.EnsureField(commonKP, nil, true, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -996,7 +1146,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
parent: f,
|
parent: f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = f.Map().createEdge(eid, refctx, ea)
|
err = f.Map().createEdge(eid, refctx, gctx, c, ea)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -1022,11 +1172,11 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
return d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense")
|
return d2parser.Errorf(refctx.Edge.Dst.Path[ij].Unbox(), "edge with board keyword alone doesn't make sense")
|
||||||
}
|
}
|
||||||
|
|
||||||
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx, true)
|
srcFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx, true, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx, true)
|
dstFA, err := refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx, true, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -1038,21 +1188,21 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if refctx.Edge.Src.HasDoubleGlob() {
|
if refctx.Edge.Src.HasMultiGlob() {
|
||||||
// If src has a double glob we only select leafs, those without children.
|
// If src has a double glob we only select leafs, those without children.
|
||||||
if src.Map().IsContainer() {
|
if src.Map().IsContainer() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ParentBoard(src) != ParentBoard(dst) {
|
if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if refctx.Edge.Dst.HasDoubleGlob() {
|
if refctx.Edge.Dst.HasMultiGlob() {
|
||||||
// If dst has a double glob we only select leafs, those without children.
|
// If dst has a double glob we only select leafs, those without children.
|
||||||
if dst.Map().IsContainer() {
|
if dst.Map().IsContainer() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ParentBoard(src) != ParentBoard(dst) {
|
if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1060,17 +1210,20 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, ea *[]*Edge) error {
|
||||||
eid2 := eid.Copy()
|
eid2 := eid.Copy()
|
||||||
eid2.SrcPath = RelIDA(m, src)
|
eid2.SrcPath = RelIDA(m, src)
|
||||||
eid2.DstPath = RelIDA(m, dst)
|
eid2.DstPath = RelIDA(m, dst)
|
||||||
e, err := m.createEdge2(eid2, refctx, src, dst)
|
|
||||||
|
e, err := m.createEdge2(eid2, refctx, gctx, c, src, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if e != nil {
|
||||||
*ea = append(*ea, e)
|
*ea = append(*ea, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Edge, error) {
|
func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c *compiler, src, dst *Field) (*Edge, error) {
|
||||||
if NodeBoardKind(src) != "" {
|
if NodeBoardKind(src) != "" {
|
||||||
return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards")
|
return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards")
|
||||||
}
|
}
|
||||||
|
|
@ -1083,7 +1236,7 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Ed
|
||||||
|
|
||||||
eid.Index = nil
|
eid.Index = nil
|
||||||
eid.Glob = true
|
eid.Glob = true
|
||||||
ea := m.GetEdges(eid, nil)
|
ea := m.GetEdges(eid, nil, nil)
|
||||||
index := len(ea)
|
index := len(ea)
|
||||||
eid.Index = &index
|
eid.Index = &index
|
||||||
eid.Glob = false
|
eid.Glob = false
|
||||||
|
|
@ -1091,9 +1244,29 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, src, dst *Field) (*Ed
|
||||||
parent: m,
|
parent: m,
|
||||||
ID: eid,
|
ID: eid,
|
||||||
References: []*EdgeReference{{
|
References: []*EdgeReference{{
|
||||||
Context: refctx,
|
Context_: refctx,
|
||||||
|
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||||
|
DueToLazyGlob_: c.lazyGlobBeingApplied,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gctx != nil {
|
||||||
|
var ks string
|
||||||
|
// We only ever want to create one of the edge per glob so we filter without the edge index.
|
||||||
|
e2 := e.Copy(e.Parent()).(*Edge)
|
||||||
|
e2.ID = e2.ID.Copy()
|
||||||
|
e2.ID.Index = nil
|
||||||
|
if refctx.Key.HasTripleGlob() {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(IDA(e2)))
|
||||||
|
} else {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e2)))
|
||||||
|
}
|
||||||
|
if _, ok := gctx.appliedEdges[ks]; ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
gctx.appliedEdges[ks] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
m.Edges = append(m.Edges, e)
|
m.Edges = append(m.Edges, e)
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
|
|
@ -1148,6 +1321,18 @@ func (e *Edge) AST() d2ast.Node {
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Edge) IDString() string {
|
||||||
|
ast := e.AST().(*d2ast.Key)
|
||||||
|
if e.ID.Index != nil {
|
||||||
|
ast.EdgeIndex = &d2ast.EdgeIndex{
|
||||||
|
Int: e.ID.Index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ast.Primary = d2ast.ScalarBox{}
|
||||||
|
ast.Value = d2ast.ValueBox{}
|
||||||
|
return d2format.Format(ast)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Array) AST() d2ast.Node {
|
func (a *Array) AST() d2ast.Node {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -1178,7 +1363,7 @@ func (m *Map) AST() d2ast.Node {
|
||||||
return astMap
|
return astMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext) {
|
func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext, c *compiler) {
|
||||||
sb := kp.Path[i]
|
sb := kp.Path[i]
|
||||||
f := m.GetField(sb.Unbox().ScalarString())
|
f := m.GetField(sb.Unbox().ScalarString())
|
||||||
if f == nil {
|
if f == nil {
|
||||||
|
|
@ -1188,13 +1373,15 @@ func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext
|
||||||
f.References = append(f.References, &FieldReference{
|
f.References = append(f.References, &FieldReference{
|
||||||
String: sb.Unbox(),
|
String: sb.Unbox(),
|
||||||
KeyPath: kp,
|
KeyPath: kp,
|
||||||
Context: refctx,
|
Context_: refctx,
|
||||||
|
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||||
|
DueToLazyGlob_: c.lazyGlobBeingApplied,
|
||||||
})
|
})
|
||||||
if i+1 == len(kp.Path) {
|
if i+1 == len(kp.Path) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if f.Map() != nil {
|
if f.Map() != nil {
|
||||||
f.Map().appendFieldReferences(i+1, kp, refctx)
|
f.Map().appendFieldReferences(i+1, kp, refctx, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1268,6 +1455,24 @@ func ParentEdge(n Node) *Edge {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParentShape(n Node) string {
|
||||||
|
for {
|
||||||
|
f, ok := n.(*Field)
|
||||||
|
if ok {
|
||||||
|
if f.Map() != nil {
|
||||||
|
shapef := f.Map().GetField("shape")
|
||||||
|
if shapef != nil && shapef.Primary() != nil {
|
||||||
|
return shapef.Primary().Value.ScalarString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.Parent()
|
||||||
|
if n == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func countUnderscores(p []string) int {
|
func countUnderscores(p []string) int {
|
||||||
for i, el := range p {
|
for i, el := range p {
|
||||||
if el != "_" {
|
if el != "_" {
|
||||||
|
|
@ -1310,6 +1515,18 @@ func parentRef(n Node) Reference {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parentPrimaryRef(n Node) Reference {
|
||||||
|
f := ParentField(n)
|
||||||
|
if f != nil {
|
||||||
|
return f.LastPrimaryRef()
|
||||||
|
}
|
||||||
|
e := ParentEdge(n)
|
||||||
|
if e != nil {
|
||||||
|
return e.LastPrimaryRef()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func parentPrimaryKey(n Node) *d2ast.Key {
|
func parentPrimaryKey(n Node) *d2ast.Key {
|
||||||
f := ParentField(n)
|
f := ParentField(n)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
|
|
@ -1325,60 +1542,65 @@ func parentPrimaryKey(n Node) *d2ast.Key {
|
||||||
// BoardIDA returns the absolute path to n from the nearest board root.
|
// BoardIDA returns the absolute path to n from the nearest board root.
|
||||||
func BoardIDA(n Node) (ida []string) {
|
func BoardIDA(n Node) (ida []string) {
|
||||||
for {
|
for {
|
||||||
f, ok := n.(*Field)
|
switch n := n.(type) {
|
||||||
if ok {
|
case *Field:
|
||||||
if f.Root() || NodeBoardKind(f) != "" {
|
if n.Root() || NodeBoardKind(n) != "" {
|
||||||
reverseIDA(ida)
|
reverseIDA(ida)
|
||||||
return ida
|
return ida
|
||||||
}
|
}
|
||||||
ida = append(ida, f.Name)
|
ida = append(ida, n.Name)
|
||||||
|
case *Edge:
|
||||||
|
ida = append(ida, n.IDString())
|
||||||
}
|
}
|
||||||
f = ParentField(n)
|
n = n.Parent()
|
||||||
if f == nil {
|
if n == nil {
|
||||||
reverseIDA(ida)
|
reverseIDA(ida)
|
||||||
return ida
|
return ida
|
||||||
}
|
}
|
||||||
n = f
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDA returns the absolute path to n.
|
// IDA returns the absolute path to n.
|
||||||
func IDA(n Node) (ida []string) {
|
func IDA(n Node) (ida []string) {
|
||||||
for {
|
for {
|
||||||
f, ok := n.(*Field)
|
switch n := n.(type) {
|
||||||
if ok {
|
case *Field:
|
||||||
ida = append(ida, f.Name)
|
ida = append(ida, n.Name)
|
||||||
if f.Root() {
|
if n.Root() {
|
||||||
reverseIDA(ida)
|
reverseIDA(ida)
|
||||||
return ida
|
return ida
|
||||||
}
|
}
|
||||||
|
case *Edge:
|
||||||
|
ida = append(ida, n.IDString())
|
||||||
}
|
}
|
||||||
f = ParentField(n)
|
n = n.Parent()
|
||||||
if f == nil {
|
if n == nil {
|
||||||
reverseIDA(ida)
|
reverseIDA(ida)
|
||||||
return ida
|
return ida
|
||||||
}
|
}
|
||||||
n = f
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelIDA returns the path to n relative to p.
|
// RelIDA returns the path to n relative to p.
|
||||||
func RelIDA(p, n Node) (ida []string) {
|
func RelIDA(p, n Node) (ida []string) {
|
||||||
for {
|
for {
|
||||||
f, ok := n.(*Field)
|
switch n := n.(type) {
|
||||||
if ok {
|
case *Field:
|
||||||
ida = append(ida, f.Name)
|
ida = append(ida, n.Name)
|
||||||
if f.Root() {
|
if n.Root() {
|
||||||
reverseIDA(ida)
|
reverseIDA(ida)
|
||||||
return ida
|
return ida
|
||||||
}
|
}
|
||||||
|
case *Edge:
|
||||||
|
ida = append(ida, n.String())
|
||||||
}
|
}
|
||||||
f = ParentField(n)
|
n = n.Parent()
|
||||||
if f == nil || f.Root() || f == p || f.Composite == p {
|
f, fok := n.(*Field)
|
||||||
|
e, eok := n.(*Edge)
|
||||||
|
if n == nil || (fok && (f.Root() || f == p || f.Composite == p)) || (eok && (e == p || e.Map_ == p)) {
|
||||||
reverseIDA(ida)
|
reverseIDA(ida)
|
||||||
return ida
|
return ida
|
||||||
}
|
}
|
||||||
n = f
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1476,7 +1698,7 @@ func (m *Map) InClass(key *d2ast.Key) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ref := range classF.References {
|
for _, ref := range classF.References {
|
||||||
if ref.Context.Key == key {
|
if ref.Context_.Key == key {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,120 @@ x -> y
|
||||||
assertQuery(t, m, 0, 0, nil, "(x -> y)[1]")
|
assertQuery(t, m, 0, 0, nil, "(x -> y)[1]")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "label-filter/1",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
x
|
||||||
|
y
|
||||||
|
p: p
|
||||||
|
a -> z: delta
|
||||||
|
|
||||||
|
*.style.opacity: 0.1
|
||||||
|
*: {
|
||||||
|
&label: x
|
||||||
|
style.opacity: 1
|
||||||
|
}
|
||||||
|
*: {
|
||||||
|
&label: p
|
||||||
|
style.opacity: 0.5
|
||||||
|
}
|
||||||
|
(* -> *)[*]: {
|
||||||
|
&label: delta
|
||||||
|
target-arrowhead.shape: diamond
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 17, 1, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, 1, "x.style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "y.style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, 0.5, "p.style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "a.style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "z.style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "(a -> z).target-arrowhead.shape")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label-filter/2",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
(* -> *)[*].style.opacity: 0.1
|
||||||
|
|
||||||
|
(* -> *)[*]: {
|
||||||
|
&label: hi
|
||||||
|
style.opacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
x -> y: hi
|
||||||
|
x -> y
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 6, 2, nil, "")
|
||||||
|
assertQuery(t, m, 2, 0, "hi", "(x -> y)[0]")
|
||||||
|
assertQuery(t, m, 0, 0, 1, "(x -> y)[0].style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[1].style.opacity")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lazy-filter",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
*: {
|
||||||
|
&label: a
|
||||||
|
style.fill: yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
# if i remove this line, the glob applies as expected
|
||||||
|
b
|
||||||
|
b.label: a
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 7, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "yellow", "a.style.fill")
|
||||||
|
assertQuery(t, m, 0, 0, "yellow", "b.style.fill")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "primary-filter",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
parent: {
|
||||||
|
a -> b1
|
||||||
|
a -> b2
|
||||||
|
a -> b3
|
||||||
|
|
||||||
|
b1 -> c1
|
||||||
|
b1 -> c2
|
||||||
|
|
||||||
|
c1: {
|
||||||
|
c1-child.class: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
c2: {
|
||||||
|
C2-child.class: hidden
|
||||||
|
}
|
||||||
|
c2.class: hidden
|
||||||
|
b2.class: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
classes: {
|
||||||
|
hidden: {
|
||||||
|
style: {
|
||||||
|
fill: red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error
|
||||||
|
**: null {
|
||||||
|
&class: hidden
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 9, 3, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runa(t, tca)
|
runa(t, tca)
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) {
|
||||||
|
|
||||||
ir = &Map{}
|
ir = &Map{}
|
||||||
ir.initRoot()
|
ir.initRoot()
|
||||||
ir.parent.(*Field).References[0].Context.Scope = ast
|
ir.parent.(*Field).References[0].Context_.Scope = ast
|
||||||
|
|
||||||
c.compileMap(ir, ast, ast)
|
c.compileMap(ir, ast, ast)
|
||||||
|
|
||||||
|
|
@ -128,14 +128,14 @@ func nilScopeMap(n Node) {
|
||||||
}
|
}
|
||||||
case *Edge:
|
case *Edge:
|
||||||
for _, r := range n.References {
|
for _, r := range n.References {
|
||||||
r.Context.ScopeMap = nil
|
r.Context_.ScopeMap = nil
|
||||||
}
|
}
|
||||||
if n.Map() != nil {
|
if n.Map() != nil {
|
||||||
nilScopeMap(n.Map())
|
nilScopeMap(n.Map())
|
||||||
}
|
}
|
||||||
case *Field:
|
case *Field:
|
||||||
for _, r := range n.References {
|
for _, r := range n.References {
|
||||||
r.Context.ScopeMap = nil
|
r.Context_.ScopeMap = nil
|
||||||
}
|
}
|
||||||
if n.Map() != nil {
|
if n.Map() != nil {
|
||||||
nilScopeMap(n.Map())
|
nilScopeMap(n.Map())
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,19 @@ label: meow`,
|
||||||
assertQuery(t, m, 0, 0, "root.layers.x.layers.y", "layers.x.y.link")
|
assertQuery(t, m, 0, 0, "root.layers.x.layers.y", "layers.x.y.link")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "boards-deep",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compileFS(t, "index.d2", map[string]string{
|
||||||
|
"index.d2": `a.link: layers.b; layers: { b: @b }`,
|
||||||
|
"b.d2": `b.link: layers.c; layers: { c: @c }`,
|
||||||
|
"c.d2": `c.link: layers.d; layers: { d: @d }`,
|
||||||
|
"d.d2": `d`,
|
||||||
|
})
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 0, 0, "root.layers.b.layers.c.layers.d", "layers.b.layers.c.c.link")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "steps-inheritence",
|
name: "steps-inheritence",
|
||||||
run: func(t testing.TB) {
|
run: func(t testing.TB) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ func OverlayMap(base, overlay *Map) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, oe := range overlay.Edges {
|
for _, oe := range overlay.Edges {
|
||||||
bea := base.GetEdges(oe.ID, nil)
|
bea := base.GetEdges(oe.ID, nil, nil)
|
||||||
if len(bea) == 0 {
|
if len(bea) == 0 {
|
||||||
base.Edges = append(base.Edges, oe.Copy(base).(*Edge))
|
base.Edges = append(base.Edges, oe.Copy(base).(*Edge))
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,68 @@ package d2ir
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/d2/d2ast"
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Map) doubleGlob(pattern []string) ([]*Field, bool) {
|
func (m *Map) multiGlob(pattern []string) ([]*Field, bool) {
|
||||||
if !(len(pattern) == 3 && pattern[0] == "*" && pattern[1] == "" && pattern[2] == "*") {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
var fa []*Field
|
var fa []*Field
|
||||||
|
if d2ast.IsDoubleGlob(pattern) {
|
||||||
m._doubleGlob(&fa)
|
m._doubleGlob(&fa)
|
||||||
return fa, true
|
return fa, true
|
||||||
|
}
|
||||||
|
if d2ast.IsTripleGlob(pattern) {
|
||||||
|
m._tripleGlob(&fa)
|
||||||
|
return fa, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Map) _doubleGlob(fa *[]*Field) {
|
func (m *Map) _doubleGlob(fa *[]*Field) {
|
||||||
for _, f := range m.Fields {
|
for _, f := range m.Fields {
|
||||||
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
|
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
|
||||||
|
if f.Name == "layers" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
|
if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// We don't ever want to append layers, scenarios or steps directly.
|
||||||
|
if f.Map() != nil {
|
||||||
|
f.Map()._tripleGlob(fa)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if NodeBoardKind(f) == "" {
|
||||||
*fa = append(*fa, f)
|
*fa = append(*fa, f)
|
||||||
|
}
|
||||||
if f.Map() != nil {
|
if f.Map() != nil {
|
||||||
f.Map()._doubleGlob(fa)
|
f.Map()._doubleGlob(fa)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Map) _tripleGlob(fa *[]*Field) {
|
||||||
|
for _, f := range m.Fields {
|
||||||
|
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
|
||||||
|
if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We don't ever want to append layers, scenarios or steps directly.
|
||||||
|
if f.Map() != nil {
|
||||||
|
f.Map()._tripleGlob(fa)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if NodeBoardKind(f) == "" {
|
||||||
|
*fa = append(*fa, f)
|
||||||
|
}
|
||||||
|
if f.Map() != nil {
|
||||||
|
f.Map()._tripleGlob(fa)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func matchPattern(s string, pattern []string) bool {
|
func matchPattern(s string, pattern []string) bool {
|
||||||
if len(pattern) == 0 {
|
if len(pattern) == 0 {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ an* -> an*`)
|
||||||
assert.Success(t, err)
|
assert.Success(t, err)
|
||||||
assertQuery(t, m, 2, 2, nil, "")
|
assertQuery(t, m, 2, 2, nil, "")
|
||||||
assertQuery(t, m, 0, 0, nil, "(animate -> animal)[0]")
|
assertQuery(t, m, 0, 0, nil, "(animate -> animal)[0]")
|
||||||
assertQuery(t, m, 0, 0, nil, "(animal -> animal)[0]")
|
assertQuery(t, m, 0, 0, nil, "(animal -> animate)[0]")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -154,7 +154,7 @@ sh*.an* -> sh*.an*`)
|
||||||
assertQuery(t, m, 3, 2, nil, "")
|
assertQuery(t, m, 3, 2, nil, "")
|
||||||
assertQuery(t, m, 2, 2, nil, "shared")
|
assertQuery(t, m, 2, 2, nil, "shared")
|
||||||
assertQuery(t, m, 0, 0, nil, "shared.(animate -> animal)[0]")
|
assertQuery(t, m, 0, 0, nil, "shared.(animate -> animal)[0]")
|
||||||
assertQuery(t, m, 0, 0, nil, "shared.(animal -> animal)[0]")
|
assertQuery(t, m, 0, 0, nil, "shared.(animal -> animate)[0]")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -285,6 +285,31 @@ d
|
||||||
assertQuery(t, m, 0, 0, nil, "(* -> *)[*]")
|
assertQuery(t, m, 0, 0, nil, "(* -> *)[*]")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "single-glob/defaults",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `wrapper.*: {
|
||||||
|
shape: page
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.a
|
||||||
|
wrapper.b
|
||||||
|
wrapper.c
|
||||||
|
wrapper.d
|
||||||
|
|
||||||
|
scenarios.x: { wrapper.p }
|
||||||
|
layers.x: { wrapper.p }
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 26, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "wrapper.a.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "wrapper.b.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "wrapper.c.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "wrapper.d.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "scenarios.x.wrapper.p.shape")
|
||||||
|
assertQuery(t, m, 0, 0, nil, "layers.x.wrapper.p")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "double-glob/edge/1",
|
name: "double-glob/edge/1",
|
||||||
run: func(t testing.TB) {
|
run: func(t testing.TB) {
|
||||||
|
|
@ -314,21 +339,438 @@ task.** -> fast
|
||||||
assertQuery(t, m, 3, 1, nil, "")
|
assertQuery(t, m, 3, 1, nil, "")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
runa(t, tca)
|
|
||||||
|
|
||||||
t.Run("errors", func(t *testing.T) {
|
|
||||||
tca := []testCase{
|
|
||||||
{
|
{
|
||||||
name: "glob-edge-glob-index",
|
name: "double-glob/defaults",
|
||||||
run: func(t testing.TB) {
|
run: func(t testing.TB) {
|
||||||
_, err := compile(t, `(* -> b)[*].style.fill: red
|
m, err := compile(t, `**: {
|
||||||
|
shape: page
|
||||||
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
d
|
||||||
|
|
||||||
|
scenarios.x: { p }
|
||||||
|
layers.x: { p }
|
||||||
`)
|
`)
|
||||||
assert.ErrorString(t, err, `TestCompile/patterns/errors/glob-edge-glob-index.d2:1:2: indexed edge does not exist`)
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 23, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "a.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "b.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "c.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "d.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "scenarios.x.p.shape")
|
||||||
|
assertQuery(t, m, 0, 0, nil, "layers.x.p")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "triple-glob/defaults",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `***: {
|
||||||
|
shape: page
|
||||||
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
d
|
||||||
|
|
||||||
|
scenarios.x: { p }
|
||||||
|
layers.x: { p }
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 24, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "a.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "b.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "c.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "d.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "scenarios.x.p.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "page", "layers.x.p.shape")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "triple-glob/edge-defaults",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `(*** -> ***)[*]: {
|
||||||
|
target-arrowhead.shape: diamond
|
||||||
|
}
|
||||||
|
|
||||||
|
a -> b
|
||||||
|
c -> d
|
||||||
|
|
||||||
|
scenarios.x: { p -> q }
|
||||||
|
layers.x: { j -> f }
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 28, 6, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "(a -> b)[0].target-arrowhead.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "(c -> d)[0].target-arrowhead.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "scenarios.x.(a -> b)[0].target-arrowhead.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "scenarios.x.(c -> d)[0].target-arrowhead.shape")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "scenarios.x.(p -> q)[0].target-arrowhead.shape")
|
||||||
|
assertQuery(t, m, 4, 1, nil, "layers.x")
|
||||||
|
assertQuery(t, m, 0, 0, "diamond", "layers.x.(j -> f)[0].target-arrowhead.shape")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/1",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
***.style.fill: yellow
|
||||||
|
**.shape: circle
|
||||||
|
*.style.multiple: true
|
||||||
|
|
||||||
|
x: {
|
||||||
|
y
|
||||||
|
}
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
next: {
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 14, 0, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/2",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
a
|
||||||
|
|
||||||
|
* -> y
|
||||||
|
|
||||||
|
b
|
||||||
|
c
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 4, 3, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/3",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
a
|
||||||
|
|
||||||
|
***.b -> c
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
z: {
|
||||||
|
d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 8, 2, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/4",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
**.child
|
||||||
|
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 6, 0, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/5",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
**.style.fill: red
|
||||||
|
|
||||||
|
scenarios: {
|
||||||
|
b: {
|
||||||
|
a -> b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 8, 1, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "scenarios.b.a.style.fill")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "scenarios.b.b.style.fill")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/6",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
(* -> *)[*].style.opacity: 0.1
|
||||||
|
|
||||||
|
x -> y: hi
|
||||||
|
x -> y
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 6, 2, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[0].style.opacity")
|
||||||
|
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[1].style.opacity")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/7",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
*: {
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
**: {
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
|
||||||
|
table: {
|
||||||
|
style.fill: blue
|
||||||
|
shape: sql_table
|
||||||
|
a: b
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 7, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "blue", "table.style.fill")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alixander-review/8",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
(a -> *)[*].style.stroke: red
|
||||||
|
(* -> *)[*].style.stroke: red
|
||||||
|
|
||||||
|
b -> c
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 4, 1, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "(b -> c)[0].style.stroke")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/1",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
**.style.fill: yellow
|
||||||
|
**.style.fill: red
|
||||||
|
|
||||||
|
a
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 3, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "a.style.fill")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/2",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
***.style.fill: yellow
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
hi: {
|
||||||
|
**.style.fill: red
|
||||||
|
# should be red, but it's yellow right now
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 5, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "layers.hi.a.style.fill")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/3",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
(*** -> ***)[*].label: hi
|
||||||
|
|
||||||
|
a -> b
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
hi: {
|
||||||
|
(*** -> ***)[*].label: bye
|
||||||
|
|
||||||
|
scenarios: {
|
||||||
|
b: {
|
||||||
|
# This label is "hi", but it should be "bye"
|
||||||
|
a -> b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 10, 2, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "hi", "(a -> b)[0].label")
|
||||||
|
assertQuery(t, m, 0, 0, "bye", "layers.hi.scenarios.b.(a -> b)[0].label")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/4",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
(*** -> ***)[*].label: hi
|
||||||
|
|
||||||
|
a -> b: {
|
||||||
|
label: bye
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 3, 1, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "bye", "(a -> b)[0].label")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/5",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
(*** -> ***)[*].label: hi
|
||||||
|
|
||||||
|
# This is "hey" right now but should be "hi"?
|
||||||
|
a -> b
|
||||||
|
|
||||||
|
(*** -> ***)[*].label: hey
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 3, 1, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "hey", "(a -> b)[0].label")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/6",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
# Nulling glob doesn't work
|
||||||
|
a
|
||||||
|
*a.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
|
||||||
|
a.icon: null
|
||||||
|
|
||||||
|
# Regular icon nulling works
|
||||||
|
b.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
|
||||||
|
b.icon: null
|
||||||
|
|
||||||
|
# Shape nulling works
|
||||||
|
*.shape: circle
|
||||||
|
a.shape: null
|
||||||
|
b.shape: null
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 2, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, nil, "a")
|
||||||
|
assertQuery(t, m, 0, 0, nil, "b")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override/7",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
# Nulling glob doesn't work
|
||||||
|
*a.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
|
||||||
|
a.icon: null
|
||||||
|
|
||||||
|
# Regular icon nulling works
|
||||||
|
b.icon: https://icons.terrastruct.com/essentials%2F073-add.svg
|
||||||
|
b.icon: null
|
||||||
|
|
||||||
|
# Shape nulling works
|
||||||
|
*.shape: circle
|
||||||
|
a.shape: null
|
||||||
|
b.shape: null
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 2, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, nil, "a")
|
||||||
|
assertQuery(t, m, 0, 0, nil, "b")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "table-class-exception",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
***: {
|
||||||
|
c: d
|
||||||
|
}
|
||||||
|
|
||||||
|
***: {
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
|
||||||
|
table: {
|
||||||
|
shape: sql_table
|
||||||
|
a: b
|
||||||
|
}
|
||||||
|
|
||||||
|
class: {
|
||||||
|
shape: class
|
||||||
|
a: b
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 13, 0, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prevent-chain-recursion",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
***: {
|
||||||
|
c: d
|
||||||
|
}
|
||||||
|
|
||||||
|
***: {
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 12, 0, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "import-glob/1",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compileFS(t, "index.d2", map[string]string{
|
||||||
|
"index.d2": "before; ...@globs.d2; after",
|
||||||
|
"globs.d2": `*: jingle
|
||||||
|
**: true
|
||||||
|
***: meow`,
|
||||||
|
})
|
||||||
|
assert.Success(t, err)
|
||||||
|
|
||||||
|
assertQuery(t, m, 2, 0, nil, "")
|
||||||
|
assertQuery(t, m, 0, 0, "meow", "before")
|
||||||
|
assertQuery(t, m, 0, 0, "meow", "after")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "import-glob/2",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compileFS(t, "index.d2", map[string]string{
|
||||||
|
"index.d2": `...@rules.d2
|
||||||
|
hi
|
||||||
|
`,
|
||||||
|
"rules.d2": `***.style.fill: red
|
||||||
|
***: meow
|
||||||
|
x`,
|
||||||
|
})
|
||||||
|
assert.Success(t, err)
|
||||||
|
|
||||||
|
assertQuery(t, m, 6, 0, nil, "")
|
||||||
|
assertQuery(t, m, 2, 0, "meow", "hi")
|
||||||
|
assertQuery(t, m, 2, 0, "meow", "x")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "hi.style.fill")
|
||||||
|
assertQuery(t, m, 0, 0, "red", "x.style.fill")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runa(t, tca)
|
runa(t, tca)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,14 @@ func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if k.Key != nil {
|
if k.Key != nil {
|
||||||
f := m.GetField(k.Key.IDA()...)
|
fa, err := m.EnsureField(k.Key, nil, false, nil)
|
||||||
if f == nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(fa) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
for _, f := range fa {
|
||||||
if len(k.Edges) == 0 {
|
if len(k.Edges) == 0 {
|
||||||
na = append(na, f)
|
na = append(na, f)
|
||||||
return na, nil
|
return na, nil
|
||||||
|
|
@ -27,6 +31,7 @@ func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eida := NewEdgeIDs(k)
|
eida := NewEdgeIDs(k)
|
||||||
|
|
||||||
|
|
@ -36,7 +41,7 @@ func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
|
||||||
ScopeMap: m,
|
ScopeMap: m,
|
||||||
Edge: k.Edges[i],
|
Edge: k.Edges[i],
|
||||||
}
|
}
|
||||||
ea := m.GetEdges(eid, refctx)
|
ea := m.GetEdges(eid, refctx, nil)
|
||||||
for _, e := range ea {
|
for _, e := range ea {
|
||||||
if k.EdgeKey == nil {
|
if k.EdgeKey == nil {
|
||||||
na = append(na, e)
|
na = append(na, e)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -140,17 +139,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScript := ""
|
mapper := NewObjectMapper()
|
||||||
idToObj := make(map[string]*d2graph.Object)
|
|
||||||
for _, obj := range g.Objects {
|
for _, obj := range g.Objects {
|
||||||
id := obj.AbsID()
|
mapper.Register(obj)
|
||||||
idToObj[id] = obj
|
}
|
||||||
|
loadScript := ""
|
||||||
width, height := obj.Width, obj.Height
|
for _, obj := range g.Objects {
|
||||||
|
loadScript += mapper.generateAddNodeLine(obj, int(obj.Width), int(obj.Height))
|
||||||
loadScript += generateAddNodeLine(id, int(width), int(height))
|
|
||||||
if obj.Parent != g.Root {
|
if obj.Parent != g.Root {
|
||||||
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
|
loadScript += mapper.generateAddParentLine(obj, obj.Parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,7 +175,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), width, height)
|
loadScript += mapper.generateAddEdgeLine(src, dst, edge.AbsID(), width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugJS {
|
if debugJS {
|
||||||
|
|
@ -209,7 +206,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
log.Debug(ctx, "graph", slog.F("json", dn))
|
log.Debug(ctx, "graph", slog.F("json", dn))
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := idToObj[dn.ID]
|
obj := mapper.ToObj(dn.ID)
|
||||||
|
|
||||||
// dagre gives center of node
|
// dagre gives center of node
|
||||||
obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2))
|
obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2))
|
||||||
|
|
@ -415,30 +412,6 @@ func setGraphAttrs(attrs dagreOpts) string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeID(id string) string {
|
|
||||||
// fixes \\
|
|
||||||
id = strings.ReplaceAll(id, "\\", `\\`)
|
|
||||||
// replaces \n with \\n whenever \n is not preceded by \ (does not replace \\n)
|
|
||||||
re := regexp.MustCompile(`[^\\]\n`)
|
|
||||||
id = re.ReplaceAllString(id, `\\n`)
|
|
||||||
// avoid an unescaped \r becoming a \n in the layout result
|
|
||||||
id = strings.ReplaceAll(id, "\r", `\r`)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateAddNodeLine(id string, width, height int) string {
|
|
||||||
id = escapeID(id)
|
|
||||||
return fmt.Sprintf("g.setNode(`%s`, { id: `%s`, width: %d, height: %d });\n", id, id, width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateAddParentLine(childID, parentID string) string {
|
|
||||||
return fmt.Sprintf("g.setParent(`%s`, `%s`);\n", escapeID(childID), escapeID(parentID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string {
|
|
||||||
return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLongestEdgeChainHead finds the longest chain in a container and gets its head
|
// getLongestEdgeChainHead finds the longest chain in a container and gets its head
|
||||||
// If there are multiple chains of the same length, get the head closest to the center
|
// If there are multiple chains of the same length, get the head closest to the center
|
||||||
func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
|
func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
|
||||||
|
|
|
||||||
63
d2layouts/d2dagrelayout/object_mapper.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -41,10 +41,38 @@ type ELKNode struct {
|
||||||
Width float64 `json:"width"`
|
Width float64 `json:"width"`
|
||||||
Height float64 `json:"height"`
|
Height float64 `json:"height"`
|
||||||
Children []*ELKNode `json:"children,omitempty"`
|
Children []*ELKNode `json:"children,omitempty"`
|
||||||
|
Ports []*ELKPort `json:"ports,omitempty"`
|
||||||
Labels []*ELKLabel `json:"labels,omitempty"`
|
Labels []*ELKLabel `json:"labels,omitempty"`
|
||||||
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
|
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PortSide string
|
||||||
|
|
||||||
|
const (
|
||||||
|
South PortSide = "SOUTH"
|
||||||
|
North PortSide = "NORTH"
|
||||||
|
East PortSide = "EAST"
|
||||||
|
West PortSide = "WEST"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Direction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Down Direction = "DOWN"
|
||||||
|
Up Direction = "UP"
|
||||||
|
Right Direction = "RIGHT"
|
||||||
|
Left Direction = "LEFT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ELKPort struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
X float64 `json:"x"`
|
||||||
|
Y float64 `json:"y"`
|
||||||
|
Width float64 `json:"width"`
|
||||||
|
Height float64 `json:"height"`
|
||||||
|
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ELKLabel struct {
|
type ELKLabel struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
X float64 `json:"x"`
|
X float64 `json:"x"`
|
||||||
|
|
@ -105,7 +133,7 @@ type elkOpts struct {
|
||||||
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
|
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
|
||||||
Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
|
Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
|
||||||
EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
|
EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
|
||||||
Direction string `json:"elk.direction"`
|
Direction Direction `json:"elk.direction"`
|
||||||
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
|
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
|
||||||
InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
|
InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
|
||||||
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
|
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
|
||||||
|
|
@ -118,6 +146,9 @@ type elkOpts struct {
|
||||||
ContentAlignment string `json:"elk.contentAlignment,omitempty"`
|
ContentAlignment string `json:"elk.contentAlignment,omitempty"`
|
||||||
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
|
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
|
||||||
|
|
||||||
|
PortSide PortSide `json:"elk.port.side,omitempty"`
|
||||||
|
PortConstraints string `json:"elk.portConstraints,omitempty"`
|
||||||
|
|
||||||
ConfigurableOpts
|
ConfigurableOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,15 +202,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
switch g.Root.Direction.Value {
|
switch g.Root.Direction.Value {
|
||||||
case "down":
|
case "down":
|
||||||
elkGraph.LayoutOptions.Direction = "DOWN"
|
elkGraph.LayoutOptions.Direction = Down
|
||||||
case "up":
|
case "up":
|
||||||
elkGraph.LayoutOptions.Direction = "UP"
|
elkGraph.LayoutOptions.Direction = Up
|
||||||
case "right":
|
case "right":
|
||||||
elkGraph.LayoutOptions.Direction = "RIGHT"
|
elkGraph.LayoutOptions.Direction = Right
|
||||||
case "left":
|
case "left":
|
||||||
elkGraph.LayoutOptions.Direction = "LEFT"
|
elkGraph.LayoutOptions.Direction = Left
|
||||||
default:
|
default:
|
||||||
elkGraph.LayoutOptions.Direction = "DOWN"
|
elkGraph.LayoutOptions.Direction = Down
|
||||||
}
|
}
|
||||||
|
|
||||||
// set label and icon positions for ELK
|
// set label and icon positions for ELK
|
||||||
|
|
@ -215,11 +246,15 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
if incoming >= 2 || outgoing >= 2 {
|
if incoming >= 2 || outgoing >= 2 {
|
||||||
switch g.Root.Direction.Value {
|
switch g.Root.Direction.Value {
|
||||||
case "right", "left":
|
case "right", "left":
|
||||||
|
if obj.Attributes.HeightAttr == nil {
|
||||||
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
|
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
if obj.Attributes.WidthAttr == nil {
|
||||||
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
|
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
width, height := adjustDimensions(obj)
|
width, height := adjustDimensions(obj)
|
||||||
|
|
||||||
|
|
@ -253,9 +288,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch elkGraph.LayoutOptions.Direction {
|
switch elkGraph.LayoutOptions.Direction {
|
||||||
case "DOWN", "UP":
|
case Down, Up:
|
||||||
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
|
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
|
||||||
case "RIGHT", "LEFT":
|
case Right, Left:
|
||||||
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
|
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -283,6 +318,33 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
} else {
|
} else {
|
||||||
elkNodes[parent].Children = append(elkNodes[parent].Children, n)
|
elkNodes[parent].Children = append(elkNodes[parent].Children, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obj.SQLTable != nil {
|
||||||
|
n.LayoutOptions.PortConstraints = "FIXED_POS"
|
||||||
|
columns := obj.SQLTable.Columns
|
||||||
|
colHeight := n.Height / float64(len(columns)+1)
|
||||||
|
n.Ports = make([]*ELKPort, 0, len(columns)*2)
|
||||||
|
var srcSide, dstSide PortSide
|
||||||
|
switch elkGraph.LayoutOptions.Direction {
|
||||||
|
case Left:
|
||||||
|
srcSide, dstSide = West, East
|
||||||
|
default:
|
||||||
|
srcSide, dstSide = East, West
|
||||||
|
}
|
||||||
|
for i, col := range columns {
|
||||||
|
n.Ports = append(n.Ports, &ELKPort{
|
||||||
|
ID: srcPortID(obj, col.Name.Label),
|
||||||
|
Y: float64(i+1)*colHeight + colHeight/2,
|
||||||
|
LayoutOptions: &elkOpts{PortSide: srcSide},
|
||||||
|
})
|
||||||
|
n.Ports = append(n.Ports, &ELKPort{
|
||||||
|
ID: dstPortID(obj, col.Name.Label),
|
||||||
|
Y: float64(i+1)*colHeight + colHeight/2,
|
||||||
|
LayoutOptions: &elkOpts{PortSide: dstSide},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
elkNodes[obj] = n
|
elkNodes[obj] = n
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -321,11 +383,64 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, edge := range g.Edges {
|
var srcSide, dstSide PortSide
|
||||||
|
switch elkGraph.LayoutOptions.Direction {
|
||||||
|
case Up:
|
||||||
|
srcSide, dstSide = North, South
|
||||||
|
default:
|
||||||
|
srcSide, dstSide = South, North
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := map[struct {
|
||||||
|
obj *d2graph.Object
|
||||||
|
side PortSide
|
||||||
|
}][]*ELKPort{}
|
||||||
|
|
||||||
|
for ei, edge := range g.Edges {
|
||||||
|
var src, dst string
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case edge.SrcTableColumnIndex != nil:
|
||||||
|
src = srcPortID(edge.Src, edge.Src.SQLTable.Columns[*edge.SrcTableColumnIndex].Name.Label)
|
||||||
|
case edge.Src.SQLTable != nil:
|
||||||
|
p := &ELKPort{
|
||||||
|
ID: fmt.Sprintf("%s.%d", srcPortID(edge.Src, "__root__"), ei),
|
||||||
|
LayoutOptions: &elkOpts{PortSide: srcSide},
|
||||||
|
}
|
||||||
|
src = p.ID
|
||||||
|
elkNodes[edge.Src].Ports = append(elkNodes[edge.Src].Ports, p)
|
||||||
|
k := struct {
|
||||||
|
obj *d2graph.Object
|
||||||
|
side PortSide
|
||||||
|
}{edge.Src, srcSide}
|
||||||
|
ports[k] = append(ports[k], p)
|
||||||
|
default:
|
||||||
|
src = edge.Src.AbsID()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case edge.DstTableColumnIndex != nil:
|
||||||
|
dst = dstPortID(edge.Dst, edge.Dst.SQLTable.Columns[*edge.DstTableColumnIndex].Name.Label)
|
||||||
|
case edge.Dst.SQLTable != nil:
|
||||||
|
p := &ELKPort{
|
||||||
|
ID: fmt.Sprintf("%s.%d", dstPortID(edge.Dst, "__root__"), ei),
|
||||||
|
LayoutOptions: &elkOpts{PortSide: dstSide},
|
||||||
|
}
|
||||||
|
dst = p.ID
|
||||||
|
elkNodes[edge.Dst].Ports = append(elkNodes[edge.Dst].Ports, p)
|
||||||
|
k := struct {
|
||||||
|
obj *d2graph.Object
|
||||||
|
side PortSide
|
||||||
|
}{edge.Dst, dstSide}
|
||||||
|
ports[k] = append(ports[k], p)
|
||||||
|
default:
|
||||||
|
dst = edge.Dst.AbsID()
|
||||||
|
}
|
||||||
|
|
||||||
e := &ELKEdge{
|
e := &ELKEdge{
|
||||||
ID: edge.AbsID(),
|
ID: edge.AbsID(),
|
||||||
Sources: []string{edge.Src.AbsID()},
|
Sources: []string{src},
|
||||||
Targets: []string{edge.Dst.AbsID()},
|
Targets: []string{dst},
|
||||||
}
|
}
|
||||||
if edge.Label.Value != "" {
|
if edge.Label.Value != "" {
|
||||||
e.Labels = append(e.Labels, &ELKLabel{
|
e.Labels = append(e.Labels, &ELKLabel{
|
||||||
|
|
@ -341,6 +456,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
elkEdges[edge] = e
|
elkEdges[edge] = e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, ports := range ports {
|
||||||
|
width := elkNodes[k.obj].Width
|
||||||
|
spacing := width / float64(len(ports)+1)
|
||||||
|
for i, p := range ports {
|
||||||
|
p.X = float64(i+1) * spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
raw, err := json.Marshal(elkGraph)
|
raw, err := json.Marshal(elkGraph)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -503,6 +626,14 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func srcPortID(obj *d2graph.Object, column string) string {
|
||||||
|
return fmt.Sprintf("%s.%s.src", obj.AbsID(), column)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dstPortID(obj *d2graph.Object, column string) string {
|
||||||
|
return fmt.Sprintf("%s.%s.dst", obj.AbsID(), column)
|
||||||
|
}
|
||||||
|
|
||||||
// deleteBends is a shim for ELK to delete unnecessary bends
|
// deleteBends is a shim for ELK to delete unnecessary bends
|
||||||
// see https://github.com/terrastruct/d2/issues/1030
|
// see https://github.com/terrastruct/d2/issues/1030
|
||||||
func deleteBends(g *d2graph.Graph) {
|
func deleteBends(g *d2graph.Graph) {
|
||||||
|
|
@ -521,30 +652,42 @@ func deleteBends(g *d2graph.Graph) {
|
||||||
var corner *geo.Point
|
var corner *geo.Point
|
||||||
var end *geo.Point
|
var end *geo.Point
|
||||||
|
|
||||||
|
var columnIndex *int
|
||||||
if isSource {
|
if isSource {
|
||||||
start = e.Route[0]
|
start = e.Route[0]
|
||||||
corner = e.Route[1]
|
corner = e.Route[1]
|
||||||
end = e.Route[2]
|
end = e.Route[2]
|
||||||
endpoint = e.Src
|
endpoint = e.Src
|
||||||
|
columnIndex = e.SrcTableColumnIndex
|
||||||
} else {
|
} else {
|
||||||
start = e.Route[len(e.Route)-1]
|
start = e.Route[len(e.Route)-1]
|
||||||
corner = e.Route[len(e.Route)-2]
|
corner = e.Route[len(e.Route)-2]
|
||||||
end = e.Route[len(e.Route)-3]
|
end = e.Route[len(e.Route)-3]
|
||||||
endpoint = e.Dst
|
endpoint = e.Dst
|
||||||
|
columnIndex = e.DstTableColumnIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
|
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
|
||||||
dx, dy := endpoint.GetModifierElementAdjustments()
|
dx, dy := endpoint.GetModifierElementAdjustments()
|
||||||
|
|
||||||
// Make sure it's still attached
|
// Make sure it's still attached
|
||||||
if isHorizontal {
|
switch {
|
||||||
|
case columnIndex != nil:
|
||||||
|
rowHeight := endpoint.Height / float64(len(endpoint.SQLTable.Columns)+1)
|
||||||
|
rowCenter := endpoint.TopLeft.Y + rowHeight*float64(*columnIndex+1) + rowHeight/2
|
||||||
|
|
||||||
|
// for row connections new Y coordinate should be within 1/3 row height from the row center
|
||||||
|
if math.Abs(end.Y-rowCenter) > rowHeight/3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case isHorizontal:
|
||||||
if end.Y <= endpoint.TopLeft.Y+10-dy {
|
if end.Y <= endpoint.TopLeft.Y+10-dy {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
|
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
if end.X <= endpoint.TopLeft.X+10 {
|
if end.X <= endpoint.TopLeft.X+10 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -606,12 +749,21 @@ func deleteBends(g *d2graph.Graph) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of ladders
|
// Get rid of ladders
|
||||||
// ELK likes to do these for some reason
|
// ELK likes to do these for some reason
|
||||||
// . ┌─
|
// . ┌─
|
||||||
// . ┌─┘
|
// . ┌─┘
|
||||||
// . │
|
// . │
|
||||||
// We want to transform these into L-shapes
|
// We want to transform these into L-shapes
|
||||||
|
|
||||||
|
points := map[geo.Point]int{}
|
||||||
|
for _, e := range g.Edges {
|
||||||
|
for _, p := range e.Route {
|
||||||
|
points[*p]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for ei, e := range g.Edges {
|
for ei, e := range g.Edges {
|
||||||
if len(e.Route) < 6 {
|
if len(e.Route) < 6 {
|
||||||
continue
|
continue
|
||||||
|
|
@ -627,6 +779,11 @@ func deleteBends(g *d2graph.Graph) {
|
||||||
end := e.Route[i+2]
|
end := e.Route[i+2]
|
||||||
after := e.Route[i+3]
|
after := e.Route[i+3]
|
||||||
|
|
||||||
|
if c, _ := points[*corner]; c > 1 {
|
||||||
|
// If corner is shared with another edge, they merge
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// S-shape on sources only concerned one segment, since the other was just along the bound of endpoint
|
// S-shape on sources only concerned one segment, since the other was just along the bound of endpoint
|
||||||
// These concern two segments
|
// These concern two segments
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package d2grid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
|
@ -11,6 +10,7 @@ import (
|
||||||
type gridDiagram struct {
|
type gridDiagram struct {
|
||||||
root *d2graph.Object
|
root *d2graph.Object
|
||||||
objects []*d2graph.Object
|
objects []*d2graph.Object
|
||||||
|
edges []*d2graph.Edge
|
||||||
rows int
|
rows int
|
||||||
columns int
|
columns int
|
||||||
|
|
||||||
|
|
@ -107,19 +107,7 @@ func (gd *gridDiagram) shift(dx, dy float64) {
|
||||||
for _, obj := range gd.objects {
|
for _, obj := range gd.objects {
|
||||||
obj.MoveWithDescendants(dx, dy)
|
obj.MoveWithDescendants(dx, dy)
|
||||||
}
|
}
|
||||||
}
|
for _, e := range gd.edges {
|
||||||
|
e.Move(dx, dy)
|
||||||
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
|
|
||||||
obj.Children = make(map[string]*d2graph.Object)
|
|
||||||
obj.ChildrenArray = make([]*d2graph.Object, 0)
|
|
||||||
|
|
||||||
restore := func(parent, child *d2graph.Object) {
|
|
||||||
parent.Children[strings.ToLower(child.ID)] = child
|
|
||||||
parent.ChildrenArray = append(parent.ChildrenArray, child)
|
|
||||||
graph.Objects = append(graph.Objects, child)
|
|
||||||
}
|
|
||||||
for _, child := range gd.objects {
|
|
||||||
restore(obj, child)
|
|
||||||
child.IterDescendants(restore)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
|
|
@ -21,77 +20,15 @@ const (
|
||||||
|
|
||||||
// Layout runs the grid layout on containers with rows/columns
|
// Layout runs the grid layout on containers with rows/columns
|
||||||
// Note: children are not allowed edges or descendants
|
// Note: children are not allowed edges or descendants
|
||||||
//
|
// 1. Run grid layout on the graph root
|
||||||
// 1. Traverse graph from root, skip objects with no rows/columns
|
// 2. Set the resulting dimensions to the graph root
|
||||||
// 2. Construct a grid with the container children
|
func Layout(ctx context.Context, g *d2graph.Graph) error {
|
||||||
// 3. Remove the children from the main graph
|
obj := g.Root
|
||||||
// 4. Run grid layout
|
|
||||||
// 5. Set the resulting dimensions to the main graph shape
|
|
||||||
// 6. Run core layouts (without grid children)
|
|
||||||
// 7. Put grid children back in correct location
|
|
||||||
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
|
|
||||||
return func(ctx context.Context, g *d2graph.Graph) error {
|
|
||||||
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g, layout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Root.IsGridDiagram() && len(g.Root.ChildrenArray) != 0 {
|
|
||||||
g.Root.TopLeft = geo.NewPoint(0, 0)
|
|
||||||
} else if err := layout(ctx, g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup(g, gridDiagrams, objectOrder)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
|
|
||||||
toRemove := make(map[*d2graph.Object]struct{})
|
|
||||||
gridDiagrams = make(map[string]*gridDiagram)
|
|
||||||
|
|
||||||
objectOrder = make(map[string]int)
|
|
||||||
for i, obj := range g.Objects {
|
|
||||||
objectOrder[obj.AbsID()] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
var processGrid func(obj *d2graph.Object) error
|
|
||||||
processGrid = func(obj *d2graph.Object) error {
|
|
||||||
for _, child := range obj.ChildrenArray {
|
|
||||||
if child.IsGridDiagram() {
|
|
||||||
if err := processGrid(child); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if len(child.ChildrenArray) > 0 {
|
|
||||||
tempGraph := g.ExtractAsNestedGraph(child)
|
|
||||||
if err := layout(ctx, tempGraph); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
g.InjectNestedGraph(tempGraph, obj)
|
|
||||||
|
|
||||||
sort.SliceStable(g.Objects, func(i, j int) bool {
|
|
||||||
return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
|
|
||||||
})
|
|
||||||
sort.SliceStable(child.ChildrenArray, func(i, j int) bool {
|
|
||||||
return objectOrder[child.ChildrenArray[i].AbsID()] < objectOrder[child.ChildrenArray[j].AbsID()]
|
|
||||||
})
|
|
||||||
sort.SliceStable(obj.ChildrenArray, func(i, j int) bool {
|
|
||||||
return objectOrder[obj.ChildrenArray[i].AbsID()] < objectOrder[obj.ChildrenArray[j].AbsID()]
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, o := range tempGraph.Objects {
|
|
||||||
toRemove[o] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gd, err := layoutGrid(g, obj)
|
gd, err := layoutGrid(g, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
obj.Children = make(map[string]*d2graph.Object)
|
|
||||||
obj.ChildrenArray = nil
|
|
||||||
|
|
||||||
if obj.Box != nil {
|
if obj.Box != nil {
|
||||||
// CONTAINER_PADDING is default, but use gap value if set
|
// CONTAINER_PADDING is default, but use gap value if set
|
||||||
|
|
@ -140,43 +77,6 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// also check for grid cells with outside top labels or icons
|
|
||||||
// the first grid object is at the top (and always exists)
|
|
||||||
topY := gd.objects[0].TopLeft.Y
|
|
||||||
highestOutside := topY
|
|
||||||
for _, o := range gd.objects {
|
|
||||||
// we only want to compute label positions for objects at the top of the grid
|
|
||||||
if o.TopLeft.Y > topY {
|
|
||||||
if gd.rowDirected {
|
|
||||||
// if the grid is rowDirected (row1, row2, etc) we can stop after finishing the first row
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
// otherwise we continue until the next column
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if o.LabelPosition != nil {
|
|
||||||
labelPosition := label.Position(*o.LabelPosition)
|
|
||||||
if labelPosition.IsOutside() {
|
|
||||||
labelTL := o.GetLabelTopLeft()
|
|
||||||
if labelTL.Y < highestOutside {
|
|
||||||
highestOutside = labelTL.Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if o.IconPosition != nil {
|
|
||||||
switch label.Position(*o.IconPosition) {
|
|
||||||
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
|
|
||||||
iconSpace := float64(d2target.MAX_ICON_SIZE + label.PADDING)
|
|
||||||
if topY-iconSpace < highestOutside {
|
|
||||||
highestOutside = topY - iconSpace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if highestOutside < topY {
|
|
||||||
occupiedHeight += topY - highestOutside + 2*label.PADDING
|
|
||||||
}
|
|
||||||
if occupiedHeight > float64(verticalPadding) {
|
if occupiedHeight > float64(verticalPadding) {
|
||||||
// if the label doesn't fit within the padding, we need to add more
|
// if the label doesn't fit within the padding, we need to add more
|
||||||
dy = occupiedHeight - float64(verticalPadding)
|
dy = occupiedHeight - float64(verticalPadding)
|
||||||
|
|
@ -195,70 +95,92 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
|
||||||
if obj.Icon != nil {
|
if obj.Icon != nil {
|
||||||
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
|
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
|
||||||
}
|
}
|
||||||
gridDiagrams[obj.AbsID()] = gd
|
|
||||||
|
|
||||||
for _, o := range gd.objects {
|
// simple straight line edge routing between grid objects
|
||||||
toRemove[o] = struct{}{}
|
for _, e := range g.Edges {
|
||||||
|
if !e.Src.Parent.IsDescendantOf(obj) && !e.Dst.Parent.IsDescendantOf(obj) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if edge is within grid, remove it from outer layout
|
||||||
|
gd.edges = append(gd.edges, e)
|
||||||
|
|
||||||
|
if e.Src.Parent != obj || e.Dst.Parent != obj {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if edge is grid child, use simple routing
|
||||||
|
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
|
||||||
|
e.TraceToShape(e.Route, 0, 1)
|
||||||
|
if e.Label.Value != "" {
|
||||||
|
e.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Root.IsGridDiagram() && len(g.Root.ChildrenArray) != 0 {
|
||||||
|
g.Root.TopLeft = geo.NewPoint(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||||
|
|
||||||
|
if g.RootLevel > 0 {
|
||||||
|
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
|
||||||
|
if obj.GridGap != nil || obj.HorizontalGap != nil {
|
||||||
|
horizontalPadding = gd.horizontalGap
|
||||||
|
}
|
||||||
|
if obj.GridGap != nil || obj.VerticalGap != nil {
|
||||||
|
verticalPadding = gd.verticalGap
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift the grid from (0, 0)
|
||||||
|
gd.shift(
|
||||||
|
obj.TopLeft.X+float64(horizontalPadding),
|
||||||
|
obj.TopLeft.Y+float64(verticalPadding),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
if len(g.Objects) > 0 {
|
|
||||||
queue := make([]*d2graph.Object, 1, len(g.Objects))
|
|
||||||
queue[0] = g.Root
|
|
||||||
for len(queue) > 0 {
|
|
||||||
obj := queue[0]
|
|
||||||
queue = queue[1:]
|
|
||||||
if len(obj.ChildrenArray) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !obj.IsGridDiagram() {
|
|
||||||
queue = append(queue, obj.ChildrenArray...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := processGrid(obj); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
|
|
||||||
for _, obj := range g.Objects {
|
|
||||||
if _, exists := toRemove[obj]; !exists {
|
|
||||||
layoutObjects = append(layoutObjects, obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.Objects = layoutObjects
|
|
||||||
|
|
||||||
return gridDiagrams, objectOrder, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
|
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
|
||||||
gd := newGridDiagram(obj)
|
gd := newGridDiagram(obj)
|
||||||
|
|
||||||
|
// position labels and icons
|
||||||
|
for _, o := range gd.objects {
|
||||||
|
positionedLabel := false
|
||||||
|
if o.Icon != nil && o.IconPosition == nil {
|
||||||
|
if len(o.ChildrenArray) > 0 {
|
||||||
|
o.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
|
||||||
|
// don't overwrite position if nested graph layout positioned label/icon
|
||||||
|
if o.LabelPosition == nil {
|
||||||
|
o.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
|
||||||
|
positionedLabel = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !positionedLabel && o.HasLabel() && o.LabelPosition == nil {
|
||||||
|
if len(o.ChildrenArray) > 0 {
|
||||||
|
o.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
|
||||||
|
} else if o.HasOutsideBottomLabel() {
|
||||||
|
o.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
|
||||||
|
} else if o.Icon != nil {
|
||||||
|
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||||
|
} else {
|
||||||
|
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// to handle objects with outside labels, we adjust their dimensions before layout and
|
||||||
|
// after layout, we remove the label adjustment and reposition TopLeft if needed
|
||||||
|
revertAdjustments := gd.sizeForOutsideLabels()
|
||||||
|
|
||||||
if gd.rows != 0 && gd.columns != 0 {
|
if gd.rows != 0 && gd.columns != 0 {
|
||||||
gd.layoutEvenly(g, obj)
|
gd.layoutEvenly(g, obj)
|
||||||
} else {
|
} else {
|
||||||
gd.layoutDynamic(g, obj)
|
gd.layoutDynamic(g, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
// position labels and icons
|
revertAdjustments()
|
||||||
for _, o := range gd.objects {
|
|
||||||
if o.Icon != nil {
|
|
||||||
// don't overwrite position if nested graph layout positioned label/icon
|
|
||||||
if o.LabelPosition == nil {
|
|
||||||
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
|
||||||
}
|
|
||||||
if o.IconPosition == nil {
|
|
||||||
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if o.LabelPosition == nil {
|
|
||||||
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gd, nil
|
return gd, nil
|
||||||
}
|
}
|
||||||
|
|
@ -486,7 +408,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
|
||||||
cursor.X = 0
|
cursor.X = 0
|
||||||
cursor.Y += rowHeight + verticalGap
|
cursor.Y += rowHeight + verticalGap
|
||||||
}
|
}
|
||||||
maxY = cursor.Y - horizontalGap
|
maxY = cursor.Y - verticalGap
|
||||||
} else {
|
} else {
|
||||||
// measure column heights
|
// measure column heights
|
||||||
colHeights := []float64{}
|
colHeights := []float64{}
|
||||||
|
|
@ -664,20 +586,14 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
|
||||||
// if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
|
// if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
|
||||||
if rowSize > okThreshold*targetSize {
|
if rowSize > okThreshold*targetSize {
|
||||||
skipCount++
|
skipCount++
|
||||||
if skipCount >= SKIP_LIMIT {
|
|
||||||
// there may even be too many to skip
|
// there may even be too many to skip
|
||||||
return true
|
return skipCount >= SKIP_LIMIT
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// row is too small to be good overall
|
// row is too small to be good overall
|
||||||
if rowSize < targetSize/okThreshold {
|
if rowSize < targetSize/okThreshold {
|
||||||
skipCount++
|
skipCount++
|
||||||
if skipCount >= SKIP_LIMIT {
|
return skipCount >= SKIP_LIMIT
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -936,53 +852,110 @@ func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalG
|
||||||
return totalDelta
|
return totalDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup restores the graph after the core layout engine finishes
|
func (gd *gridDiagram) sizeForOutsideLabels() (revert func()) {
|
||||||
// - translating the grid to its position placed by the core layout engine
|
margins := make(map[*d2graph.Object]geo.Spacing)
|
||||||
// - restore the children of the grid
|
|
||||||
// - sorts objects to their original graph order
|
|
||||||
func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objectsOrder map[string]int) {
|
|
||||||
defer func() {
|
|
||||||
sort.SliceStable(graph.Objects, func(i, j int) bool {
|
|
||||||
return objectsOrder[graph.Objects[i].AbsID()] < objectsOrder[graph.Objects[j].AbsID()]
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
|
|
||||||
var restore func(obj *d2graph.Object)
|
for _, o := range gd.objects {
|
||||||
restore = func(obj *d2graph.Object) {
|
margin := o.GetMargin()
|
||||||
gd, exists := gridDiagrams[obj.AbsID()]
|
margins[o] = margin
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
|
||||||
|
|
||||||
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
|
o.Height += margin.Top + margin.Bottom
|
||||||
if obj.GridGap != nil || obj.HorizontalGap != nil {
|
o.Width += margin.Left + margin.Right
|
||||||
horizontalPadding = gd.horizontalGap
|
|
||||||
}
|
|
||||||
if obj.GridGap != nil || obj.VerticalGap != nil {
|
|
||||||
verticalPadding = gd.verticalGap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shift the grid from (0, 0)
|
// Example: a single column with 3 shapes and
|
||||||
gd.shift(
|
// `x.label: long label {near: outside-bottom-left}`
|
||||||
obj.TopLeft.X+float64(horizontalPadding),
|
// `y.label: outsider {near: outside-right-center}`
|
||||||
obj.TopLeft.Y+float64(verticalPadding),
|
// . ┌───────────────────┐
|
||||||
)
|
// . │ widest shape here │
|
||||||
gd.cleanup(obj, graph)
|
// . └───────────────────┘
|
||||||
|
// . ┌───┐
|
||||||
|
// . │ x │
|
||||||
|
// . └───┘
|
||||||
|
// . long label
|
||||||
|
// . ├─────────┤ x's new width
|
||||||
|
// . ├─mr──┤ margin.right added to width during layout
|
||||||
|
// . ┌───┐
|
||||||
|
// . │ y │ outsider
|
||||||
|
// . └───┘
|
||||||
|
// . ├─────────────┤ y's new width
|
||||||
|
// . ├───mr────┤ margin.right added to width during layout
|
||||||
|
|
||||||
for _, child := range obj.ChildrenArray {
|
// BEFORE LAYOUT
|
||||||
restore(child)
|
// . ┌───────────────────┐
|
||||||
|
// . │ widest shape here │
|
||||||
|
// . └───────────────────┘
|
||||||
|
// . ┌─────────┐
|
||||||
|
// . │ x │
|
||||||
|
// . └─────────┘
|
||||||
|
// . ┌─────────────┐
|
||||||
|
// . │ y │
|
||||||
|
// . └─────────────┘
|
||||||
|
|
||||||
|
// AFTER LAYOUT
|
||||||
|
// . ┌───────────────────┐
|
||||||
|
// . │ widest shape here │
|
||||||
|
// . └───────────────────┘
|
||||||
|
// . ┌───────────────────┐
|
||||||
|
// . │ x │
|
||||||
|
// . └───────────────────┘
|
||||||
|
// . ┌───────────────────┐
|
||||||
|
// . │ y │
|
||||||
|
// . └───────────────────┘
|
||||||
|
|
||||||
|
// CLEANUP 1/2
|
||||||
|
// . ┌───────────────────┐
|
||||||
|
// . │ widest shape here │
|
||||||
|
// . └───────────────────┘
|
||||||
|
// . ┌─────────────┐
|
||||||
|
// . │ x │
|
||||||
|
// . └─────────────┘
|
||||||
|
// . long label ├─mr──┤ remove margin we added
|
||||||
|
// . ┌─────────┐
|
||||||
|
// . │ y │ outsider
|
||||||
|
// . └─────────┘
|
||||||
|
// . ├───mr────┤ remove margin we added
|
||||||
|
// CLEANUP 2/2
|
||||||
|
// . ┌───────────────────┐
|
||||||
|
// . │ widest shape here │
|
||||||
|
// . └───────────────────┘
|
||||||
|
// . ┌───────────────────┐
|
||||||
|
// . │ x │
|
||||||
|
// . └───────────────────┘
|
||||||
|
// . long label ├─mr──┤ we removed too much so add back margin we subtracted, then subtract new margin
|
||||||
|
// . ┌─────────┐
|
||||||
|
// . │ y │ outsider
|
||||||
|
// . └─────────┘
|
||||||
|
// . ├───mr────┤ margin.right is still needed
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
for _, o := range gd.objects {
|
||||||
|
m, has := margins[o]
|
||||||
|
if !has {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
dy := m.Top + m.Bottom
|
||||||
|
dx := m.Left + m.Right
|
||||||
|
o.Height -= dy
|
||||||
|
o.Width -= dx
|
||||||
|
|
||||||
|
// less margin may be needed if layout grew the object
|
||||||
|
// compute the new margin after removing the old margin we added
|
||||||
|
margin := o.GetMargin()
|
||||||
|
marginX := margin.Left + margin.Right
|
||||||
|
marginY := margin.Top + margin.Bottom
|
||||||
|
if marginX < dx {
|
||||||
|
// layout grew width and now we need less of a margin (but we subtracted too much)
|
||||||
|
// add back dx and subtract the new amount
|
||||||
|
o.Width += dx - marginX
|
||||||
|
}
|
||||||
|
if marginY < dy {
|
||||||
|
o.Height += dy - marginY
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.Root.IsGridDiagram() {
|
if margin.Left > 0 || margin.Top > 0 {
|
||||||
gd, exists := gridDiagrams[graph.Root.AbsID()]
|
o.MoveWithDescendants(margin.Left, margin.Top)
|
||||||
if exists {
|
|
||||||
gd.cleanup(graph.Root, graph)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, obj := range graph.Objects {
|
|
||||||
restore(obj)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
460
d2layouts/d2layouts.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,23 @@ import (
|
||||||
|
|
||||||
const pad = 20
|
const pad = 20
|
||||||
|
|
||||||
|
type set map[string]struct{}
|
||||||
|
|
||||||
|
var HorizontalCenterNears = set{
|
||||||
|
"center-left": {},
|
||||||
|
"center-right": {},
|
||||||
|
}
|
||||||
|
var VerticalCenterNears = set{
|
||||||
|
"top-center": {},
|
||||||
|
"bottom-center": {},
|
||||||
|
}
|
||||||
|
var NonCenterNears = set{
|
||||||
|
"top-left": {},
|
||||||
|
"top-right": {},
|
||||||
|
"bottom-left": {},
|
||||||
|
"bottom-right": {},
|
||||||
|
}
|
||||||
|
|
||||||
// Layout finds the shapes which are assigned constant near keywords and places them.
|
// Layout finds the shapes which are assigned constant near keywords and places them.
|
||||||
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
|
func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph.Graph) error {
|
||||||
if len(constantNearGraphs) == 0 {
|
if len(constantNearGraphs) == 0 {
|
||||||
|
|
@ -30,10 +47,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph
|
||||||
// Imagine the graph has two long texts, one at top center and one at top left.
|
// Imagine the graph has two long texts, one at top center and one at top left.
|
||||||
// Top left should go left enough to not collide with center.
|
// Top left should go left enough to not collide with center.
|
||||||
// So place the center ones first, then the later ones will consider them for bounding box
|
// So place the center ones first, then the later ones will consider them for bounding box
|
||||||
for _, processCenters := range []bool{true, false} {
|
for _, currentSet := range []set{VerticalCenterNears, HorizontalCenterNears, NonCenterNears} {
|
||||||
for _, tempGraph := range constantNearGraphs {
|
for _, tempGraph := range constantNearGraphs {
|
||||||
obj := tempGraph.Root.ChildrenArray[0]
|
obj := tempGraph.Root.ChildrenArray[0]
|
||||||
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") {
|
_, in := currentSet[d2graph.Key(obj.NearKey)[0]]
|
||||||
|
if in {
|
||||||
prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
|
prevX, prevY := obj.TopLeft.X, obj.TopLeft.Y
|
||||||
obj.TopLeft = geo.NewPoint(place(obj))
|
obj.TopLeft = geo.NewPoint(place(obj))
|
||||||
dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
|
dx, dy := obj.TopLeft.X-prevX, obj.TopLeft.Y-prevY
|
||||||
|
|
@ -56,7 +74,8 @@ func Layout(ctx context.Context, g *d2graph.Graph, constantNearGraphs []*d2graph
|
||||||
}
|
}
|
||||||
for _, tempGraph := range constantNearGraphs {
|
for _, tempGraph := range constantNearGraphs {
|
||||||
obj := tempGraph.Root.ChildrenArray[0]
|
obj := tempGraph.Root.ChildrenArray[0]
|
||||||
if processCenters == strings.Contains(d2graph.Key(obj.NearKey)[0], "-center") {
|
_, in := currentSet[d2graph.Key(obj.NearKey)[0]]
|
||||||
|
if in {
|
||||||
// The z-index for constant nears does not matter, as it will not collide
|
// The z-index for constant nears does not matter, as it will not collide
|
||||||
g.Objects = append(g.Objects, tempGraph.Objects...)
|
g.Objects = append(g.Objects, tempGraph.Objects...)
|
||||||
if obj.Parent.Children == nil {
|
if obj.Parent.Children == nil {
|
||||||
|
|
@ -83,28 +102,20 @@ func place(obj *d2graph.Object) (float64, float64) {
|
||||||
switch nearKeyStr {
|
switch nearKeyStr {
|
||||||
case "top-left":
|
case "top-left":
|
||||||
x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
|
x, y = tl.X-obj.Width-pad, tl.Y-obj.Height-pad
|
||||||
break
|
|
||||||
case "top-center":
|
case "top-center":
|
||||||
x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
|
x, y = tl.X+w/2-obj.Width/2, tl.Y-obj.Height-pad
|
||||||
break
|
|
||||||
case "top-right":
|
case "top-right":
|
||||||
x, y = br.X+pad, tl.Y-obj.Height-pad
|
x, y = br.X+pad, tl.Y-obj.Height-pad
|
||||||
break
|
|
||||||
case "center-left":
|
case "center-left":
|
||||||
x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
|
x, y = tl.X-obj.Width-pad, tl.Y+h/2-obj.Height/2
|
||||||
break
|
|
||||||
case "center-right":
|
case "center-right":
|
||||||
x, y = br.X+pad, tl.Y+h/2-obj.Height/2
|
x, y = br.X+pad, tl.Y+h/2-obj.Height/2
|
||||||
break
|
|
||||||
case "bottom-left":
|
case "bottom-left":
|
||||||
x, y = tl.X-obj.Width-pad, br.Y+pad
|
x, y = tl.X-obj.Width-pad, br.Y+pad
|
||||||
break
|
|
||||||
case "bottom-center":
|
case "bottom-center":
|
||||||
x, y = br.X-w/2-obj.Width/2, br.Y+pad
|
x, y = br.X-w/2-obj.Width/2, br.Y+pad
|
||||||
break
|
|
||||||
case "bottom-right":
|
case "bottom-right":
|
||||||
x, y = br.X+pad, br.Y+pad
|
x, y = br.X+pad, br.Y+pad
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
|
if obj.LabelPosition != nil && !strings.Contains(*obj.LabelPosition, "INSIDE") {
|
||||||
|
|
@ -134,28 +145,6 @@ func place(obj *d2graph.Object) (float64, float64) {
|
||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithoutConstantNears plucks out the graph objects which have "near" set to a constant value
|
|
||||||
// This is to be called before layout engines so they don't take part in regular positioning
|
|
||||||
func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (constantNearGraphs []*d2graph.Graph) {
|
|
||||||
for i := 0; i < len(g.Objects); i++ {
|
|
||||||
obj := g.Objects[i]
|
|
||||||
if obj.NearKey == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
|
|
||||||
if isKey {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
|
|
||||||
if isConst {
|
|
||||||
tempGraph := g.ExtractAsNestedGraph(obj)
|
|
||||||
constantNearGraphs = append(constantNearGraphs, tempGraph)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return constantNearGraphs
|
|
||||||
}
|
|
||||||
|
|
||||||
// boundingBox gets the center of the graph as defined by shapes
|
// boundingBox gets the center of the graph as defined by shapes
|
||||||
// The bounds taking into consideration only shapes gives more of a feeling of true center
|
// The bounds taking into consideration only shapes gives more of a feeling of true center
|
||||||
// It differs from d2target.BoundingBox which needs to include every visible thing
|
// It differs from d2target.BoundingBox which needs to include every visible thing
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package d2sequence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
@ -15,149 +14,24 @@ import (
|
||||||
|
|
||||||
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
|
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
|
||||||
//
|
//
|
||||||
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
|
// 1. Run layout on sequence diagrams
|
||||||
// 2. Construct a sequence diagram from all descendant objects and edges
|
// 2. Set the resulting dimensions to the main graph shape
|
||||||
// 3. Remove those objects and edges from the main graph
|
|
||||||
// 4. Run layout on sequence diagrams
|
|
||||||
// 5. Set the resulting dimensions to the main graph shape
|
|
||||||
// 6. Run core layouts (still without sequence diagram innards)
|
|
||||||
// 7. Put back sequence diagram innards in correct location
|
|
||||||
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
|
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
|
||||||
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g)
|
// used in layout code
|
||||||
|
g.Root.Shape.Value = d2target.ShapeSequenceDiagram
|
||||||
|
|
||||||
|
sd, err := layoutSequenceDiagram(g, g.Root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
g.Root.Box = geo.NewBox(nil, sd.getWidth()+GROUP_CONTAINER_PADDING*2, sd.getHeight()+GROUP_CONTAINER_PADDING*2)
|
||||||
|
|
||||||
if g.Root.IsSequenceDiagram() {
|
|
||||||
// the sequence diagram is the only layout engine if the whole diagram is
|
// the sequence diagram is the only layout engine if the whole diagram is
|
||||||
// shape: sequence_diagram
|
// shape: sequence_diagram
|
||||||
g.Root.TopLeft = geo.NewPoint(0, 0)
|
g.Root.TopLeft = geo.NewPoint(0, 0)
|
||||||
} else if err := layout(ctx, g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
|
obj := g.Root
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) {
|
|
||||||
objectsToRemove := make(map[*d2graph.Object]struct{})
|
|
||||||
edgesToRemove := make(map[*d2graph.Edge]struct{})
|
|
||||||
sequenceDiagrams := make(map[string]*sequenceDiagram)
|
|
||||||
|
|
||||||
if len(g.Objects) > 0 {
|
|
||||||
queue := make([]*d2graph.Object, 1, len(g.Objects))
|
|
||||||
queue[0] = g.Root
|
|
||||||
for len(queue) > 0 {
|
|
||||||
obj := queue[0]
|
|
||||||
queue = queue[1:]
|
|
||||||
if len(obj.ChildrenArray) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if obj.Shape.Value != d2target.ShapeSequenceDiagram {
|
|
||||||
queue = append(queue, obj.ChildrenArray...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sd, err := layoutSequenceDiagram(g, obj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
obj.Children = make(map[string]*d2graph.Object)
|
|
||||||
obj.ChildrenArray = nil
|
|
||||||
obj.Box = geo.NewBox(nil, sd.getWidth()+GROUP_CONTAINER_PADDING*2, sd.getHeight()+GROUP_CONTAINER_PADDING*2)
|
|
||||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
|
||||||
sequenceDiagrams[obj.AbsID()] = sd
|
|
||||||
|
|
||||||
for _, edge := range sd.messages {
|
|
||||||
edgesToRemove[edge] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, obj := range sd.actors {
|
|
||||||
objectsToRemove[obj] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, obj := range sd.notes {
|
|
||||||
objectsToRemove[obj] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, obj := range sd.groups {
|
|
||||||
objectsToRemove[obj] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, obj := range sd.spans {
|
|
||||||
objectsToRemove[obj] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove)
|
|
||||||
g.Edges = layoutEdges
|
|
||||||
layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove)
|
|
||||||
// TODO this isn't a proper deletion because the objects still appear as children of the object
|
|
||||||
g.Objects = layoutObjects
|
|
||||||
|
|
||||||
return sequenceDiagrams, objectOrder, edgeOrder, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants
|
|
||||||
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
|
|
||||||
var edges []*d2graph.Edge
|
|
||||||
for _, edge := range g.Edges {
|
|
||||||
// both Src and Dst must be inside the sequence diagram
|
|
||||||
if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) {
|
|
||||||
edges = append(edges, edge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = sd.layout()
|
|
||||||
return sd, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) {
|
|
||||||
edgeOrder := make(map[string]int)
|
|
||||||
layoutEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(toRemove))
|
|
||||||
|
|
||||||
for i, edge := range g.Edges {
|
|
||||||
edgeOrder[edge.AbsID()] = i
|
|
||||||
if _, exists := toRemove[edge]; !exists {
|
|
||||||
layoutEdges = append(layoutEdges, edge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return layoutEdges, edgeOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLayoutObjects(g *d2graph.Graph, toRemove map[*d2graph.Object]struct{}) ([]*d2graph.Object, map[string]int) {
|
|
||||||
objectOrder := make(map[string]int)
|
|
||||||
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
|
|
||||||
for i, obj := range g.Objects {
|
|
||||||
objectOrder[obj.AbsID()] = i
|
|
||||||
if _, exists := toRemove[obj]; !exists {
|
|
||||||
layoutObjects = append(layoutObjects, obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return layoutObjects, objectOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup restores the graph after the core layout engine finishes
|
|
||||||
// - translating the sequence diagram to its position placed by the core layout engine
|
|
||||||
// - restore the children of the sequence diagram graph object
|
|
||||||
// - adds the sequence diagram edges (messages) back to the graph
|
|
||||||
// - adds the sequence diagram lifelines to the graph edges
|
|
||||||
// - adds the sequence diagram descendants back to the graph objects
|
|
||||||
// - sorts edges and objects to their original graph order
|
|
||||||
func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, objectsOrder, edgesOrder map[string]int) {
|
|
||||||
var objects []*d2graph.Object
|
|
||||||
if g.Root.IsSequenceDiagram() {
|
|
||||||
objects = []*d2graph.Object{g.Root}
|
|
||||||
} else {
|
|
||||||
objects = g.Objects
|
|
||||||
}
|
|
||||||
for _, obj := range objects {
|
|
||||||
sd, exists := sequenceDiagrams[obj.AbsID()]
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||||
|
|
||||||
// shift the sequence diagrams as they are always placed at (0, 0) with some padding
|
// shift the sequence diagrams as they are always placed at (0, 0) with some padding
|
||||||
|
|
@ -181,29 +55,25 @@ func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, obj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Edges = append(g.Edges, sd.messages...)
|
|
||||||
g.Edges = append(g.Edges, sd.lifelines...)
|
g.Edges = append(g.Edges, sd.lifelines...)
|
||||||
g.Objects = append(g.Objects, sd.actors...)
|
|
||||||
g.Objects = append(g.Objects, sd.notes...)
|
|
||||||
g.Objects = append(g.Objects, sd.groups...)
|
|
||||||
g.Objects = append(g.Objects, sd.spans...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no new objects, so just keep the same position
|
return nil
|
||||||
sort.SliceStable(g.Objects, func(i, j int) bool {
|
}
|
||||||
return objectsOrder[g.Objects[i].AbsID()] < objectsOrder[g.Objects[j].AbsID()]
|
|
||||||
})
|
// layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants
|
||||||
|
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
|
||||||
// sequence diagrams add lifelines, and they must be the last ones in this slice
|
var edges []*d2graph.Edge
|
||||||
sort.SliceStable(g.Edges, func(i, j int) bool {
|
for _, edge := range g.Edges {
|
||||||
iOrder, iExists := edgesOrder[g.Edges[i].AbsID()]
|
// both Src and Dst must be inside the sequence diagram
|
||||||
jOrder, jExists := edgesOrder[g.Edges[j].AbsID()]
|
if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) {
|
||||||
if iExists && jExists {
|
edges = append(edges, edge)
|
||||||
return iOrder < jOrder
|
}
|
||||||
} else if iExists && !jExists {
|
}
|
||||||
return true
|
|
||||||
}
|
sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
|
||||||
// either both don't exist or i doesn't exist and j exists
|
if err != nil {
|
||||||
return false
|
return nil, err
|
||||||
})
|
}
|
||||||
|
err = sd.layout()
|
||||||
|
return sd, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
@ -411,6 +412,25 @@ func (sd *sequenceDiagram) addLifelineEdges() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsLifelineEnd(obj *d2graph.Object) bool {
|
||||||
|
// lifeline ends only have ID and no graph parent or box set
|
||||||
|
if obj.Graph != nil || obj.Parent != nil || obj.Box != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.Contains(obj.ID, "-lifeline-end-") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.Split(obj.ID, "-lifeline-end-")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
hash := parts[len(parts)-1]
|
||||||
|
actorID := strings.Join(parts[:len(parts)-1], "-lifeline-end-")
|
||||||
|
if strconv.Itoa(go2.StringToIntHash(actorID+"-lifeline-end")) == hash {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (sd *sequenceDiagram) placeNotes() {
|
func (sd *sequenceDiagram) placeNotes() {
|
||||||
rankToX := make(map[int]float64)
|
rankToX := make(map[int]float64)
|
||||||
for _, actor := range sd.actors {
|
for _, actor := range sd.actors {
|
||||||
|
|
|
||||||
23
d2lib/d2.go
|
|
@ -10,10 +10,8 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2compiler"
|
"oss.terrastruct.com/d2/d2compiler"
|
||||||
"oss.terrastruct.com/d2/d2exporter"
|
"oss.terrastruct.com/d2/d2exporter"
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
|
"oss.terrastruct.com/d2/d2layouts"
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2grid"
|
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2near"
|
|
||||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
|
|
@ -84,23 +82,8 @@ func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
constantNearGraphs := d2near.WithoutConstantNears(ctx, g)
|
graphInfo := d2layouts.NestedGraphInfo(g.Root)
|
||||||
|
err = d2layouts.LayoutNested(ctx, g, graphInfo, coreLayout)
|
||||||
layoutWithGrids := d2grid.Layout(ctx, g, coreLayout)
|
|
||||||
|
|
||||||
// run core layout for constantNears
|
|
||||||
for _, tempGraph := range constantNearGraphs {
|
|
||||||
if err = layoutWithGrids(ctx, tempGraph); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d2sequence.Layout(ctx, g, layoutWithGrids)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d2near.Layout(ctx, g, constantNearGraphs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
g, err = recompile(g.AST)
|
g, err = recompile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +112,7 @@ func Set(g *d2graph.Graph, boardPath []string, key string, tag, value *string) (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey, dstKey *string) (_ *d2graph.Graph, err error) {
|
func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey, dstKey *string) (_ *d2graph.Graph, err error) {
|
||||||
|
|
@ -271,7 +271,7 @@ func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathFromScopeKey(g *d2graph.Graph, key *d2ast.Key, scopeak []string) ([]*d2ast.StringBox, error) {
|
func pathFromScopeKey(g *d2graph.Graph, key *d2ast.Key, scopeak []string) ([]*d2ast.StringBox, error) {
|
||||||
|
|
@ -303,13 +303,15 @@ func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Objec
|
||||||
return pathFromScopeKey(g, key, scopeak)
|
return pathFromScopeKey(g, key, scopeak)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recompile(ast *d2ast.Map) (*d2graph.Graph, error) {
|
func recompile(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||||
s := d2format.Format(ast)
|
s := d2format.Format(g.AST)
|
||||||
g, _, err := d2compiler.Compile(ast.Range.Path, strings.NewReader(s), nil)
|
g2, _, err := d2compiler.Compile(g.AST.Range.Path, strings.NewReader(s), &d2compiler.CompileOptions{
|
||||||
|
FS: g.FS,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err)
|
return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err)
|
||||||
}
|
}
|
||||||
return g, nil
|
return g2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO merge flat styles
|
// TODO merge flat styles
|
||||||
|
|
@ -451,7 +453,9 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ir, err := d2ir.Compile(g.AST, nil)
|
ir, err := d2ir.Compile(g.AST, &d2ir.CompileOptions{
|
||||||
|
FS: g.FS,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -489,7 +493,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
||||||
noVal2 := &tmp2
|
noVal2 := &tmp2
|
||||||
noVal1.Value = d2ast.ValueBox{}
|
noVal1.Value = d2ast.ValueBox{}
|
||||||
noVal2.Value = d2ast.ValueBox{}
|
noVal2.Value = d2ast.ValueBox{}
|
||||||
if noVal1.Equals(noVal2) {
|
if noVal1.D2OracleEquals(noVal2) {
|
||||||
ref.MapKey.Value = mk.Value
|
ref.MapKey.Value = mk.Value
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -555,6 +559,12 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
||||||
|
|
||||||
if reserved {
|
if reserved {
|
||||||
inlined := func(s *d2graph.Scalar) bool {
|
inlined := func(s *d2graph.Scalar) bool {
|
||||||
|
if s != nil && s.MapKey != nil {
|
||||||
|
// The value was set outside of what's writeable
|
||||||
|
if s.MapKey.Range.Path != baseAST.Range.Path {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey)
|
return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey)
|
||||||
}
|
}
|
||||||
reservedIndex := toSkip - 1
|
reservedIndex := toSkip - 1
|
||||||
|
|
@ -766,7 +776,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
||||||
|
|
||||||
func appendUniqueMapKey(m *d2ast.Map, mk *d2ast.Key) {
|
func appendUniqueMapKey(m *d2ast.Map, mk *d2ast.Key) {
|
||||||
for _, n := range m.Nodes {
|
for _, n := range m.Nodes {
|
||||||
if n.MapKey != nil && n.MapKey.Equals(mk) {
|
if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -797,11 +807,6 @@ func appendMapKey(m *d2ast.Map, mk *d2ast.Key) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func prependMapKey(m *d2ast.Map, mk *d2ast.Key) {
|
|
||||||
appendMapKey(m, mk)
|
|
||||||
m.Nodes = append([]d2ast.MapNodeBox{m.Nodes[len(m.Nodes)-1]}, m.Nodes[:len(m.Nodes)-1]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) {
|
func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) {
|
||||||
defer xdefer.Errorf(&err, "failed to delete %#v", key)
|
defer xdefer.Errorf(&err, "failed to delete %#v", key)
|
||||||
|
|
||||||
|
|
@ -890,19 +895,20 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prependMapKey(baseAST, mk)
|
// NOTE: it only needs to be after the last ref, but perhaps simplest and cleanest to append all nulls at the end
|
||||||
|
appendMapKey(baseAST, mk)
|
||||||
}
|
}
|
||||||
if len(boardPath) > 0 {
|
if len(boardPath) > 0 {
|
||||||
replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
|
replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
|
||||||
if !replaced {
|
if !replaced {
|
||||||
return nil, fmt.Errorf("board %v AST not found", boardPath)
|
return nil, fmt.Errorf("board %v AST not found", boardPath)
|
||||||
}
|
}
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
return recompile(boardG.AST)
|
return recompile(boardG)
|
||||||
}
|
}
|
||||||
|
|
||||||
prevG, _ := recompile(boardG.AST)
|
prevG, _ := recompile(boardG)
|
||||||
|
|
||||||
boardG, err = renameConflictsToParent(boardG, mk.Key)
|
boardG, err = renameConflictsToParent(boardG, mk.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -931,7 +937,7 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prependMapKey(baseAST, mk)
|
appendMapKey(baseAST, mk)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(boardPath) > 0 {
|
if len(boardPath) > 0 {
|
||||||
|
|
@ -939,10 +945,10 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
||||||
if !replaced {
|
if !replaced {
|
||||||
return nil, fmt.Errorf("board %v AST not found", boardPath)
|
return nil, fmt.Errorf("board %v AST not found", boardPath)
|
||||||
}
|
}
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
return recompile(boardG.AST)
|
return recompile(boardG)
|
||||||
}
|
}
|
||||||
|
|
||||||
func bumpChildrenUnderscores(m *d2ast.Map) {
|
func bumpChildrenUnderscores(m *d2ast.Map) {
|
||||||
|
|
@ -1182,7 +1188,7 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
|
||||||
if err := deleteEdgeField(g, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()); err != nil {
|
if err := deleteEdgeField(g, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
isStyleKey := false
|
isStyleKey := false
|
||||||
|
|
@ -1221,7 +1227,7 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteMapField(m *d2ast.Map, field string) {
|
func deleteMapField(m *d2ast.Map, field string) {
|
||||||
|
|
@ -1285,7 +1291,7 @@ func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error {
|
||||||
copy(tmpNodes, ref.Scope.Nodes)
|
copy(tmpNodes, ref.Scope.Nodes)
|
||||||
// If I delete this, will the object still exist?
|
// If I delete this, will the object still exist?
|
||||||
deleteFromMap(ref.Scope, ref.MapKey)
|
deleteFromMap(ref.Scope, ref.MapKey)
|
||||||
g2, err := recompile(g.AST)
|
g2, err := recompile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -1465,7 +1471,7 @@ func ensureNode(g *d2graph.Graph, excludedEdges []*d2ast.Edge, scopeObj *d2graph
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, n := range scope.Nodes {
|
for _, n := range scope.Nodes {
|
||||||
if n.MapKey != nil && n.MapKey.Equals(mk) {
|
if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1605,10 +1611,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
||||||
ref.MapKey.Edges[ref.MapKeyEdgeIndex].SrcArrow = mk2.Edges[0].SrcArrow
|
ref.MapKey.Edges[ref.MapKeyEdgeIndex].SrcArrow = mk2.Edges[0].SrcArrow
|
||||||
ref.MapKey.Edges[ref.MapKeyEdgeIndex].DstArrow = mk2.Edges[0].DstArrow
|
ref.MapKey.Edges[ref.MapKeyEdgeIndex].DstArrow = mk2.Edges[0].DstArrow
|
||||||
}
|
}
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
prevG, _ := recompile(boardG.AST)
|
prevG, _ := recompile(boardG)
|
||||||
|
|
||||||
ak := d2graph.Key(mk.Key)
|
ak := d2graph.Key(mk.Key)
|
||||||
ak2 := d2graph.Key(mk2.Key)
|
ak2 := d2graph.Key(mk2.Key)
|
||||||
|
|
@ -1916,7 +1922,7 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
||||||
ref.Key.Path = ref.Key.Path[ref.KeyPathIndex:]
|
ref.Key.Path = ref.Key.Path[ref.KeyPathIndex:]
|
||||||
exists := false
|
exists := false
|
||||||
for _, n := range toScope.Nodes {
|
for _, n := range toScope.Nodes {
|
||||||
if n.MapKey != nil && n.MapKey != ref.MapKey && n.MapKey.Equals(ref.MapKey) {
|
if n.MapKey != nil && n.MapKey != ref.MapKey && n.MapKey.D2OracleEquals(ref.MapKey) {
|
||||||
exists = true
|
exists = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2026,10 +2032,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
||||||
if !replaced {
|
if !replaced {
|
||||||
return nil, fmt.Errorf("board %v AST not found", boardPath)
|
return nil, fmt.Errorf("board %v AST not found", boardPath)
|
||||||
}
|
}
|
||||||
return recompile(g.AST)
|
return recompile(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
return recompile(boardG.AST)
|
return recompile(boardG)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterReserved takes a Value and splits it into 2
|
// filterReserved takes a Value and splits it into 2
|
||||||
|
|
@ -2141,7 +2147,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpG, _ := recompile(prevG.AST)
|
tmpG, _ := recompile(prevG)
|
||||||
appendMapKey(tmpG.AST, valueMK)
|
appendMapKey(tmpG.AST, valueMK)
|
||||||
if to == nil {
|
if to == nil {
|
||||||
deltas, err := DeleteIDDeltas(tmpG, nil, *from)
|
deltas, err := DeleteIDDeltas(tmpG, nil, *from)
|
||||||
|
|
@ -2186,7 +2192,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpG, _ := recompile(prevG.AST)
|
tmpG, _ := recompile(prevG)
|
||||||
appendMapKey(tmpG.AST, valueMK)
|
appendMapKey(tmpG.AST, valueMK)
|
||||||
if to == nil {
|
if to == nil {
|
||||||
deltas, err := DeleteIDDeltas(tmpG, nil, *from)
|
deltas, err := DeleteIDDeltas(tmpG, nil, *from)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ package d2oracle_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -732,6 +735,7 @@ func TestSet(t *testing.T) {
|
||||||
boardPath []string
|
boardPath []string
|
||||||
name string
|
name string
|
||||||
text string
|
text string
|
||||||
|
fsTexts map[string]string
|
||||||
key string
|
key string
|
||||||
tag *string
|
tag *string
|
||||||
value *string
|
value *string
|
||||||
|
|
@ -1984,6 +1988,76 @@ scenarios: {
|
||||||
c -> d
|
c -> d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "import/1",
|
||||||
|
|
||||||
|
text: `x: {
|
||||||
|
...@meow.x
|
||||||
|
y
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fsTexts: map[string]string{
|
||||||
|
"meow": `x: {
|
||||||
|
style.fill: blue
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
key: `x.style.stroke`,
|
||||||
|
value: go2.Pointer(`red`),
|
||||||
|
exp: `x: {
|
||||||
|
...@meow.x
|
||||||
|
y
|
||||||
|
style.stroke: red
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "import/2",
|
||||||
|
|
||||||
|
text: `x: {
|
||||||
|
...@meow.x
|
||||||
|
y
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fsTexts: map[string]string{
|
||||||
|
"meow": `x: {
|
||||||
|
style.fill: blue
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
key: `x.style.fill`,
|
||||||
|
value: go2.Pointer(`red`),
|
||||||
|
exp: `x: {
|
||||||
|
...@meow.x
|
||||||
|
y
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "import/3",
|
||||||
|
|
||||||
|
text: `x: {
|
||||||
|
...@meow.x
|
||||||
|
y
|
||||||
|
style.fill: red
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fsTexts: map[string]string{
|
||||||
|
"meow": `x: {
|
||||||
|
style.fill: blue
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
key: `x.style.fill`,
|
||||||
|
value: go2.Pointer(`yellow`),
|
||||||
|
exp: `x: {
|
||||||
|
...@meow.x
|
||||||
|
y
|
||||||
|
style.fill: yellow
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1995,6 +2069,7 @@ scenarios: {
|
||||||
|
|
||||||
et := editTest{
|
et := editTest{
|
||||||
text: tc.text,
|
text: tc.text,
|
||||||
|
fsTexts: tc.fsTexts,
|
||||||
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||||
return d2oracle.Set(g, tc.boardPath, tc.key, tc.tag, tc.value)
|
return d2oracle.Set(g, tc.boardPath, tc.key, tc.tag, tc.value)
|
||||||
},
|
},
|
||||||
|
|
@ -2412,6 +2487,7 @@ func TestRename(t *testing.T) {
|
||||||
boardPath []string
|
boardPath []string
|
||||||
|
|
||||||
text string
|
text string
|
||||||
|
fsTexts map[string]string
|
||||||
key string
|
key string
|
||||||
newName string
|
newName string
|
||||||
|
|
||||||
|
|
@ -2923,6 +2999,7 @@ scenarios: {
|
||||||
|
|
||||||
et := editTest{
|
et := editTest{
|
||||||
text: tc.text,
|
text: tc.text,
|
||||||
|
fsTexts: tc.fsTexts,
|
||||||
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||||
objectsBefore := len(g.Objects)
|
objectsBefore := len(g.Objects)
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -2956,6 +3033,7 @@ func TestMove(t *testing.T) {
|
||||||
boardPath []string
|
boardPath []string
|
||||||
|
|
||||||
text string
|
text string
|
||||||
|
fsTexts map[string]string
|
||||||
key string
|
key string
|
||||||
newKey string
|
newKey string
|
||||||
includeDescendants bool
|
includeDescendants bool
|
||||||
|
|
@ -5191,6 +5269,7 @@ scenarios: {
|
||||||
|
|
||||||
et := editTest{
|
et := editTest{
|
||||||
text: tc.text,
|
text: tc.text,
|
||||||
|
fsTexts: tc.fsTexts,
|
||||||
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||||
objectsBefore := len(g.Objects)
|
objectsBefore := len(g.Objects)
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -5222,6 +5301,7 @@ func TestDelete(t *testing.T) {
|
||||||
boardPath []string
|
boardPath []string
|
||||||
|
|
||||||
text string
|
text string
|
||||||
|
fsTexts map[string]string
|
||||||
key string
|
key string
|
||||||
|
|
||||||
expErr string
|
expErr string
|
||||||
|
|
@ -6934,10 +7014,9 @@ scenarios: {
|
||||||
|
|
||||||
scenarios: {
|
scenarios: {
|
||||||
x: {
|
x: {
|
||||||
a: null
|
|
||||||
|
|
||||||
b
|
b
|
||||||
c
|
c
|
||||||
|
a: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
@ -6961,10 +7040,9 @@ scenarios: {
|
||||||
|
|
||||||
scenarios: {
|
scenarios: {
|
||||||
x: {
|
x: {
|
||||||
(a -> b)[0]: null
|
|
||||||
|
|
||||||
b
|
b
|
||||||
c
|
c
|
||||||
|
(a -> b)[0]: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
@ -6978,6 +7056,7 @@ scenarios: {
|
||||||
|
|
||||||
et := editTest{
|
et := editTest{
|
||||||
text: tc.text,
|
text: tc.text,
|
||||||
|
fsTexts: tc.fsTexts,
|
||||||
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||||
return d2oracle.Delete(g, tc.boardPath, tc.key)
|
return d2oracle.Delete(g, tc.boardPath, tc.key)
|
||||||
},
|
},
|
||||||
|
|
@ -6993,6 +7072,7 @@ scenarios: {
|
||||||
|
|
||||||
type editTest struct {
|
type editTest struct {
|
||||||
text string
|
text string
|
||||||
|
fsTexts map[string]string
|
||||||
testFunc func(*d2graph.Graph) (*d2graph.Graph, error)
|
testFunc func(*d2graph.Graph) (*d2graph.Graph, error)
|
||||||
|
|
||||||
exp string
|
exp string
|
||||||
|
|
@ -7002,7 +7082,13 @@ type editTest struct {
|
||||||
|
|
||||||
func (tc editTest) run(t *testing.T) {
|
func (tc editTest) run(t *testing.T) {
|
||||||
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
|
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
|
||||||
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
|
tfs := testFS(make(map[string]*testF))
|
||||||
|
for name, text := range tc.fsTexts {
|
||||||
|
tfs[name] = &testF{content: text}
|
||||||
|
}
|
||||||
|
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), &d2compiler.CompileOptions{
|
||||||
|
FS: tfs,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -8360,3 +8446,38 @@ scenarios: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testF struct {
|
||||||
|
content string
|
||||||
|
readIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *testF) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *testF) Read(p []byte) (int, error) {
|
||||||
|
data := []byte(tf.content)
|
||||||
|
if tf.readIndex >= len(data) {
|
||||||
|
tf.readIndex = 0
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
readBytes := copy(p, data[tf.readIndex:])
|
||||||
|
tf.readIndex += readBytes
|
||||||
|
return readBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *testF) Stat() (os.FileInfo, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type testFS map[string]*testF
|
||||||
|
|
||||||
|
func (tfs testFS) Open(name string) (fs.File, error) {
|
||||||
|
for k := range tfs {
|
||||||
|
if strings.HasSuffix(name[:len(name)-3], k) {
|
||||||
|
return tfs[k], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,29 @@ func GetBoardGraph(g *d2graph.Graph, boardPath []string) *d2graph.Graph {
|
||||||
}
|
}
|
||||||
for i, b := range g.Layers {
|
for i, b := range g.Layers {
|
||||||
if b.Name == boardPath[0] {
|
if b.Name == boardPath[0] {
|
||||||
return GetBoardGraph(g.Layers[i], boardPath[1:])
|
g2 := GetBoardGraph(g.Layers[i], boardPath[1:])
|
||||||
|
if g2 != nil {
|
||||||
|
g2.FS = g.FS
|
||||||
|
}
|
||||||
|
return g2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, b := range g.Scenarios {
|
for i, b := range g.Scenarios {
|
||||||
if b.Name == boardPath[0] {
|
if b.Name == boardPath[0] {
|
||||||
return GetBoardGraph(g.Scenarios[i], boardPath[1:])
|
g2 := GetBoardGraph(g.Scenarios[i], boardPath[1:])
|
||||||
|
if g2 != nil {
|
||||||
|
g2.FS = g.FS
|
||||||
|
}
|
||||||
|
return g2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, b := range g.Steps {
|
for i, b := range g.Steps {
|
||||||
if b.Name == boardPath[0] {
|
if b.Name == boardPath[0] {
|
||||||
return GetBoardGraph(g.Steps[i], boardPath[1:])
|
g2 := GetBoardGraph(g.Steps[i], boardPath[1:])
|
||||||
|
if g2 != nil {
|
||||||
|
g2.FS = g.FS
|
||||||
|
}
|
||||||
|
return g2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ type parser struct {
|
||||||
|
|
||||||
// TODO: rename to Error and make existing Error a private type errorWithRange
|
// TODO: rename to Error and make existing Error a private type errorWithRange
|
||||||
type ParseError struct {
|
type ParseError struct {
|
||||||
|
// Errors from globs need to be deduplicated
|
||||||
|
ErrorsLookup map[d2ast.Error]struct{} `json:"-"`
|
||||||
Errors []d2ast.Error `json:"errs"`
|
Errors []d2ast.Error `json:"errs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -660,16 +662,27 @@ func (p *parser) parseMapKey() (mk *d2ast.Key) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Check for ampersand/@.
|
// Check for not ampersand/@.
|
||||||
r, eof := p.peek()
|
r, eof := p.peek()
|
||||||
if eof {
|
if eof {
|
||||||
return mk
|
return mk
|
||||||
}
|
}
|
||||||
if r != '&' {
|
if r == '!' {
|
||||||
p.rewind()
|
r, eof := p.peek()
|
||||||
|
if eof {
|
||||||
|
return mk
|
||||||
|
}
|
||||||
|
if r == '&' {
|
||||||
|
p.commit()
|
||||||
|
mk.NotAmpersand = true
|
||||||
} else {
|
} else {
|
||||||
|
p.rewind()
|
||||||
|
}
|
||||||
|
} else if r == '&' {
|
||||||
p.commit()
|
p.commit()
|
||||||
mk.Ampersand = true
|
mk.Ampersand = true
|
||||||
|
} else {
|
||||||
|
p.rewind()
|
||||||
}
|
}
|
||||||
|
|
||||||
r, eof = p.peek()
|
r, eof = p.peek()
|
||||||
|
|
@ -1174,6 +1187,7 @@ func (p *parser) parseUnquotedString(inKey bool) (s *d2ast.UnquotedString) {
|
||||||
rawv := rawb.String()
|
rawv := rawb.String()
|
||||||
s.Value = append(s.Value, d2ast.InterpolationBox{String: &sv, StringRaw: &rawv})
|
s.Value = append(s.Value, d2ast.InterpolationBox{String: &sv, StringRaw: &rawv})
|
||||||
sb.Reset()
|
sb.Reset()
|
||||||
|
rawb.Reset()
|
||||||
}
|
}
|
||||||
s.Value = append(s.Value, d2ast.InterpolationBox{Substitution: subst})
|
s.Value = append(s.Value, d2ast.InterpolationBox{Substitution: subst})
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,18 @@ b-
|
||||||
c-
|
c-
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "not-amper",
|
||||||
|
text: `
|
||||||
|
&k: amper
|
||||||
|
!&k: not amper
|
||||||
|
`,
|
||||||
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
||||||
|
assert.Success(t, err)
|
||||||
|
assert.True(t, ast.Nodes[0].MapKey.Ampersand)
|
||||||
|
assert.True(t, ast.Nodes[1].MapKey.NotAmpersand)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "whitespace_range",
|
name: "whitespace_range",
|
||||||
text: ` a -> b -> c `,
|
text: ` a -> b -> c `,
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,6 @@ func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
|
||||||
// 1. It first searches the bundled plugins in the global plugins slice.
|
// 1. It first searches the bundled plugins in the global plugins slice.
|
||||||
// 2. If not found, it then searches each directory in $PATH for a binary with the name
|
// 2. If not found, it then searches each directory in $PATH for a binary with the name
|
||||||
// d2plugin-<name>.
|
// d2plugin-<name>.
|
||||||
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
|
|
||||||
// 3. If such a binary is found, it builds an execPlugin in exec.go
|
// 3. If such a binary is found, it builds an execPlugin in exec.go
|
||||||
// to get a plugin implementation around the binary and returns it.
|
// to get a plugin implementation around the binary and returns it.
|
||||||
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
|
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/lib/font"
|
"oss.terrastruct.com/d2/lib/font"
|
||||||
fontlib "oss.terrastruct.com/d2/lib/font"
|
fontlib "oss.terrastruct.com/d2/lib/font"
|
||||||
|
"oss.terrastruct.com/d2/lib/syncmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FontFamily string
|
type FontFamily string
|
||||||
|
|
@ -44,14 +45,15 @@ func (f Font) GetEncodedSubset(corpus string) string {
|
||||||
|
|
||||||
FontFamiliesMu.Lock()
|
FontFamiliesMu.Lock()
|
||||||
defer FontFamiliesMu.Unlock()
|
defer FontFamiliesMu.Unlock()
|
||||||
fontBuf := make([]byte, len(FontFaces[f]))
|
face := FontFaces.Get(f)
|
||||||
copy(fontBuf, FontFaces[f])
|
fontBuf := make([]byte, len(face))
|
||||||
|
copy(fontBuf, face)
|
||||||
fontBuf = font.UTF8CutFont(fontBuf, uniqueChars)
|
fontBuf = font.UTF8CutFont(fontBuf, uniqueChars)
|
||||||
|
|
||||||
fontBuf, err := fontlib.Sfnt2Woff(fontBuf)
|
fontBuf, err := fontlib.Sfnt2Woff(fontBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If subset fails, return full encoding
|
// If subset fails, return full encoding
|
||||||
return FontEncodings[f]
|
return FontEncodings.Get(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(fontBuf))
|
return fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(fontBuf))
|
||||||
|
|
@ -134,165 +136,197 @@ var fuzzyBubblesBoldBase64 string
|
||||||
//go:embed ttf/*
|
//go:embed ttf/*
|
||||||
var fontFacesFS embed.FS
|
var fontFacesFS embed.FS
|
||||||
|
|
||||||
var FontEncodings map[Font]string
|
var FontEncodings syncmap.SyncMap[Font, string]
|
||||||
var FontFaces map[Font][]byte
|
var FontFaces syncmap.SyncMap[Font, []byte]
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
FontEncodings = map[Font]string{
|
FontEncodings = syncmap.New[Font, string]()
|
||||||
{
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}: sourceSansProRegularBase64,
|
},
|
||||||
{
|
sourceSansProRegularBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}: sourceSansProBoldBase64,
|
},
|
||||||
{
|
sourceSansProBoldBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
}: sourceSansProSemiboldBase64,
|
},
|
||||||
{
|
sourceSansProSemiboldBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
}: sourceSansProItalicBase64,
|
},
|
||||||
{
|
sourceSansProItalicBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}: sourceCodeProRegularBase64,
|
},
|
||||||
{
|
sourceCodeProRegularBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}: sourceCodeProBoldBase64,
|
},
|
||||||
{
|
sourceCodeProBoldBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
}: sourceCodeProSemiboldBase64,
|
},
|
||||||
{
|
sourceCodeProSemiboldBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
}: sourceCodeProItalicBase64,
|
},
|
||||||
{
|
sourceCodeProItalicBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}: fuzzyBubblesRegularBase64,
|
},
|
||||||
{
|
fuzzyBubblesRegularBase64)
|
||||||
|
|
||||||
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
// This font has no italic, so just reuse regular
|
// This font has no italic, so just reuse regular
|
||||||
}: fuzzyBubblesRegularBase64,
|
}, fuzzyBubblesRegularBase64)
|
||||||
{
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}: fuzzyBubblesBoldBase64,
|
}, fuzzyBubblesBoldBase64)
|
||||||
{
|
FontEncodings.Set(
|
||||||
|
Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
// This font has no semibold, so just reuse bold
|
// This font has no semibold, so just reuse bold
|
||||||
}: fuzzyBubblesBoldBase64,
|
}, fuzzyBubblesBoldBase64)
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range FontEncodings {
|
FontEncodings.Range(func(k Font, v string) bool {
|
||||||
FontEncodings[k] = strings.TrimSuffix(v, "\n")
|
FontEncodings.Set(k, strings.TrimSuffix(v, "\n"))
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
FontFaces = syncmap.New[Font, []byte]()
|
||||||
|
|
||||||
FontFaces = map[Font][]byte{}
|
|
||||||
b, err := fontFacesFS.ReadFile("ttf/SourceSansPro-Regular.ttf")
|
b, err := fontFacesFS.ReadFile("ttf/SourceSansPro-Regular.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Regular.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Regular.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Bold.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Bold.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Semibold.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Semibold.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Bold.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Bold.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Semibold.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Semibold.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Regular.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Regular.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}] = b
|
}, b)
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
}] = b
|
}, b)
|
||||||
|
|
||||||
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
|
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}] = b
|
}, b)
|
||||||
FontFaces[Font{
|
FontFaces.Set(Font{
|
||||||
Family: HandDrawn,
|
Family: HandDrawn,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
}] = b
|
}, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
var D2_FONT_TO_FAMILY = map[string]FontFamily{
|
var D2_FONT_TO_FAMILY = map[string]FontFamily{
|
||||||
|
|
@ -301,14 +335,14 @@ var D2_FONT_TO_FAMILY = map[string]FontFamily{
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
|
func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
|
||||||
FontFaces[font] = ttf
|
FontFaces.Set(font, ttf)
|
||||||
|
|
||||||
woff, err := fontlib.Sfnt2Woff(ttf)
|
woff, err := fontlib.Sfnt2Woff(ttf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encode ttf to woff: %v", err)
|
return fmt.Errorf("failed to encode ttf to woff: %v", err)
|
||||||
}
|
}
|
||||||
encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woff))
|
encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woff))
|
||||||
FontEncodings[font] = encodedWoff
|
FontEncodings.Set(font, encodedWoff)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -332,8 +366,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_REGULAR,
|
Style: FONT_STYLE_REGULAR,
|
||||||
}
|
}
|
||||||
FontFaces[regularFont] = FontFaces[fallbackFont]
|
FontFaces.Set(regularFont, FontFaces.Get(fallbackFont))
|
||||||
FontEncodings[regularFont] = FontEncodings[fallbackFont]
|
FontEncodings.Set(regularFont, FontEncodings.Get(fallbackFont))
|
||||||
}
|
}
|
||||||
|
|
||||||
italicFont := Font{
|
italicFont := Font{
|
||||||
|
|
@ -350,8 +384,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_ITALIC,
|
Style: FONT_STYLE_ITALIC,
|
||||||
}
|
}
|
||||||
FontFaces[italicFont] = FontFaces[fallbackFont]
|
FontFaces.Set(italicFont, FontFaces.Get(fallbackFont))
|
||||||
FontEncodings[italicFont] = FontEncodings[fallbackFont]
|
FontEncodings.Set(italicFont, FontEncodings.Get(fallbackFont))
|
||||||
}
|
}
|
||||||
|
|
||||||
boldFont := Font{
|
boldFont := Font{
|
||||||
|
|
@ -368,8 +402,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}
|
}
|
||||||
FontFaces[boldFont] = FontFaces[fallbackFont]
|
FontFaces.Set(boldFont, FontFaces.Get(fallbackFont))
|
||||||
FontEncodings[boldFont] = FontEncodings[fallbackFont]
|
FontEncodings.Set(boldFont, FontEncodings.Get(fallbackFont))
|
||||||
}
|
}
|
||||||
|
|
||||||
semiboldFont := Font{
|
semiboldFont := Font{
|
||||||
|
|
@ -386,8 +420,8 @@ func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []by
|
||||||
Family: SourceSansPro,
|
Family: SourceSansPro,
|
||||||
Style: FONT_STYLE_SEMIBOLD,
|
Style: FONT_STYLE_SEMIBOLD,
|
||||||
}
|
}
|
||||||
FontFaces[semiboldFont] = FontFaces[fallbackFont]
|
FontFaces.Set(semiboldFont, FontFaces.Get(fallbackFont))
|
||||||
FontEncodings[semiboldFont] = FontEncodings[fallbackFont]
|
FontEncodings.Set(semiboldFont, FontEncodings.Get(fallbackFont))
|
||||||
}
|
}
|
||||||
|
|
||||||
FontFamilies = append(FontFamilies, customFontFamily)
|
FontFamilies = append(FontFamilies, customFontFamily)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,9 @@ func TestCutFont(t *testing.T) {
|
||||||
Family: SourceCodePro,
|
Family: SourceCodePro,
|
||||||
Style: FONT_STYLE_BOLD,
|
Style: FONT_STYLE_BOLD,
|
||||||
}
|
}
|
||||||
fontBuf := make([]byte, len(FontFaces[f]))
|
face := FontFaces.Get(f)
|
||||||
copy(fontBuf, FontFaces[f])
|
fontBuf := make([]byte, len(face))
|
||||||
|
copy(fontBuf, face)
|
||||||
fontBuf = font.UTF8CutFont(fontBuf, " 1")
|
fontBuf = font.UTF8CutFont(fontBuf, " 1")
|
||||||
err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf)
|
err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf)
|
||||||
assert.Success(t, err)
|
assert.Success(t, err)
|
||||||
|
|
|
||||||
4
d2renderers/d2latex/mathjax.js
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor();
|
const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor();
|
||||||
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
|
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
|
||||||
const html = MathJax._.mathjax.mathjax.document('', {
|
const html = MathJax._.mathjax.mathjax.document('', {
|
||||||
InputJax: new MathJax._.input.tex_ts.TeX({ packages: ['base', 'mathtools', 'amscd', 'braket', 'cancel', 'cases', 'color', 'gensymb', 'mhchem', 'physics'] }),
|
InputJax: new MathJax._.input.tex_ts.TeX({ packages: ['base', 'mathtools', 'ams', 'amscd', 'braket', 'cancel', 'cases', 'color', 'gensymb', 'mhchem', 'physics'] }),
|
||||||
OutputJax: new MathJax._.output.svg_ts.SVG(),
|
OutputJax: new MathJax._.output.svg_ts.SVG(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3902229455 .text-bold {
|
.d2-3902229455 .text-bold {
|
||||||
font-family: "d2-3902229455-font-bold";
|
font-family: "d2-3902229455-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3902229455 .text-bold {
|
.d2-3902229455 .text-bold {
|
||||||
font-family: "d2-3902229455-font-bold";
|
font-family: "d2-3902229455-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2916329547 .text {
|
.d2-2916329547 .text {
|
||||||
font-family: "d2-2916329547-font-regular";
|
font-family: "d2-2916329547-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2916329547 .text {
|
.d2-2916329547 .text {
|
||||||
font-family: "d2-2916329547-font-regular";
|
font-family: "d2-2916329547-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3148583989 .text-bold {
|
.d2-3148583989 .text-bold {
|
||||||
font-family: "d2-3148583989-font-bold";
|
font-family: "d2-3148583989-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3148583989 .text-bold {
|
.d2-3148583989 .text-bold {
|
||||||
font-family: "d2-3148583989-font-bold";
|
font-family: "d2-3148583989-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-327680947 .text-bold {
|
.d2-327680947 .text-bold {
|
||||||
font-family: "d2-327680947-font-bold";
|
font-family: "d2-327680947-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-327680947 .text-bold {
|
.d2-327680947 .text-bold {
|
||||||
font-family: "d2-327680947-font-bold";
|
font-family: "d2-327680947-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-914436609 .text {
|
.d2-914436609 .text {
|
||||||
font-family: "d2-914436609-font-regular";
|
font-family: "d2-914436609-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-914436609 .text {
|
.d2-914436609 .text {
|
||||||
font-family: "d2-914436609-font-regular";
|
font-family: "d2-914436609-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2730605657 .text-mono {
|
.d2-2730605657 .text-mono {
|
||||||
font-family: "d2-2730605657-font-mono";
|
font-family: "d2-2730605657-font-mono";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1467 386"><svg id="d2-svg" class="d2-206057491" width="1467" height="386" viewBox="-101 -101 1467 386"><rect x="-101.000000" y="-101.000000" width="1467.000000" height="386.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1467 386"><svg id="d2-svg" class="d2-206057491" width="1467" height="386" viewBox="-101 -101 1467 386"><rect x="-101.000000" y="-101.000000" width="1467.000000" height="386.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-206057491 .text {
|
.d2-206057491 .text {
|
||||||
font-family: "d2-206057491-font-regular";
|
font-family: "d2-206057491-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2730605657 .text-mono {
|
.d2-2730605657 .text-mono {
|
||||||
font-family: "d2-2730605657-font-mono";
|
font-family: "d2-2730605657-font-mono";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2029734873 .text-bold {
|
.d2-2029734873 .text-bold {
|
||||||
font-family: "d2-2029734873-font-bold";
|
font-family: "d2-2029734873-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2029734873 .text-bold {
|
.d2-2029734873 .text-bold {
|
||||||
font-family: "d2-2029734873-font-bold";
|
font-family: "d2-2029734873-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-341527886 .text-bold {
|
.d2-341527886 .text-bold {
|
||||||
font-family: "d2-341527886-font-bold";
|
font-family: "d2-341527886-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1740 600"><svg id="d2-svg" class="d2-341527886" width="1740" height="600" viewBox="-101 -101 1740 600"><rect x="-101.000000" y="-101.000000" width="1740.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-341527886 .text-bold {
|
.d2-341527886 .text-bold {
|
||||||
font-family: "d2-341527886-font-bold";
|
font-family: "d2-341527886-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 398 285"><svg id="d2-svg" class="d2-1379818723" width="398" height="285" viewBox="-101 -115 398 285"><rect x="-101.000000" y="-115.000000" width="398.000000" height="285.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 398 285"><svg id="d2-svg" class="d2-1379818723" width="398" height="285" viewBox="-101 -115 398 285"><rect x="-101.000000" y="-115.000000" width="398.000000" height="285.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1379818723 .text-bold {
|
.d2-1379818723 .text-bold {
|
||||||
font-family: "d2-1379818723-font-bold";
|
font-family: "d2-1379818723-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-1207082110" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-1207082110" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1207082110 .text-bold {
|
.d2-1207082110 .text-bold {
|
||||||
font-family: "d2-1207082110-font-bold";
|
font-family: "d2-1207082110-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1441 721"><svg id="d2-svg" class="d2-1360573900" width="1441" height="721" viewBox="-101 -112 1441 721"><rect x="-101.000000" y="-112.000000" width="1441.000000" height="721.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1441 721"><svg id="d2-svg" class="d2-1360573900" width="1441" height="721" viewBox="-101 -112 1441 721"><rect x="-101.000000" y="-112.000000" width="1441.000000" height="721.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1360573900 .text-bold {
|
.d2-1360573900 .text-bold {
|
||||||
font-family: "d2-1360573900-font-bold";
|
font-family: "d2-1360573900-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1052 863"><svg id="d2-svg" class="d2-611371411" width="1052" height="863" viewBox="-61 -1 1052 863"><rect x="-61.000000" y="-1.000000" width="1052.000000" height="863.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1052 863"><svg id="d2-svg" class="d2-611371411" width="1052" height="863" viewBox="-61 -1 1052 863"><rect x="-61.000000" y="-1.000000" width="1052.000000" height="863.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-611371411 .text-mono {
|
.d2-611371411 .text-mono {
|
||||||
font-family: "d2-611371411-font-mono";
|
font-family: "d2-611371411-font-mono";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 922 408"><svg id="d2-svg" class="d2-2864094478" width="922" height="408" viewBox="-91 -141 922 408"><rect x="-91.000000" y="-141.000000" width="922.000000" height="408.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 922 408"><svg id="d2-svg" class="d2-2864094478" width="922" height="408" viewBox="-91 -141 922 408"><rect x="-91.000000" y="-141.000000" width="922.000000" height="408.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2864094478 .text {
|
.d2-2864094478 .text {
|
||||||
font-family: "d2-2864094478-font-regular";
|
font-family: "d2-2864094478-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 337 560"><svg id="d2-svg" class="d2-2874225056" width="337" height="560" viewBox="-89 -89 337 560"><rect x="-89.000000" y="-89.000000" width="337.000000" height="560.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 337 560"><svg id="d2-svg" class="d2-2874225056" width="337" height="560" viewBox="-89 -89 337 560"><rect x="-89.000000" y="-89.000000" width="337.000000" height="560.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2874225056 .text-bold {
|
.d2-2874225056 .text-bold {
|
||||||
font-family: "d2-2874225056-font-bold";
|
font-family: "d2-2874225056-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 676 434"><svg id="d2-svg" class="d2-2558489054" width="676" height="434" viewBox="-101 -101 676 434"><rect x="-101.000000" y="-101.000000" width="676.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 676 434"><svg id="d2-svg" class="d2-2558489054" width="676" height="434" viewBox="-101 -101 676 434"><rect x="-101.000000" y="-101.000000" width="676.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2558489054 .text-bold {
|
.d2-2558489054 .text-bold {
|
||||||
font-family: "d2-2558489054-font-bold";
|
font-family: "d2-2558489054-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1098532122 .text {
|
.d2-1098532122 .text {
|
||||||
font-family: "d2-1098532122-font-regular";
|
font-family: "d2-1098532122-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1098532122 .text {
|
.d2-1098532122 .text {
|
||||||
font-family: "d2-1098532122-font-regular";
|
font-family: "d2-1098532122-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-4290168978 .text-bold {
|
.d2-4290168978 .text-bold {
|
||||||
font-family: "d2-4290168978-font-bold";
|
font-family: "d2-4290168978-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 516 636"><svg id="d2-svg" class="d2-3736831304" width="516" height="636" viewBox="-78 -122 516 636"><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" fill="#947A6D" stroke-width="0" /><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" class="paper-overlay" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 516 636"><svg id="d2-svg" class="d2-3736831304" width="516" height="636" viewBox="-78 -122 516 636"><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" fill="#947A6D" stroke-width="0" /><rect x="-78.000000" y="-122.000000" width="516.000000" height="636.000000" rx="0.000000" class="paper-overlay" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3736831304 .text-mono {
|
.d2-3736831304 .text-mono {
|
||||||
font-family: "d2-3736831304-font-mono";
|
font-family: "d2-3736831304-font-mono";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 497 KiB After Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2504113906 .text {
|
.d2-2504113906 .text {
|
||||||
font-family: "d2-2504113906-font-regular";
|
font-family: "d2-2504113906-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2504113906 .text {
|
.d2-2504113906 .text {
|
||||||
font-family: "d2-2504113906-font-regular";
|
font-family: "d2-2504113906-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 832 1627"><svg id="d2-svg" class="d2-790545213" width="832" height="1627" viewBox="-92 -101 832 1627"><rect x="-92.000000" y="-101.000000" width="832.000000" height="1627.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 832 1627"><svg id="d2-svg" class="d2-790545213" width="832" height="1627" viewBox="-92 -101 832 1627"><rect x="-92.000000" y="-101.000000" width="832.000000" height="1627.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-790545213 .text-mono {
|
.d2-790545213 .text-mono {
|
||||||
font-family: "d2-790545213-font-mono";
|
font-family: "d2-790545213-font-mono";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-103900560 .text {
|
.d2-103900560 .text {
|
||||||
font-family: "d2-103900560-font-regular";
|
font-family: "d2-103900560-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 2801 2344"><svg id="d2-svg" class="d2-103900560" width="2801" height="2344" viewBox="-101 -101 2801 2344"><rect x="-101.000000" y="-101.000000" width="2801.000000" height="2344.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-103900560 .text {
|
.d2-103900560 .text {
|
||||||
font-family: "d2-103900560-font-regular";
|
font-family: "d2-103900560-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
|
@ -129,7 +129,7 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
|
||||||
font-family: font-regular;
|
font-family: font-regular;
|
||||||
src: url("%s");
|
src: url("%s");
|
||||||
}
|
}
|
||||||
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)])
|
]]></style>`, d2fonts.FontEncodings.Get(d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)))
|
||||||
}
|
}
|
||||||
if !strings.Contains(svg, `font-family: "font-bold"`) {
|
if !strings.Contains(svg, `font-family: "font-bold"`) {
|
||||||
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
|
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
|
||||||
|
|
@ -140,7 +140,7 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
|
||||||
font-family: font-bold;
|
font-family: font-bold;
|
||||||
src: url("%s");
|
src: url("%s");
|
||||||
}
|
}
|
||||||
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)])
|
]]></style>`, d2fonts.FontEncodings.Get(d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)))
|
||||||
}
|
}
|
||||||
|
|
||||||
closingIndex := strings.LastIndex(svg, "</svg></svg>")
|
closingIndex := strings.LastIndex(svg, "</svg></svg>")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1431 1588"><svg id="d2-svg" class="d2-4070036272" width="1431" height="1588" viewBox="-192 -70 1431 1588"><rect x="-192.000000" y="-70.000000" width="1431" height="1588" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1431 1588"><svg id="d2-svg" class="d2-4070036272" width="1431" height="1588" viewBox="-192 -70 1431 1588"><rect x="-192.000000" y="-70.000000" width="1431" height="1588" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-4070036272 .text {
|
.d2-4070036272 .text {
|
||||||
font-family: "d2-4070036272-font-regular";
|
font-family: "d2-4070036272-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 677 KiB After Width: | Height: | Size: 677 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-3057089836" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-3057089836" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.appendix-icon {
|
.appendix-icon {
|
||||||
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 657 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-3606605273" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-3606605273" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.appendix-icon {
|
.appendix-icon {
|
||||||
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 662 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-4098508292" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 564 682"><svg id="d2-svg" class="d2-4098508292" width="564" height="682" viewBox="-101 -118 564 682"><rect x="-101.000000" y="-118.000000" width="564" height="682" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.appendix-icon {
|
.appendix-icon {
|
||||||
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 662 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-3026443247" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" fill="PaleVioletRed" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-3026443247" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" fill="PaleVioletRed" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.appendix-icon {
|
.appendix-icon {
|
||||||
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 661 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-2257413360" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 565 630"><svg id="d2-svg" class="d2-2257413360" width="565" height="630" viewBox="-101 -118 565 630"><rect x="-101.000000" y="-118.000000" width="565" height="630" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.appendix-icon {
|
.appendix-icon {
|
||||||
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 661 KiB |
|
|
@ -613,6 +613,15 @@ func renderArrowheadLabel(connection d2target.Connection, text string, isDst boo
|
||||||
textEl.X = baselineCenter.X
|
textEl.X = baselineCenter.X
|
||||||
textEl.Y = baselineCenter.Y
|
textEl.Y = baselineCenter.Y
|
||||||
textEl.Fill = d2target.FG_COLOR
|
textEl.Fill = d2target.FG_COLOR
|
||||||
|
if isDst {
|
||||||
|
if connection.DstLabel.Color != "" {
|
||||||
|
textEl.Fill = connection.DstLabel.Color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if connection.SrcLabel.Color != "" {
|
||||||
|
textEl.Fill = connection.SrcLabel.Color
|
||||||
|
}
|
||||||
|
}
|
||||||
textEl.ClassName = "text-italic"
|
textEl.ClassName = "text-italic"
|
||||||
textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
|
textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
|
||||||
textEl.Content = RenderText(text, textEl.X, height)
|
textEl.Content = RenderText(text, textEl.X, height)
|
||||||
|
|
@ -1258,7 +1267,13 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
if !isLight {
|
if !isLight {
|
||||||
class = "dark-code"
|
class = "dark-code"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, `<g transform="translate(%f %f)" class="%s">`, box.TopLeft.X, box.TopLeft.Y, class)
|
var fontSize string
|
||||||
|
if targetShape.FontSize != d2fonts.FONT_SIZE_M {
|
||||||
|
fontSize = fmt.Sprintf(` style="font-size:%v"`, targetShape.FontSize)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(writer, `<g transform="translate(%f %f)" class="%s"%s>`,
|
||||||
|
box.TopLeft.X, box.TopLeft.Y, class, fontSize,
|
||||||
|
)
|
||||||
rectEl := d2themes.NewThemableElement("rect")
|
rectEl := d2themes.NewThemableElement("rect")
|
||||||
rectEl.Width = float64(targetShape.Width)
|
rectEl.Width = float64(targetShape.Width)
|
||||||
rectEl.Height = float64(targetShape.Height)
|
rectEl.Height = float64(targetShape.Height)
|
||||||
|
|
@ -1312,9 +1327,20 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
mdEl := d2themes.NewThemableElement("div")
|
mdEl := d2themes.NewThemableElement("div")
|
||||||
mdEl.ClassName = "md"
|
mdEl.ClassName = "md"
|
||||||
mdEl.Content = render
|
mdEl.Content = render
|
||||||
|
|
||||||
|
// We have to set with styles since within foreignObject, we're in html
|
||||||
|
// land and not SVG attributes
|
||||||
|
var styles []string
|
||||||
if targetShape.FontSize != textmeasure.MarkdownFontSize {
|
if targetShape.FontSize != textmeasure.MarkdownFontSize {
|
||||||
mdEl.Style = fmt.Sprintf("font-size:%vpx", targetShape.FontSize)
|
styles = append(styles, fmt.Sprintf("font-size:%vpx", targetShape.FontSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !color.IsThemeColor(targetShape.Color) {
|
||||||
|
styles = append(styles, fmt.Sprintf(`color:%s`, targetShape.Color))
|
||||||
|
}
|
||||||
|
|
||||||
|
mdEl.Style = strings.Join(styles, ";")
|
||||||
|
|
||||||
fmt.Fprint(writer, mdEl.Render())
|
fmt.Fprint(writer, mdEl.Render())
|
||||||
fmt.Fprint(writer, `</foreignObject></g>`)
|
fmt.Fprint(writer, `</foreignObject></g>`)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1400 710"><svg id="d2-svg" class="d2-3902229455" width="1400" height="710" viewBox="-101 -101 1400 710"><rect x="-101.000000" y="-101.000000" width="1400.000000" height="710.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3902229455 .text-bold {
|
.d2-3902229455 .text-bold {
|
||||||
font-family: "d2-3902229455-font-bold";
|
font-family: "d2-3902229455-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 392 820"><svg id="d2-svg" class="d2-2916329547" width="392" height="820" viewBox="-91 -121 392 820"><rect x="-91.000000" y="-121.000000" width="392.000000" height="820.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2916329547 .text {
|
.d2-2916329547 .text {
|
||||||
font-family: "d2-2916329547-font-regular";
|
font-family: "d2-2916329547-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1245 615"><svg id="d2-svg" class="d2-3148583989" width="1245" height="615" viewBox="-91 -81 1245 615"><rect x="-91.000000" y="-81.000000" width="1245.000000" height="615.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-3148583989 .text-bold {
|
.d2-3148583989 .text-bold {
|
||||||
font-family: "d2-3148583989-font-bold";
|
font-family: "d2-3148583989-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 434"><svg id="d2-svg" class="d2-327680947" width="257" height="434" viewBox="-101 -101 257 434"><rect x="-101.000000" y="-101.000000" width="257.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-327680947 .text-bold {
|
.d2-327680947 .text-bold {
|
||||||
font-family: "d2-327680947-font-bold";
|
font-family: "d2-327680947-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 634"><svg id="d2-svg" class="d2-914436609" width="350" height="634" viewBox="-91 -121 350 634"><rect x="-91.000000" y="-121.000000" width="350.000000" height="634.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-914436609 .text {
|
.d2-914436609 .text {
|
||||||
font-family: "d2-914436609-font-regular";
|
font-family: "d2-914436609-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 624 570"><svg id="d2-svg" class="d2-2730605657" width="624" height="570" viewBox="-101 -101 624 570"><rect x="-101.000000" y="-101.000000" width="624.000000" height="570.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2730605657 .text-mono {
|
.d2-2730605657 .text-mono {
|
||||||
font-family: "d2-2730605657-font-mono";
|
font-family: "d2-2730605657-font-mono";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 985 417"><svg id="d2-svg" class="d2-1533752388" width="985" height="417" viewBox="-101 -101 985 417"><rect x="-101.000000" y="-101.000000" width="985.000000" height="417.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 985 417"><svg id="d2-svg" class="d2-1533752388" width="985" height="417" viewBox="-101 -101 985 417"><rect x="-101.000000" y="-101.000000" width="985.000000" height="417.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1533752388 .text {
|
.d2-1533752388 .text {
|
||||||
font-family: "d2-1533752388-font-regular";
|
font-family: "d2-1533752388-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 257 455"><svg id="d2-svg" class="d2-2029734873" width="257" height="455" viewBox="-101 -101 257 455"><rect x="-101.000000" y="-101.000000" width="257.000000" height="455.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2029734873 .text-bold {
|
.d2-2029734873 .text-bold {
|
||||||
font-family: "d2-2029734873-font-bold";
|
font-family: "d2-2029734873-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1178 477"><svg id="d2-svg" class="d2-1098532122" width="1178" height="477" viewBox="-100 -101 1178 477"><rect x="-100.000000" y="-101.000000" width="1178.000000" height="477.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-1098532122 .text {
|
.d2-1098532122 .text {
|
||||||
font-family: "d2-1098532122-font-regular";
|
font-family: "d2-1098532122-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 758 268"><svg id="d2-svg" class="d2-4290168978" width="758" height="268" viewBox="-101 -101 758 268"><rect x="-101.000000" y="-101.000000" width="758.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-4290168978 .text-bold {
|
.d2-4290168978 .text-bold {
|
||||||
font-family: "d2-4290168978-font-bold";
|
font-family: "d2-4290168978-font-bold";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 1068 662"><svg id="d2-svg" class="d2-2504113906" width="1068" height="662" viewBox="-101 -101 1068 662"><rect x="-101.000000" y="-101.000000" width="1068.000000" height="662.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
|
||||||
.d2-2504113906 .text {
|
.d2-2504113906 .text {
|
||||||
font-family: "d2-2504113906-font-regular";
|
font-family: "d2-2504113906-font-regular";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |